diff --git a/kontor-api/src/apis/base.py b/kontor-api/src/apis/base.py index 9341bf0..ac9d9f1 100644 --- a/kontor-api/src/apis/base.py +++ b/kontor-api/src/apis/base.py @@ -1,12 +1,20 @@ +""" +add router for different parts (like comics, tysc, media) +""" + from fastapi import APIRouter from src.apis.version1 import comic, media, tysc, admin -api_router = APIRouter(prefix="/api") +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(tysc.router, prefix="/tysc", tags=["tysc"]) +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"]) diff --git a/kontor-api/src/apis/version1/fieldposition.py b/kontor-api/src/apis/version1/fieldposition.py new file mode 100644 index 0000000..a5ec1be --- /dev/null +++ b/kontor-api/src/apis/version1/fieldposition.py @@ -0,0 +1,19 @@ +from typing import List + +from fastapi import APIRouter + +from src.db.models.tysc import FieldPosition +from src.db.session import SessionDep +from src.schema.tysc.fieldposition import FieldPositionResponse, to_response + + +router = APIRouter() + +@router.get("/positions") +def get_all_teams(db: SessionDep) -> List[FieldPositionResponse]: + results: list[FieldPositionResponse] = [] + sports = db.query(FieldPosition).all() + for sport in sports: + response = to_response(sport) + results.append(response) + return results diff --git a/kontor-api/src/apis/version1/player.py b/kontor-api/src/apis/version1/player.py new file mode 100644 index 0000000..ddffec8 --- /dev/null +++ b/kontor-api/src/apis/version1/player.py @@ -0,0 +1,19 @@ +from typing import List + +from fastapi import APIRouter + +from src.db.models.tysc import Player +from src.db.session import SessionDep +from src.schema.tysc.player import PlayerResponse, to_response + + +router = APIRouter() + +@router.get("/players") +def get_all_players(db: SessionDep) -> List[PlayerResponse]: + results: List[PlayerResponse] = [] + players = db.query(Player).all() + for player in players: + response = to_response(player) + results.append(response) + return results diff --git a/kontor-api/src/apis/version1/tysc.py b/kontor-api/src/apis/version1/sport.py similarity index 72% rename from kontor-api/src/apis/version1/tysc.py rename to kontor-api/src/apis/version1/sport.py index 9d68288..cdceed5 100644 --- a/kontor-api/src/apis/version1/tysc.py +++ b/kontor-api/src/apis/version1/sport.py @@ -2,7 +2,7 @@ from typing import List from fastapi import APIRouter from src.db.session import SessionDep -from src.schema.tysc.sport import SportResponse +from src.schema.tysc.sport import SportResponse, to_response from src.db.models.tysc import Sport router = APIRouter() @@ -12,5 +12,6 @@ def get_all_sports(db: SessionDep) -> List[SportResponse]: results: list[SportResponse] = [] sports = db.query(Sport).all() for sport in sports: - results.append(SportResponse(id=sport.id, name=sport.name)) + response = to_response(sport) + results.append(response) return results diff --git a/kontor-api/src/apis/version1/team.py b/kontor-api/src/apis/version1/team.py new file mode 100644 index 0000000..3f3acd4 --- /dev/null +++ b/kontor-api/src/apis/version1/team.py @@ -0,0 +1,19 @@ +from typing import List + +from fastapi import APIRouter + +from src.db.models.tysc import Team +from src.db.session import SessionDep +from src.schema.tysc.team import TeamResponse, to_response + + +router = APIRouter() + +@router.get("/teams") +def get_all_teams(db: SessionDep) -> List[TeamResponse]: + results: list[TeamResponse] = [] + sports = db.query(Team).all() + for sport in sports: + response = to_response(sport) + results.append(response) + return results diff --git a/kontor-api/src/apis/version1/vendor.py b/kontor-api/src/apis/version1/vendor.py new file mode 100644 index 0000000..ea2c4f3 --- /dev/null +++ b/kontor-api/src/apis/version1/vendor.py @@ -0,0 +1,22 @@ +from typing import List + +from fastapi import APIRouter + +from src.db.models.tysc import Vendor +from src.db.session import SessionDep +from src.schema.tysc.vendor import VendorResponse, to_response + + +router = APIRouter() + +@router.get("/vendors") +def get_all_vendors(db: SessionDep) -> List[VendorResponse]: + """ + retrieve all vendors as json response. + """ + results: list[VendorResponse] = [] + vendors = db.query(Vendor).all() + for vendor in vendors: + response = to_response(vendor) + results.append(response) + return results diff --git a/kontor-api/src/schema/comics/comic.py b/kontor-api/src/schema/comics/comic.py index b6b89dc..2e06418 100644 --- a/kontor-api/src/schema/comics/comic.py +++ b/kontor-api/src/schema/comics/comic.py @@ -6,6 +6,9 @@ from src.db.models.comic import Comic class ComicResponse(BaseModel): + """ + Pydantic model for returning Comic objects. + """ id: str title: str completed: bool @@ -23,6 +26,9 @@ class ComicDetailsResponse(BaseModel): class ComicSchema(BaseModel): + """ + Pydantic model for uploading Comic object. + """ id: str title: str weblink: Optional[AnyUrl] diff --git a/kontor-api/src/schema/tysc/fieldposition.py b/kontor-api/src/schema/tysc/fieldposition.py new file mode 100644 index 0000000..4d57115 --- /dev/null +++ b/kontor-api/src/schema/tysc/fieldposition.py @@ -0,0 +1,28 @@ +from datetime import datetime + +from pydantic import BaseModel + +from src.db.models.tysc import FieldPosition, Team + + +class FieldPositionResponse(BaseModel): + id: str + created_date: datetime + last_modified_date: datetime + version: int + name: str + short_name: str + sport_id: str + + +def to_response(fieldposition: FieldPosition) -> FieldPositionResponse: + response: FieldPositionResponse = FieldPositionResponse( + id=fieldposition.id, + created_date=fieldposition.created_date, + last_modified_date=fieldposition.last_modified_date, + version=fieldposition.version, + name=fieldposition.name, + short_name=fieldposition.short_name, + sport_id=fieldposition.sport_id + ) + return response diff --git a/kontor-api/src/schema/tysc/player.py b/kontor-api/src/schema/tysc/player.py new file mode 100644 index 0000000..47f9b14 --- /dev/null +++ b/kontor-api/src/schema/tysc/player.py @@ -0,0 +1,25 @@ +from datetime import datetime + +from pydantic import BaseModel + +from src.db.models.tysc import Player + + +class PlayerResponse(BaseModel): + id: str + created_date: datetime + last_modified_date: datetime + version: int + first_name: str + last_name: str + +def to_response(player: Player) -> PlayerResponse: + response: PlayerResponse = PlayerResponse( + id=player.id, + created_date=player.created_date, + last_modified_date=player.last_modified_date, + version=player.version, + first_name=player.first_name, + last_name=player.last_name + ) + return response diff --git a/kontor-api/src/schema/tysc/rooster.py b/kontor-api/src/schema/tysc/rooster.py new file mode 100644 index 0000000..645cce6 --- /dev/null +++ b/kontor-api/src/schema/tysc/rooster.py @@ -0,0 +1,36 @@ +from datetime import datetime + +from pydantic import BaseModel + +from src.db.models.tysc import Rooster + + +class RoosterResponse(BaseModel): + """ + Pydantic model for returning Rooster objects. + """ + id: str + created_date: datetime + last_modified_date: datetime + version: int + year: int + team_id: str + player_id: str + position_id: str + + +def to_reponse(rooster: Rooster) -> RoosterResponse: + """ + convert database object to response object (Pydantic). + """ + response: RoosterResponse = RoosterResponse( + id=rooster.id, + created_date=rooster.created_date, + last_modified_date=rooster.last_modified_date, + version=rooster.version, + year=rooster.year, + team_id=rooster.team_id, + player_id=rooster.player_id, + position_id=rooster.position_id + ) + return response diff --git a/kontor-api/src/schema/tysc/sport.py b/kontor-api/src/schema/tysc/sport.py index 017d169..59da984 100644 --- a/kontor-api/src/schema/tysc/sport.py +++ b/kontor-api/src/schema/tysc/sport.py @@ -1,8 +1,24 @@ -from typing import AnyStr +from datetime import datetime from pydantic import BaseModel +from src.db.models.tysc import Sport + class SportResponse(BaseModel): - id: AnyStr + id: str + created_date: datetime + last_modified_date: datetime + version: int name: str + + +def to_response(sport: Sport) -> SportResponse: + response: SportResponse = SportResponse( + id=sport.id, + created_date=sport.created_date, + last_modified_date=sport.last_modified_date, + version=sport.version, + name=sport.name + ) + return response diff --git a/kontor-api/src/schema/tysc/team.py b/kontor-api/src/schema/tysc/team.py new file mode 100644 index 0000000..abe63a7 --- /dev/null +++ b/kontor-api/src/schema/tysc/team.py @@ -0,0 +1,28 @@ +from datetime import datetime + +from pydantic import BaseModel + +from src.db.models.tysc import Team + + +class TeamResponse(BaseModel): + id: str + created_date: datetime + last_modified_date: datetime + version: int + name: str + short_name: str + sport_id: str + + +def to_response(team: Team) -> TeamResponse: + response: TeamResponse = TeamResponse( + id=team.id, + created_date=team.created_date, + last_modified_date=team.last_modified_date, + version=team.version, + name=team.name, + short_name=team.short_name, + sport_id=team.sport_id + ) + return response diff --git a/kontor-api/src/schema/tysc/vendor.py b/kontor-api/src/schema/tysc/vendor.py new file mode 100644 index 0000000..afea4e5 --- /dev/null +++ b/kontor-api/src/schema/tysc/vendor.py @@ -0,0 +1,32 @@ +""" +class and function for json response objects for Vendor. +""" +from datetime import datetime + +from pydantic import BaseModel + +from src.db.models.tysc import Vendor + + +class VendorResponse(BaseModel): + """ + Pydantic model for Vendor reponse object. + """ + id: str + created_date: datetime + last_modified_date: datetime + version: int + name: str + +def to_response(vendor: Vendor) -> VendorResponse: + """ + convert database object Vendor to response object VendorResponse. + """ + reponse: VendorResponse = VendorResponse( + id=vendor.id, + created_date=vendor.created_date, + last_modified_date=vendor.last_modified_date, + version=vendor.version, + name=vendor.name + ) + return reponse diff --git a/kontor-scripts/Makefile b/kontor-scripts/Makefile deleted file mode 100644 index e66d08d..0000000 --- a/kontor-scripts/Makefile +++ /dev/null @@ -1,15 +0,0 @@ - -clean: - find . -name '*.py[co]' -delete - -test: - python -m pytest \ - -v \ - --cov=kontor \ - --cov-report=term \ - --cov-report=html:coverage-report \ - tests/ - -docker: clean - docker build -t kontor:latest . - diff --git a/kontor-scripts/api.py b/kontor-scripts/api.py new file mode 100644 index 0000000..e37ba58 --- /dev/null +++ b/kontor-scripts/api.py @@ -0,0 +1,166 @@ +from dataclasses import dataclass +import logging +import logging.config +from logging import Logger +from pathlib import Path +from typing import Dict, List, Optional +from uuid import UUID + +from platformdirs import PlatformDirs +import requests +import yaml + + +MAPPING: Dict[str, str] = { + "sport": "api/tysc/sports", + "player": "api/tysc/players", + "team": "api/tysc/teams", + "field_position": "api/tysc/positions", + "rooster": "api/tysc/roosters", + "vendor": "api/tysc/vendors", + "card_set": "api/tysc/cardsets", + "card": "api/tysc/cards", + "artist": "api/comics/artists", + "publisher": "api/comics/publishers", + "worktype": "api/comics/worktypes", + "comic": "api/comics/comics", + "volume": "api/comics/volumes", + "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": "", + "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": "", +} + + +@dataclass +class Login: + """ + Dataclass to store login information. + """ + + email: str + password: str + + +@dataclass +class Server: + """ + Dataclass to represent a Kontor-API instance. + """ + + name: str + url: str + token: str + token_type: str + timeout: int + + def login(self, login: Login, log: Logger): + """ + get token from server by calling login endpoint. + """ + if not self.token: + log.info("Call login first") + login_url = f"{self.url}/login" + login_data = {} + login_data["email"] = login.email + login_data["password"] = login.password + response = requests.post(login_url, json=login_data, timeout=self.timeout) + status = response.status_code + log.info(f"Status: {status}") + if status != 200: + log.fatal("authentication failed") + return + data = response.json() + log.debug(f"got data: {data}") + token = data["access_token"] + token_type = data["token_type"] + self.token = str(token) + self.token_type = str(token_type) + + def request(self, log: Logger, table: str, param: Optional[str] = None): + if not param: + url: str = f"{self.url}/{MAPPING[table]}" + else: + 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}") + data = response.json() + return data + + def update(self, log: Logger, table: str, item_id: UUID, file_info: dict): + url: str = f"{self.url}/{MAPPING[table]}/{item_id}" + headers: Dict[str, str] = {"Authorization": f"Bearer {self.token}"} + update = requests.put( + url, headers=headers, json=file_info, timeout=self.timeout + ) + log.info(f"Status: {update.status_code}") + return update + + +@dataclass +class ApiConfig: + """ + Dataclass to define required contents of configuration file. + """ + + login: Login + server: List[Server] + + +def get_logger(level, config: str): + """ + get Logger according to given log level by verbosity. + """ + dirs = PlatformDirs(config) + logging_config = Path(dirs.user_config_dir, "logging-config.yaml") + log_config = None + with open(logging_config, "rt", encoding="utf-8") as f: + log_config = yaml.safe_load(f.read()) + logging.config.dictConfig(log_config) + logger = logging.getLogger("development") + if level is not None: + match level: + case 0: + logger.setLevel(logging.CRITICAL) + case 1: + logger.setLevel(logging.INFO) + case 2: + logger.setLevel(logging.DEBUG) + case _: + logger.setLevel(logging.INFO) + return logger + + +def get_api_config(log: Logger, config: str) -> ApiConfig: + dirs = PlatformDirs(config) + api_config = Path(dirs.user_config_dir, "api.yaml") + with open(api_config, "rt") as f: + api_data = yaml.safe_load(f.read()) + servers = [Server(**server) for server in api_data["server"]] + login = Login(**(api_data["login"])) + apiConfig = ApiConfig(server=servers, login=login) + log.debug(apiConfig) + if not api_data: + log.fatal("API configuration is missing") + return apiConfig + for server in apiConfig.server: + server.login(apiConfig.login, log) + with open(api_config, "w") as f: + yaml.dump(api_data, f) + return apiConfig diff --git a/kontor-scripts/download.py b/kontor-scripts/download.py index c65ec4b..3c7d4f6 100644 --- a/kontor-scripts/download.py +++ b/kontor-scripts/download.py @@ -2,7 +2,6 @@ download files with URLs from DB """ -import os import re import subprocess import sys @@ -10,16 +9,17 @@ from argparse import ArgumentDefaultsHelpFormatter, ArgumentParser from datetime import datetime from enum import Enum, auto from pathlib import Path -from typing import Any, Dict +from typing import Dict +from logging import Logger from uuid import UUID import requests -from config import get_api_config, get_logger +from api import Server, get_api_config, get_logger parser = ArgumentParser(formatter_class=ArgumentDefaultsHelpFormatter) parser.add_argument("--verbose", "-v", action="count", default=0) -parser.add_argument("--config", "-c", default="kontor-docker") +parser.add_argument("--config", "-c", default="kontor-api") parser.add_argument("--dir", "-d", default="/data/media") parser.add_argument("--limit", "-l", type=int, help="maximum number of links to check") parser.add_argument("--tool", "-t", default="yt-dlp") @@ -148,15 +148,15 @@ if __name__ == "__main__": match download_status: case FileStatus.DOWNLOADED: rename_file(item) - update_status(file_id, item, api_data) + update_status(file_id, item, server=server, log=log) case FileStatus.RENAMED: log.info("update status") - update_status(file_id, item, api_data) + update_status(file_id, item, server=server, log=log) case FileStatus.UNKNOWN: download_file(link, item, args.dir) rename_file(item) log.info(f"{item}") - update_status(file_id, item, api_data) + update_status(file_id, item, server=server, log=log) log.warning(f"processed {mediafile_index}/{entries_count}") if args.limit and args.limit <= mediafile_index: break diff --git a/kontor-scripts/sync.py b/kontor-scripts/sync.py index 3f42f2e..e84886a 100644 --- a/kontor-scripts/sync.py +++ b/kontor-scripts/sync.py @@ -1,14 +1,6 @@ from argparse import ArgumentDefaultsHelpFormatter, ArgumentParser from dataclasses import dataclass -import logging -import logging.config -from logging import Logger -from pathlib import Path -from typing import Dict, List - -from platformdirs import PlatformDirs -import requests -import yaml +from api import get_logger, get_api_config parser = ArgumentParser(formatter_class=ArgumentDefaultsHelpFormatter) @@ -19,99 +11,11 @@ parser.add_argument("--cleanup", "-d", action="store_true") args = parser.parse_args() -@dataclass -class Login: - email: str - password: str - - -@dataclass -class Server: - name: str - url: str - token: str - token_type: str - - def login(self, login: Login, log: Logger): - if not self.token: - log.info("Call login first") - login_url = f"{self.url}/login" - login_data = {} - login_data["email"] = login.email - login_data["password"] = login.password - response = requests.post(login_url, json=login_data) - status = response.status_code - log.info(f"Status: {status}") - if status != 200: - log.fatal("authentication failed") - return - data = response.json() - log.debug(f"got data: {data}") - token = data["access_token"] - token_type = data["token_type"] - self.token = str(token) - self.token_type = str(token_type) - - def request(self, log: Logger): - url: str = f"{self.url}/api/media/files" - headers: Dict[str, str] = {"Authorization": f"Bearer {self.token}"} - response = requests.get(url, headers=headers) - log.info(f"Status: {response.status_code}") - data = response.json() - return data - - -@dataclass -class ApiConfig: - login: Login - server: List[Server] - -def get_logger(level, config: str): - dirs = PlatformDirs(config) - logging_config = Path(dirs.user_config_dir, "logging-config.yaml") - log_config = None - with open(logging_config, "rt") as f: - log_config = yaml.safe_load(f.read()) - logging.config.dictConfig(log_config) - logger = logging.getLogger("development") - if level is not None: - match level: - case 0: - logger.setLevel(logging.CRITICAL) - case 1: - logger.setLevel(logging.INFO) - case 2: - logger.setLevel(logging.DEBUG) - case _: - logger.setLevel(logging.INFO) - return logger - - -def get_api_config(log: Logger, config: str) -> ApiConfig: - # api_data: Dict[str, Any] = {} - dirs = PlatformDirs(config) - api_config = Path(dirs.user_config_dir, "api.yaml") - with open(api_config, "rt") as f: - api_data = yaml.safe_load(f.read()) - servers = [Server(**server) for server in api_data['server']] - login = Login(**(api_data["login"])) - apiConfig = ApiConfig(server=servers, login=login) - log.info(apiConfig) - if not api_data: - log.fatal("API configuration is missing") - return apiConfig - for server in apiConfig.server: - server.login(apiConfig.login, log) - with open(api_config, "w") as f: - yaml.dump(api_data, f) - return apiConfig - - 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) + data = server.request(logger, "media_file") logger.info(len(data)) logger.info("kontor.sync finished")