add missing endpoints

This commit is contained in:
Thomas Peetz
2026-05-18 16:45:56 +02:00
committed by Thomas Peetz
parent b2cf1c4698
commit fb289ab298
30 changed files with 335 additions and 71 deletions
+23 -12
View File
@@ -2,19 +2,30 @@
add router for different parts (like comics, tysc, media)
"""
from fastapi import APIRouter
from fastapi import APIRouter, Depends
from src.apis.version1.admin import token
from src.apis.version1.comics import artist, comic, issue
from src.apis.version1.media import mediaactor, mediaactorfile, mediafile
from src.apis.version1.tysc import card, cardset, fieldposition, player, rooster, sport, team, vendor
from src.core.security import get_current_user_from_token
from src.apis.version1 import comic, media, tysc, admin
api_router = APIRouter(prefix="/api")
api_router.include_router(comic.router, prefix="/comics", tags=["comics"])
api_router.include_router(mediafile.router, prefix="/media", tags=["media"])
api_router.include_router(mediaactor.router, prefix="/media", tags=["media"])
api_router.include_router(mediaactorfile.router, prefix="/media", tags=["media"])
api_router.include_router(sport.router, prefix="/tysc", tags=["tysc"])
api_router.include_router(player.router, prefix="/tysc", tags=["tysc"])
api_router.include_router(team.router, prefix="/tysc", tags=["tysc"])
api_router.include_router(fieldposition.router, prefix="/tysc", tags=["tysc"])
api_router.include_router(vendor.router, prefix="/tysc", tags=["tysc"])
api_router.include_router(admin.router, prefix="/login", tags=["login"])
api_router.include_router(user.router, prefix="/user", tags=["user"])
api_router.include_router(comic.router, prefix="/comics", tags=["comics"], dependencies=[Depends(get_current_user_from_token)])
api_router.include_router(artist.router, prefix="/comics", tags=["comics"], dependencies=[Depends(get_current_user_from_token)])
api_router.include_router(issue.router, prefix="/comics", tags=["comics"], dependencies=[Depends(get_current_user_from_token)])
api_router.include_router(mediafile.router, prefix="/media", tags=["media"], dependencies=[Depends(get_current_user_from_token)])
api_router.include_router(mediaactor.router, prefix="/media", tags=["media"], dependencies=[Depends(get_current_user_from_token)])
api_router.include_router(mediaactorfile.router, prefix="/media", tags=["media"], dependencies=[Depends(get_current_user_from_token)])
api_router.include_router(sport.router, prefix="/tysc", tags=["tysc"], dependencies=[Depends(get_current_user_from_token)])
api_router.include_router(player.router, prefix="/tysc", tags=["tysc"], dependencies=[Depends(get_current_user_from_token)])
api_router.include_router(team.router, prefix="/tysc", tags=["tysc"], dependencies=[Depends(get_current_user_from_token)])
api_router.include_router(fieldposition.router, prefix="/tysc", tags=["tysc"], dependencies=[Depends(get_current_user_from_token)])
api_router.include_router(rooster.router, prefix="/tysc", tags=["tysc"], dependencies=[Depends(get_current_user_from_token)])
api_router.include_router(vendor.router, prefix="/tysc", tags=["tysc"], dependencies=[Depends(get_current_user_from_token)])
api_router.include_router(cardset.router, prefix="/tysc", tags=["tysc"], dependencies=[Depends(get_current_user_from_token)])
api_router.include_router(card.router, prefix="/tysc", tags=["tysc"], dependencies=[Depends(get_current_user_from_token)])
api_router.include_router(article.router, prefix="/bookshelf", tags=["bookshelf"], dependencies=[Depends(get_current_user_from_token)])
api_router.include_router(token.router, prefix="/login", tags=["login"])
api_router.include_router(profile.router, prefix="/user", tags=["user"], dependencies=[Depends(get_current_user_from_token)])
@@ -26,7 +26,7 @@ class LoginRequest(BaseModel):
)
def login(request: LoginRequest) -> Token:
logger.info(f"login with {request.email}")
user = authenticate_user_by_email(request.email, request.password)
user = authenticate_user_by_email(str(request.email), str(request.password))
scopes = ["admin", "read"]
if not user:
raise HTTPException(
@@ -0,0 +1,20 @@
from typing import List
from fastapi import APIRouter
from src.db.models.bookshelf import Article
from src.db.session import SessionDep
from src.schema.bookshelf.article import ArticleResponse, to_response
router = APIRouter()
@router.get("/articles", response_model=List[ArticleResponse])
def get_all_artists(db: SessionDep) -> List[ArticleResponse]:
results: List[ArticleResponse] = []
articles = db.query(Article).all()
for article in articles:
response = to_response(article)
results.append(response)
return results
@@ -0,0 +1,43 @@
from typing import List
from fastapi import APIRouter, HTTPException, status
from src.db.models.comic import Artist
from src.db.repository.comics.artist import get_artist_details
from src.db.session import SessionDep
from src.schema.comics.artist import ArtistCreation, ArtistResponse
from src.schema.comics.artist_details import ArtistDetailResponse
router = APIRouter()
@router.get("/artists", response_model=List[ArtistResponse])
def get_all_artists(db: SessionDep) -> List[ArtistResponse]:
results: List[ArtistResponse] = []
artists = db.query(Artist).all()
for artist in artists:
results.append(ArtistResponse(id=artist.id, name=str(artist.name)))
return results
@router.get("/artists/{artist_id}", response_model=ArtistDetailResponse)
def get_artist(artist_id: str, db: SessionDep) -> ArtistDetailResponse:
artist = db.get(Artist, artist_id)
if artist is None:
raise HTTPException(status_code=404, detail="Artist could not be found")
response: ArtistDetailResponse = get_artist_details(artist)
return response
@router.post("/artists", status_code=status.HTTP_201_CREATED)
def add_artist(db: SessionDep, artist_creation: ArtistCreation) -> ArtistResponse:
artist: Artist = Artist()
setattr(artist, "name", artist_creation.name)
try:
db.add(artist)
db.commit()
except:
raise HTTPException(status_code=409, detail="Artist already added")
response = ArtistResponse(id=artist.id, name=str(artist.name))
return response
@@ -13,7 +13,7 @@ router = APIRouter()
@router.get("/comics")
def get_all_comics(db: SessionDep) -> List[ComicResponse]: # type: ignore
def get_all_comics(db: SessionDep) -> List[ComicResponse]:
results: List[ComicResponse] = []
comics = list_comics(db)
for comic in comics:
@@ -0,0 +1,19 @@
from typing import List
from fastapi import APIRouter
from src.db.models.comic import Issue
from src.db.repository.comics.comic import get_issue_details
from src.db.session import SessionDep
from src.schema.comics.issue_details import IssueDetailsResponse
router = APIRouter()
@router.get("/issues", response_model=List[IssueDetailsResponse])
def get_issues(db: SessionDep) -> List[IssueDetailsResponse]:
results: List[IssueDetailsResponse] = []
issues = db.query(Issue).all()
for issue in issues:
results.append(get_issue_details(issue))
return results
@@ -1,9 +1,8 @@
from typing import List
from fastapi import APIRouter, status, HTTPException, Depends
from sqlalchemy import select, Sequence
from fastapi import APIRouter, status, HTTPException
from sqlalchemy import select
from src.core.log_conf import logger
from src.core.security import UserDep, get_current_user_from_token
from src.db.repository.media import (
create_new_mediaactorfile,
create_new_mediafile,
@@ -34,11 +33,7 @@ def update_titles(db: SessionDep) -> list[MediaFileResponse]:
return results
@router.get(
"/files",
response_model=list[MediaFileResponse],
dependencies=[Depends(get_current_user_from_token)],
)
@router.get("/files", response_model=list[MediaFileResponse])
def get_all_files(
db: SessionDep, review: bool = False, download: bool = False
) -> List[MediaFileResponse]:
@@ -59,11 +54,7 @@ def get_all_files(
return results
@router.get(
"/files/{file_id}",
response_model=MediaFileResponse,
dependencies=[Depends(get_current_user_from_token)],
)
@router.get("/files/{file_id}", response_model=MediaFileResponse)
def get_file(file_id: str, db: SessionDep) -> MediaFileResponse:
"""
Get MediaFile with given id or return HTTPException.
+19
View File
@@ -0,0 +1,19 @@
from typing import List
from fastapi import APIRouter
from src.db.models.tysc import Card
from src.db.session import SessionDep
from src.schema.tysc.card import CardResponse, to_response
router = APIRouter()
@router.get("/cards")
def get_all_players(db: SessionDep) -> List[CardResponse]:
results: List[CardResponse] = []
cards = db.query(Card).all()
for card in cards:
response = to_response(card)
results.append(response)
return results
@@ -0,0 +1,19 @@
from typing import List
from fastapi import APIRouter
from src.db.models.tysc import CardSet
from src.db.session import SessionDep
from src.schema.tysc.cardset import CardSetResponse, to_response
router = APIRouter()
@router.get("/cardsets")
def get_all_players(db: SessionDep) -> List[CardSetResponse]:
results: List[CardSetResponse] = []
cardsets = db.query(CardSet).all()
for cardset in cardsets:
response = to_response(cardset)
results.append(response)
return results
@@ -0,0 +1,19 @@
from typing import List
from fastapi import APIRouter
from src.db.models.tysc import Rooster
from src.db.session import SessionDep
from src.schema.tysc.rooster import RoosterResponse, to_response
router = APIRouter()
@router.get("/roosters")
def get_all_sports(db: SessionDep) -> List[RoosterResponse]:
results: list[RoosterResponse] = []
roosters = db.query(Rooster).all()
for rooster in roosters:
response = to_response(rooster)
results.append(response)
return results
+24 -22
View File
@@ -1,50 +1,52 @@
from typing import List
from sqlalchemy import Column, ForeignKey, Integer, String
from sqlalchemy.orm import relationship
from sqlalchemy.orm import Mapped, mapped_column, relationship
from src.db.models.base import Base, BaseMixin
class Article(Base, BaseMixin):
__tablename__ = 'article'
title = Column(String, unique=True)
title: Mapped[str] = mapped_column(unique=True)
article_authors = relationship("ArticleAuthor")
class Author(Base, BaseMixin):
__tablename__ = 'author'
first_name = Column(String)
last_name = Column(String)
article_authors = relationship("ArticleAuthor")
book_authors = relationship("BookAuthor")
first_name: Mapped[str]
last_name: Mapped[str]
article_authors: Mapped[List["ArticleAuthor"]] = relationship(back_populates="author")
book_authors: Mapped[List["BookAuthor"]] = relationship(back_populates="author")
class BookshelfPublisher(Base, BaseMixin):
__tablename__ = 'bookshelf_publisher'
name = Column(String, unique=True)
books = relationship("Book")
name: Mapped[str] = mapped_column(unique=True)
books: Mapped[List["Book"]] = relationship(back_populates="publisher")
class Book(Base, BaseMixin):
__tablename__ = 'book'
isbn = Column(String, unique=True)
title = Column(String)
year = Column(Integer, nullable=False)
publisher_id = Column(String, ForeignKey('bookshelf_publisher.id'), nullable=False)
publisher = relationship('BookshelfPublisher', back_populates="books")
book_authors = relationship("BookAuthor")
isbn: Mapped[str] = mapped_column(unique=True)
title: Mapped[str]
year: Mapped[int] = mapped_column(nullable=False)
publisher_id: Mapped[str] = mapped_column(ForeignKey("bookshelf_publisher.id"), nullable=False)
publisher: Mapped[BookshelfPublisher] = relationship(back_populates="books")
book_authors: Mapped[List["BookAuthor"]] = relationship(back_populates="book")
class ArticleAuthor(Base, BaseMixin):
__tablename__ = 'article_author'
article_id = Column(String, ForeignKey('article.id'), nullable=False)
article = relationship('Article', back_populates="article_authors")
author_id = Column(String, ForeignKey('author.id'), nullable=False)
author = relationship('Author', back_populates="article_authors")
article_id: Mapped[str] = mapped_column(ForeignKey("article.id"), nullable=False)
article: Mapped[Article] = relationship(back_populates="article_authors")
author_id: Mapped[str] = mapped_column(ForeignKey("author.id"), nullable=False)
author: Mapped[Author] = relationship(back_populates="article_authors")
class BookAuthor(Base, BaseMixin):
__tablename__ = 'book_author'
author_id = Column(String, ForeignKey('author.id'), nullable=False)
author = relationship('Author', back_populates="book_authors")
book_id = Column(String, ForeignKey('book.id'), nullable=False)
book = relationship('Book', back_populates="book_authors")
author_id: Mapped[str] = mapped_column(ForeignKey("author.id"), nullable=False)
author: Mapped[Author] = relationship(back_populates="book_authors")
book_id: Mapped[str] = mapped_column(ForeignKey("book.id"), nullable=False)
book: Mapped[Book] = relationship(back_populates="book_authors")
@@ -14,6 +14,9 @@ def list_comics(db: Session) -> List[Type[Comic]]:
def get_issue_details(issue: Issue) -> IssueDetailsResponse:
volume = None
if issue.volume:
volume = VolumeResponse(id=issue.volume.id, name=issue.volume.name)
response = IssueDetailsResponse(
id=issue.id,
issue_number=issue.issue_number,
@@ -0,0 +1,23 @@
from datetime import datetime
from pydantic import BaseModel
from src.db.models.bookshelf import Article
class ArticleResponse(BaseModel):
id: str
created_date: datetime
last_modified_date: datetime
version: int
title: str
def to_response(article: Article) -> ArticleResponse:
response: ArticleResponse = ArticleResponse(
id=article.id,
created_date=article.created_date,
last_modified_date=article.last_modified_date,
version=article.version,
title=article.title
)
return response
+31
View File
@@ -0,0 +1,31 @@
from datetime import datetime
from pydantic import BaseModel
from src.db.models.tysc import Card
class CardResponse(BaseModel):
id: str
created_date: datetime
last_modified_date: datetime
version: int
card_number: int
year: int
card_set_id: str
rooster_id: str
vendor_id: str
def to_response(card: Card) -> CardResponse:
response: CardResponse = CardResponse(
id=card.id,
created_date=card.created_date,
last_modified_date=card.last_modified_date,
version=card.version,
card_number=card.card_number,
year=card.year,
card_set_id=card.card_set_id,
rooster_id=card.rooster_id,
vendor_id=card.vendor_id
)
return response
+30
View File
@@ -0,0 +1,30 @@
from datetime import datetime
from pydantic import BaseModel
from src.db.models.tysc import CardSet
class CardSetResponse(BaseModel):
id: str
created_date: datetime
last_modified_date: datetime
version: int
name: str
parallel_set: bool
insert_set: bool
vendor_id: str
def to_response(cardset: CardSet) -> CardSetResponse:
response: CardSetResponse = CardSetResponse(
id=cardset.id,
created_date=cardset.created_date,
last_modified_date=cardset.last_modified_date,
version=cardset.version,
name=cardset.name,
parallel_set=cardset.parallel_set,
insert_set=cardset.insert_set,
vendor_id=cardset.vendor_id
)
return response
+1 -1
View File
@@ -2,7 +2,7 @@ from datetime import datetime
from pydantic import BaseModel
from src.db.models.tysc import FieldPosition, Team
from src.db.models.tysc import FieldPosition
class FieldPositionResponse(BaseModel):
+1 -1
View File
@@ -19,7 +19,7 @@ class RoosterResponse(BaseModel):
position_id: str
def to_reponse(rooster: Rooster) -> RoosterResponse:
def to_response(rooster: Rooster) -> RoosterResponse:
"""
convert database object to response object (Pydantic).
"""
+3 -1
View File
@@ -30,6 +30,8 @@ async def login(request: Request, db: Session = Depends(get_db)):
return response
except HTTPException:
form.__dict__.update(msg="")
form.__dict__.get("errors").append("Incorrect Email or Password")
errors = form.__dict__.get("errors")
if errors:
errors.append("Incorrect Email or Password")
return templates.TemplateResponse("auth/login.html", form.__dict__)
return templates.TemplateResponse("auth/login.html", form.__dict__)
+31 -13
View File
@@ -28,24 +28,30 @@ MAPPING: Dict[str, str] = {
"story_arc": "api/comics/storyarcs",
"issue": "api/comics/issues",
"issue_work": "api/comics/issueworks",
"article": "",
"bookshelf_publisher": "",
"book": "",
"author": "",
"article_author": "",
"book_author": "",
"media_article": "",
"article": "api/bookshelf/articles",
"bookshelf_publisher": "api/bookshelf/publishers",
"book": "api/bookshelf/books",
"author": "api/bookshelf/authors",
"article_author": "api/bookshelf/articleauthors",
"book_author": "api/bookshelf/bookauthors",
"media_article": "api/media/articles",
"media_video": "api/media/videos",
"media_file": "api/media/files",
"media_actor": "api/media/actors",
"media_actor_file": "api/media/actorfiles",
"profile": "",
"permission": "",
"assignment": "",
"token": "",
"mail_account": "",
"profile": "api/user/profiles",
"permission": "api/user/permissions",
"assignment": "api/user/assignments",
"token": "api/user/tokens",
"mail_account": "api/admin/mailaccount",
}
class EndPointNotAvailableException(Exception):
"""
Raised when calling an not existing endpoint.
"""
pass
@dataclass
class Login:
@@ -99,7 +105,9 @@ class Server:
url: str = f"{self.url}/{MAPPING[table]}?{param}"
headers: Dict[str, str] = {"Authorization": f"Bearer {self.token}"}
response = requests.get(url, headers=headers, timeout=self.timeout)
log.info(f"Status: {response.status_code}")
log.debug(f"Status: {response.status_code}")
if response.status_code==404:
raise EndPointNotAvailableException
data = response.json()
return data
@@ -121,6 +129,16 @@ class ApiConfig:
login: Login
server: List[Server]
def get_server(self, server_name: str) -> Optional[Server]:
"""
"""
found_server = None
for server in self.server:
if server.name == server_name:
found_server = server
return found_server
def get_logger(level, config: str):
+1 -1
View File
@@ -1,4 +1,4 @@
from typing import Any, AnyStr, Dict
from typing import Any, Dict
from sqlalchemy import Column, ForeignKey, Integer, String
from sqlalchemy.orm import relationship
+19 -5
View File
@@ -1,12 +1,13 @@
from argparse import ArgumentDefaultsHelpFormatter, ArgumentParser
from dataclasses import dataclass
from api import get_logger, get_api_config
from typing import List
from api import MAPPING, EndPointNotAvailableException, Server, get_logger, get_api_config
parser = ArgumentParser(formatter_class=ArgumentDefaultsHelpFormatter)
parser.add_argument("--verbose", "-v", action="count", default=0)
parser.add_argument("--config", "-c", default="kontor-api")
parser.add_argument("--dry-run", "-m", action="store_true")
parser.add_argument("--server", "-s")
parser.add_argument("--cleanup", "-d", action="store_true")
args = parser.parse_args()
@@ -15,7 +16,20 @@ if __name__== "__main__":
logger = get_logger(args.verbose, "kontor")
logger.info("kontor.sync started")
apiConfig = get_api_config(logger, args.config)
for server in apiConfig.server:
data = server.request(logger, "media_file")
logger.info(len(data))
server_list: List[Server] = []
if args.server:
server = apiConfig.get_server(args.server)
if server:
server_list.append(server)
else:
server_list.extend(apiConfig.server)
for server in server_list:
for table, path in MAPPING.items():
try:
data = server.request(logger, table=table)
logger.info("%s: %s", table, len(data))
if len(data) == 1:
logger.info("show data: %s", data)
except EndPointNotAvailableException:
logger.info("Endpoint not implemented")
logger.info("kontor.sync finished")