add display for MetaData

This commit is contained in:
Thomas Peetz
2025-05-04 12:27:03 +02:00
committed by Thomas Peetz
parent aa375573de
commit 30f9e89c60
13 changed files with 269 additions and 26 deletions
+40
View File
@@ -1,4 +1,13 @@
from typing import Annotated
from typing import Dict
from typing import Optional
from fastapi import HTTPException
from fastapi import Request
from fastapi import status
from fastapi.openapi.models import OAuthFlows as OAuthFlowsModel
from fastapi.security import OAuth2
from fastapi.security.utils import get_authorization_scheme_param
from fastapi import Depends
from sqlalchemy.orm import Session
@@ -6,3 +15,34 @@ from sqlalchemy.orm import Session
from src.db.session import get_db
SessionDep = Annotated[Session, Depends(get_db)]
class OAuth2PasswordBearerWithCookie(OAuth2):
def __init__(
self,
tokenUrl: str,
scheme_name: Optional[str] = None,
scopes: Optional[Dict[str, str]] = None,
auto_error: bool = True,
):
if not scopes:
scopes = {}
flows = OAuthFlowsModel(password={"tokenUrl": tokenUrl, "scopes": scopes})
super().__init__(flows=flows, scheme_name=scheme_name, auto_error=auto_error)
async def __call__(self, request: Request) -> Optional[str]:
authorization: str = request.cookies.get(
"access_token"
) # changed to accept access token from httpOnly Cookie
scheme, param = get_authorization_scheme_param(authorization)
if not authorization or scheme.lower() != "bearer":
if self.auto_error:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Not authenticated",
headers={"WWW-Authenticate": "Bearer"},
)
else:
return None
return param
+69
View File
@@ -0,0 +1,69 @@
import logging
from datetime import timedelta
import bcrypt
from fastapi import APIRouter, HTTPException, status, Response, Depends
from fastapi.security import OAuth2PasswordRequestForm
from jose import jwt, JWTError
from src.apis.utils import SessionDep, OAuth2PasswordBearerWithCookie
from src.core.config import settings
from src.core.security import create_access_token
from src.db.models.admin import Profile
from src.db.repository.admin import get_profile
from src.schema.admin import Token
router = APIRouter()
def authenticate_user(username: str, password: str, db: SessionDep) -> Profile | None:
user = get_profile(username=username, db=db)
print(user)
if not user:
return None
if bcrypt.checkpw(password.encode(), user.password.encode()):
print("User successful authenticated")
else:
logging.info("Authentication failed!")
return user
@router.post("/token", response_model=Token)
def login_for_access_token(response: Response, db: SessionDep, form_data: OAuth2PasswordRequestForm = Depends()):
user = authenticate_user(form_data.username, form_data.password, db)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
)
access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
access_token = create_access_token(
data={"sub": user.email}, expires_delta=access_token_expires
)
response.set_cookie(
key="access_token", value=f"Bearer {access_token}", httponly=True
)
return {"access_token": access_token, "token_type": "bearer"}
oauth2_scheme = OAuth2PasswordBearerWithCookie(tokenUrl="/api/login/token")
def get_current_user_from_token(db: SessionDep, token: str = Depends(oauth2_scheme)):
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
)
try:
payload = jwt.decode(
token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM]
)
username: str = payload.get("sub")
print("username/email extracted is ", username)
if username is None:
raise credentials_exception
except JWTError:
raise credentials_exception
user = get_profile(username=username, db=db)
if user is None:
raise credentials_exception
return user
+1 -5
View File
@@ -8,11 +8,7 @@ from src.schema.comics.artist import ArtistCreation, ArtistDetailResponse, Artis
from src.db.models.comic import Comic, Artist, Issue
from src.schema.comics.issue import IssueDetailsResponse
router = APIRouter(
prefix="/comic",
tags=["comics"],
responses={404: {"description": "Not found"}},
)
router = APIRouter()
@router.get("/comics")
+10 -12
View File
@@ -1,22 +1,20 @@
from typing import List, AnyStr
from uuid import UUID
from fastapi import APIRouter, status, HTTPException
from fastapi import APIRouter, status, HTTPException, Depends
from sqlalchemy import select, Sequence
from src.apis.utils import SessionDep
from src.apis.version1.admin import get_current_user_from_token
from src.db.models.admin import Profile
from src.schema.media.file import MediaFileResponse, Link, get_file_details, set_file
from src.db.models.media import MediaFile
router = APIRouter(
prefix="/media",
tags=["media"]
)
router = APIRouter()
@router.get("/update-titles")
def update_titles(db: SessionDep) -> list[MediaFileResponse]:
results: list[MediaFileResponse] = []
files = db.query(MediaFile).filter(MediaFile.review == 1).all()
files = db.query(MediaFile).filter(MediaFile.review == True).all()
for mediafile in files:
mediafile.update_title()
db.add(mediafile)
@@ -27,13 +25,13 @@ def update_titles(db: SessionDep) -> list[MediaFileResponse]:
@router.get("/files", response_model=List[MediaFileResponse])
def get_all_files(db: SessionDep, review: bool = False, download: bool = False) -> List[MediaFileResponse]:
def get_all_files(db: SessionDep, review: bool = False, download: bool = False, current_user: Profile = Depends(get_current_user_from_token)) -> List[MediaFileResponse]:
results: list[MediaFileResponse] = []
files: Sequence[MediaFile]
if review:
files = db.query(MediaFile).filter(MediaFile.review == 1).all()
files = db.query(MediaFile).filter(MediaFile.review == True).all()
elif download:
files = db.query(MediaFile).filter(MediaFile.should_download == 1).all()
files = db.query(MediaFile).filter(MediaFile.should_download == True).all()
else:
files = db.scalars(select(MediaFile)).all()
for mediafile in files:
@@ -66,8 +64,8 @@ def add_file(new_link: Link, db: SessionDep) -> MediaFileResponse:
try:
mediaFile: MediaFile = MediaFile()
setattr(mediaFile, "url", new_link.url)
setattr(mediaFile, "review", 1)
setattr(mediaFile, "should_download", 1)
setattr(mediaFile, "review", True)
setattr(mediaFile, "should_download", True)
db.add(mediaFile)
db.commit()
except:
+26
View File
@@ -0,0 +1,26 @@
from typing import List
from fastapi import APIRouter
from src.apis.utils import SessionDep
from src.db.models.metadata import MetaDataTable, MetaDataColumn
from src.db.repository.metadata import get_tables, get_columns
from src.schema.admin import MetaDataTableResponse, MetaDataColumnResponse
router = APIRouter()
@router.get("/tables")
def get_meta_data_tables(db: SessionDep) -> List[MetaDataTableResponse]:
tables = db.query(MetaDataTable).all()
response: List[MetaDataTableResponse] = get_tables(tables)
return response
@router.get("/columns")
def get_meta_data_columns(db: SessionDep) -> List[MetaDataColumnResponse]:
columns = db.query(MetaDataColumn).all()
response: List[MetaDataColumnResponse] = get_columns(columns)
return response
+1 -5
View File
@@ -5,11 +5,7 @@ from src.apis.utils import SessionDep
from src.schema.tysc.sport import SportResponse
from src.db.models.tysc import Sport
router = APIRouter(
prefix="/tysc",
tags=["tysc"],
responses={404: {"description": "Not found"}},
)
router = APIRouter()
@router.get("/sports")
def get_all_sports(db: SessionDep) -> List[SportResponse]:
+2 -3
View File
@@ -1,10 +1,9 @@
import json
import logging
import uuid
from datetime import datetime
from enum import Enum, auto
from pathlib import Path
from typing import Any
from typing import Any, List
from sqlalchemy import select
from sqlalchemy.exc import IntegrityError
@@ -360,7 +359,7 @@ class KontorDB:
update_list[link.id] = link.title
return update_list
def get_download_list(self) -> list[uuid.UUID]:
def get_download_list(self) -> List[str]:
download_list = []
__session__ = sessionmaker(self.engine)
_filter = { 'should_download': True}
+32
View File
@@ -0,0 +1,32 @@
from typing import List
from src.db.models.metadata import MetaDataTable, MetaDataColumn
from src.schema.admin import MetaDataTableResponse, MetaDataColumnResponse
def get_tables(tables: List[MetaDataTable]) -> List[MetaDataTableResponse]:
results: List[MetaDataTableResponse] = []
for table in tables:
result = MetaDataTableResponse(id=table.id, name=table.table_name)
results.append(result)
return results
def get_columns(columns: List[MetaDataColumn]) -> List[MetaDataColumnResponse]:
results: List[MetaDataColumnResponse] = []
for column in columns:
result = MetaDataColumnResponse(
id=column.id,
table_name=column.table.table_name,
column_name=column.column_name,
column_sync_name=column.column_sync_name,
column_type=column.column_type,
column_modifier=column.column_modifier,
column_order=column.column_order,
is_shown=column.is_shown,
column_label=column.column_label,
show_filter=column.show_filter,
filter_label=column.filter_label,
ref_column=column.ref_column
)
results.append(result)
return results
+6
View File
@@ -28,6 +28,12 @@ async def lifespan(app: FastAPI):
@asynccontextmanager
async def lifespan(app: FastAPI):
await check_db_connected()
yield
await check_db_disconnected()
@asynccontextmanager
async def lifespan(app: FastAPI):
await check_db_connected()
+26
View File
@@ -0,0 +1,26 @@
from typing import Optional
from pydantic import BaseModel
class Token(BaseModel):
access_token: str
token_type: str
class MetaDataTableResponse(BaseModel):
id: str
name: str
class MetaDataColumnResponse(BaseModel):
id: str
table_name: str
column_name: str
column_sync_name: str
column_type: str
column_modifier: Optional[str]
column_order: int
is_shown: bool
column_label: Optional[str]
show_filter: bool
filter_label: Optional[str]
ref_column: Optional[str]
-1
View File
@@ -2,7 +2,6 @@ from typing import List, Dict
from pydantic import BaseModel
from src.db.models.comic import Artist
@@ -0,0 +1,21 @@
{% extends "shared/base.html" %}
{% block title %}
<title>MetaData</title>
{% endblock %}
{% block content %}
{% with msg=msg %}
{% include "components/alerts.html" %}
{% endwith %}
<div class="container">
{% for table in data %}
<div class="row">
{% with obj=table %}
{% include "components/metadatatable_cards.html" %}
{% endwith %}
<br>
</div>
{% endfor %}
</div>
{% endblock %}
@@ -0,0 +1,35 @@
<div class="card shadow p-3 mb-2 bg-body rounded">
<div class="card-body">
<h5 class="card-title">{{obj.table_name}}</h5>
<table class="table table-hover">
<thead><tr>
<th scope="col">Column Name</th>
<th scope="col">Column Sync Name</th>
<th scope="col">Column Type</th>
<th scope="col">Column Modifier</th>
<th scope="col">Column Order</th>
<th scope="col">Is Shown</th>
<th scope="col">Column Label</th>
<th scope="col">Show Filter</th>
<th scope="col">Filter Label</th>
<th scope="col">Ref Column</th>
</tr></thead>
<tbody>
{% for column in obj.table_columns %}
<tr>
<th scope="row"><a href="/admin/metadata/{{column.id}}">{{column.column_name}}</a></th>
<td>{{column.column_sync_name}}</td>
<td>{{column.column_type}}</td>
<td>{{column.column_modifier}}</td>
<td>{{column.column_order}}</td>
<td>{% with check=column.is_shown %}{% include "components/check.html" %}{% endwith %}</td>
<td>{{column.column_label}}</td>
<td>{% with check=column.show_filter %}{% include "components/check.html" %}{% endwith %}</td>
<td>{{column.filter_label}}</td>
<td>{{column.ref_column}}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>