21 Commits

Author SHA1 Message Date
tpeetz cd033f458d add missing endpoints
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 5s
2026-05-17 19:52:00 +02:00
tpeetz 1b58ec8e27 remove obsolete scripts 2026-05-17 01:08:50 +02:00
Thomas Peetz 85b5168445 fix typo in workflow demo.yaml
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 4s
2026-05-05 18:05:19 +02:00
Thomas Peetz e8763fe635 test gitea actions
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 13s
2026-05-05 18:02:08 +02:00
Thomas Peetz 69e58e6dfb Add environment variable for DB server 2026-05-04 15:21:18 +02:00
Thomas Peetz d8914627a3 move *.service files to subdirectory container and add service file for standalone operations in standalone 2026-05-04 15:02:41 +02:00
Thomas Peetz b256e20491 renamed systemd unit files 2026-04-27 07:08:41 +02:00
tpeetz a8df860752 Merge branch 'feature/44-evaluate-typst-for-documentation' into 'develop/0.3.0'
Resolve "Evaluate Typst for documentation"

Closes #44

See merge request tpeetz/kontor!42
2026-04-26 21:09:54 +02:00
tpeetz 4e88b52dc3 use separate files for chapters 2026-04-26 21:09:06 +02:00
tpeetz 9772c9f554 add subprojekt kontor-doc 2026-04-26 16:08:17 +02:00
tpeetz e81d473a69 Einrichtung Build 2026-04-23 23:14:26 +02:00
tpeetz aca9a1aa4a integrate kontor-angular 2026-04-19 20:05:50 +02:00
tpeetz 57129c2d37 integration von kontor-angular 2026-04-18 00:29:25 +02:00
tpeetz e09f7cddf3 rename Dockerfile to Containerfile and integrate kontor-angular in pod kontor 2026-04-17 00:18:50 +02:00
Thomas Peetz a68dd05794 update kontor-api to reflect api changes 2026-04-15 14:38:38 +02:00
Thomas Peetz 62e22c56c4 update usage of api for login by email or username 2026-04-15 11:32:18 +02:00
Thomas Peetz 7aa6a12044 improve konto-vue/Containerfile by explicitly stating images 2026-04-15 10:26:35 +02:00
tpeetz 39a0759b0b fix OAuth authentication 2026-04-12 20:08:42 +02:00
tpeetz e82f2825ae update systemd service files and add script to copy these files 2026-04-12 17:51:19 +02:00
tpeetz 74d29ae8ef Merge branch 'feature/43-typeerror-not-all-arguments-converted-during-string-formatting' into 'develop/0.3.0'
Resolve "TypeError: not all arguments converted during string formatting"

Closes #43

See merge request tpeetz/kontor!41
2026-04-12 17:14:46 +02:00
tpeetz 614c5f20aa Resolve "TypeError: not all arguments converted during string formatting" 2026-04-12 17:14:46 +02:00
69 changed files with 1761 additions and 1051 deletions
+19
View File
@@ -0,0 +1,19 @@
name: Gitea Actions Demo
run-name: ${{ gitea.actor }} ist testing out Gitea Actions
on: [push]
jobs:
Explore-Gitea-Actions:
runs-on: inky
steps:
- run: echo "The job was automatically triggered by a ${{ gitea.event_name }} event."
- run: echo "This job is now running on a ${{ runner.os }} server hosted by Gitea!"
- run: echo "The name of your branch is ${{ gitea.ref }} and your repository is ${{ gitea.repository }}."
- name: Check out repository code
uses: actions/checkout@v4
- run: echo "The ${{ gitea.repository }} repository has been cloned to the runner."
- run: echo "The workflow is now ready to test your code on the runner."
- name: List files in the directory
run: |
ls ${{ gitea.workspace }}
- run: echo "The job's status is ${{ job.status }}."
@@ -1,5 +1,5 @@
### STAGE 1: Build ### ### STAGE 1: Build ###
FROM node:22.15-alpine AS build FROM docker.io/node:22-alpine AS build
WORKDIR /app WORKDIR /app
COPY package.json package-lock.json ./ COPY package.json package-lock.json ./
RUN npm install RUN npm install
@@ -7,8 +7,9 @@ COPY . .
RUN npm run build RUN npm run build
### STAGE 2: Run ### ### STAGE 2: Run ###
FROM nginx:1.17.1-alpine FROM docker.io/library/nginx:stable-alpine
COPY nginx.conf /etc/nginx/conf.d/default.conf COPY nginx.conf /etc/nginx/conf.d/default.conf
COPY --from=build /app/dist/kontor-angular/browser /usr/share/nginx/html COPY --from=build /app/dist/kontor-angular/browser /usr/share/nginx/html
EXPOSE 80 EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
+1
View File
@@ -1,4 +1,5 @@
server { server {
listen 8800;
# Root-Verzeichnis für den Server setzen (wir kopieren unsere Anwendung hierher) # Root-Verzeichnis für den Server setzen (wir kopieren unsere Anwendung hierher)
root /usr/share/nginx/html; root /usr/share/nginx/html;
+22 -2
View File
@@ -1,12 +1,32 @@
"""
add router for different parts (like comics, tysc, media)
"""
from fastapi import APIRouter from fastapi import APIRouter
from src.apis.version1 import comic, mediaactor, mediafile, mediaactorfile, tysc, admin, user from src.apis.version1 import (
comic,
mediaactor,
mediafile,
mediaactorfile,
sport,
player,
team,
fieldposition,
vendor,
admin,
user,
)
api_router = APIRouter(prefix="/api") api_router = APIRouter(prefix="/api")
api_router.include_router(comic.router, prefix="/comics", tags=["comics"]) api_router.include_router(comic.router, prefix="/comics", tags=["comics"])
api_router.include_router(mediafile.router, prefix="/media", tags=["media"]) api_router.include_router(mediafile.router, prefix="/media", tags=["media"])
api_router.include_router(mediaactor.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(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(admin.router, prefix="/login", tags=["login"])
api_router.include_router(user.router, prefix="/user", tags=["user"]) api_router.include_router(user.router, prefix="/user", tags=["user"])
+5 -3
View File
@@ -5,7 +5,8 @@ from fastapi import APIRouter, Body, HTTPException, status, Depends, Response
from fastapi.security import OAuth2PasswordRequestForm from fastapi.security import OAuth2PasswordRequestForm
from src.core.config import settings from src.core.config import settings
from src.core.security import create_access_token, authenticate_user, get_current_active_user from src.core.log_conf import logger
from src.core.security import create_access_token, authenticate_user_by_email, authenticate_user_by_username, get_current_active_user
from src.db.models.admin import Profile from src.db.models.admin import Profile
from src.schema.admin import Token, ProfileModel from src.schema.admin import Token, ProfileModel
from src.webapps.auth.forms import LoginForm from src.webapps.auth.forms import LoginForm
@@ -15,7 +16,8 @@ router = APIRouter()
@router.post("/token") @router.post("/token")
def login_for_access_token(form_data: Annotated[OAuth2PasswordRequestForm, Depends()]) -> Token: def login_for_access_token(form_data: Annotated[OAuth2PasswordRequestForm, Depends()]) -> Token:
user = authenticate_user(form_data.username, form_data.password) user = authenticate_user_by_username(form_data.username, form_data.password)
logger.info(f"Request /token: login with {form_data.username}")
if not user: if not user:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, status_code=status.HTTP_401_UNAUTHORIZED,
@@ -31,7 +33,7 @@ def login_for_access_token(form_data: Annotated[OAuth2PasswordRequestForm, Depen
# @router.post("/token-cookie", response_model=Token) # @router.post("/token-cookie", response_model=Token)
def login_for_token_cookie(response: Response, form_data: LoginForm = Depends()): def login_for_token_cookie(response: Response, form_data: LoginForm = Depends()):
user = authenticate_user(form_data.username, form_data.password) # type: ignore user = authenticate_user_by_email(form_data.username, form_data.password) # type: ignore
if not user: if not user:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, status_code=status.HTTP_401_UNAUTHORIZED,
@@ -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
+19 -4
View File
@@ -1,11 +1,12 @@
from datetime import timedelta from datetime import timedelta
from fastapi import APIRouter, HTTPException, status from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordRequestForm, OAuth2PasswordBearer, SecurityScopes
from pydantic import BaseModel from pydantic import BaseModel
from typing import Annotated
from src.core.config import settings from src.core.config import settings
from src.core.log_conf import logger from src.core.log_conf import logger
from src.core.security import authenticate_user, create_access_token from src.core.security import authenticate_user_by_email, authenticate_user_by_username, create_access_token
from src.schema.admin import Token from src.schema.admin import Token
login_router = APIRouter() login_router = APIRouter()
@@ -25,7 +26,7 @@ class LoginRequest(BaseModel):
) )
def login(request: LoginRequest) -> Token: def login(request: LoginRequest) -> Token:
logger.info(f"login with {request.email}") logger.info(f"login with {request.email}")
user = authenticate_user(request.email, request.password) user = authenticate_user_by_email(request.email, request.password)
scopes = ["admin", "read"] scopes = ["admin", "read"]
if not user: if not user:
raise HTTPException( raise HTTPException(
@@ -39,3 +40,17 @@ def login(request: LoginRequest) -> Token:
expires_delta=access_token_expires, expires_delta=access_token_expires,
) )
return Token(access_token=access_token, token_type="bearer") return Token(access_token=access_token, token_type="bearer")
@login_router.post("/token", tags=["login"], summary="Login for access token")
async def login_for_access_token(
form_data: Annotated[OAuth2PasswordRequestForm, Depends()],
) -> Token:
user = authenticate_user_by_username(form_data.username, form_data.password)
if not user:
raise HTTPException(status_code=400, detail="Incorrect username or password")
access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
access_token = create_access_token(
data={"sub": user.user_name, "scope": " ".join(form_data.scopes)},
expires_delta=access_token_expires,
)
return Token(access_token=access_token, token_type="bearer")
+19
View File
@@ -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
@@ -2,7 +2,7 @@ from typing import List
from fastapi import APIRouter from fastapi import APIRouter
from src.db.session import SessionDep 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 from src.db.models.tysc import Sport
router = APIRouter() router = APIRouter()
@@ -12,5 +12,6 @@ def get_all_sports(db: SessionDep) -> List[SportResponse]:
results: list[SportResponse] = [] results: list[SportResponse] = []
sports = db.query(Sport).all() sports = db.query(Sport).all()
for sport in sports: for sport in sports:
results.append(SportResponse(id=sport.id, name=sport.name)) response = to_response(sport)
results.append(response)
return results return results
+19
View File
@@ -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
+22
View File
@@ -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
+57 -37
View File
@@ -13,48 +13,47 @@ from pydantic import ValidationError
from src.core.config import settings from src.core.config import settings
from src.core.log_conf import logger from src.core.log_conf import logger
from src.db.models.admin import Profile from src.db.models.admin import Profile
from src.db.repository.admin import get_profile, is_database_empty from src.db.repository.admin import get_profile_by_username, get_profile_by_email, is_database_empty
from src.db.session import SessionLocal from src.db.session import SessionLocal
from src.schema.admin import ProfileModel, TokenData from src.schema.admin import ProfileModel, TokenData
oauth2_scheme = OAuth2PasswordBearer( oauth2_scheme = OAuth2PasswordBearer(
tokenUrl="/api/login/token", tokenUrl="/token",
scopes={"me": "read", "admin": "read"}, scopes={"me": "read", "admin": "read", "ROLE_ADMIN": "admin", "ROLE_MEDIA": "media", "ROLE_USER": "user"},
) )
class OAuth2PasswordBearerWithCookie(OAuth2): # class OAuth2PasswordBearerWithCookie(OAuth2):
def __init__( # def __init__(
self, # self,
tokenUrl: str, # tokenUrl: str,
scheme_name: Optional[str] = None, # scheme_name: Optional[str] = None,
scopes: Optional[Dict[str, str]] = None, # scopes: Optional[Dict[str, str]] = None,
auto_error: bool = True, # auto_error: bool = True,
): # ):
if not scopes: # if not scopes:
scopes = {} # scopes = {}
flows = OAuthFlowsModel(password={"tokenUrl": tokenUrl, "scopes": scopes}) # type: ignore # flows = OAuthFlowsModel(password={"tokenUrl": tokenUrl, "scopes": scopes}) # type: ignore
super().__init__(flows=flows, scheme_name=scheme_name, auto_error=auto_error) # super().__init__(flows=flows, scheme_name=scheme_name, auto_error=auto_error)
async def __call__(self, request: Request) -> Optional[str]: # async def __call__(self, request: Request) -> Optional[str]:
authorization: str = request.cookies.get("access_token") # changed to accept access token from httpOnly Cookie # authorization: str = request.cookies.get("access_token") # changed to accept access token from httpOnly Cookie
scheme, param = get_authorization_scheme_param(authorization) # scheme, param = get_authorization_scheme_param(authorization)
if not authorization or scheme.lower() != "bearer": # if not authorization or scheme.lower() != "bearer":
if self.auto_error: # if self.auto_error:
raise HTTPException( # raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, # status_code=status.HTTP_401_UNAUTHORIZED,
detail="Not authenticated", # detail="Not authenticated",
headers={"WWW-Authenticate": "Bearer"}, # headers={"WWW-Authenticate": "Bearer"},
) # )
else: # else:
return None # return None
return param # return param
def authenticate_user_by_email(email: str, password: str) -> Optional[Profile]:
def authenticate_user(username: str, password: str) -> Optional[Profile]:
with SessionLocal() as db: with SessionLocal() as db:
user = get_profile(username=username, db=db) user = get_profile_by_email(email=email, db=db)
logger.debug(user) logger.debug(user)
if not user: if not user:
if is_database_empty(db): if is_database_empty(db):
@@ -65,7 +64,26 @@ def authenticate_user(username: str, password: str) -> Optional[Profile]:
return None return None
else: else:
if bcrypt.checkpw(password.encode(), user.password.encode()): if bcrypt.checkpw(password.encode(), user.password.encode()):
print("User successful authenticated") logger.info("User successful authenticated")
else:
logger.info("Authentication failed!")
return user
def authenticate_user_by_username(username: str, password: str) -> Optional[Profile]:
with SessionLocal() as db:
user = get_profile_by_username(username=username, db=db)
logger.debug(user)
if not user:
if is_database_empty(db):
logger.info("database is empty, use temporary access")
user = Profile()
user.email = "init_user@thpeetz.de"
return user
return None
else:
if bcrypt.checkpw(password.encode(), user.password.encode()):
logger.info("User successful authenticated")
else: else:
logger.info("Authentication failed!") logger.info("Authentication failed!")
return user return user
@@ -103,17 +121,19 @@ async def get_current_user(
token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM] token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM]
) )
username: str = payload.get("sub") # type: ignore username: str = payload.get("sub") # type: ignore
logger.info("username/email extracted is ", username) logger.info("username/email extracted is %s", username)
if username is None: if username is None:
raise credentials_exception raise credentials_exception
scope: str = payload.get("scope", "") scope: str = payload.get("scope", "")
token_scopes: List[str] = scope.split(" ") token_scopes: List[str] = scope.split(" ")
token_data = TokenData(scopes=token_scopes, username=username) token_data = TokenData(scopes=token_scopes, username=username)
except (JWTError, ValidationError): except (JWTError, ValidationError):
logger.info("Exception raised", exc_info=True)
raise credentials_exception raise credentials_exception
with SessionLocal() as db: with SessionLocal() as db:
user = get_profile(username=token_data.username, db=db) # type: ignore user = get_profile_by_username(username=token_data.username, db=db)
if user is None: if user is None:
logger.info("user not found")
raise credentials_exception raise credentials_exception
for scope in security_scopes.scopes: for scope in security_scopes.scopes:
if scope not in token_scopes: if scope not in token_scopes:
@@ -128,7 +148,7 @@ async def get_current_user(
async def get_current_active_user( async def get_current_active_user(
current_user: Annotated[Profile, Security(get_current_user, scopes=["me"])], current_user: Annotated[Profile, Security(get_current_user, scopes=["me"])],
) -> ProfileModel: ) -> ProfileModel:
if not current_user.enabled: # type: ignore if not current_user.enabled:
raise HTTPException(status_code=400, detail="Inactive user") raise HTTPException(status_code=400, detail="Inactive user")
user_model = ProfileModel( user_model = ProfileModel(
username=current_user.user_name, username=current_user.user_name,
@@ -150,13 +170,13 @@ def get_current_user_from_token(token: str = Depends(oauth2_scheme)):
token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM] token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM]
) )
username: str = payload.get("sub") # type: ignore username: str = payload.get("sub") # type: ignore
logger.info("username/email extracted is ", username) logger.info("username/email extracted is %s", username)
if username is None: if username is None:
raise credentials_exception raise credentials_exception
except JWTError: except JWTError:
raise credentials_exception raise credentials_exception
with SessionLocal() as db: with SessionLocal() as db:
user = get_profile(username=username, db=db) user = get_profile_by_email(email=username, db=db)
if user is None: if user is None:
raise credentials_exception raise credentials_exception
return user return user
+7 -3
View File
@@ -1,12 +1,16 @@
from typing import AnyStr, Optional from typing import Optional
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from src.db.models.admin import Profile from src.db.models.admin import Profile
def get_profile(username: AnyStr, db: Session) -> Optional[Profile]: def get_profile_by_username(username: str, db: Session) -> Optional[Profile]:
profile = db.query(Profile).filter(Profile.email == username).first() profile = db.query(Profile).filter(Profile.user_name == username).first()
return profile
def get_profile_by_email(email: str, db: Session) -> Optional[Profile]:
profile = db.query(Profile).filter(Profile.email == email).first()
return profile return profile
def is_database_empty(db: Session) -> bool: def is_database_empty(db: Session) -> bool:
+9 -1
View File
@@ -1,15 +1,23 @@
"""
Schema definitions for Comics.
"""
from typing import Optional from typing import Optional
from pydantic import BaseModel, AnyUrl from pydantic import BaseModel, AnyUrl
from src.core.log_conf import logger
class ComicResponse(BaseModel): class ComicResponse(BaseModel):
"""
Pydantic model for returning Comic objects.
"""
id: str id: str
title: str title: str
completed: bool completed: bool
class ComicSchema(BaseModel): class ComicSchema(BaseModel):
"""
Pydantic model for uploading Comic object.
"""
id: str id: str
title: str title: str
weblink: Optional[AnyUrl] weblink: Optional[AnyUrl]
@@ -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
+25
View File
@@ -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
+36
View File
@@ -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
+18 -2
View File
@@ -1,8 +1,24 @@
from typing import AnyStr from datetime import datetime
from pydantic import BaseModel from pydantic import BaseModel
from src.db.models.tysc import Sport
class SportResponse(BaseModel): class SportResponse(BaseModel):
id: AnyStr id: str
created_date: datetime
last_modified_date: datetime
version: int
name: str 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
+28
View File
@@ -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
+32
View File
@@ -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
+12
View File
@@ -0,0 +1,12 @@
#
# https://help.github.com/articles/dealing-with-line-endings/
#
# Linux start script should use lf
/gradlew text eol=lf
# These are Windows script files and should use crlf
*.bat text eol=crlf
# Binary files should be left untouched
*.jar binary
+8
View File
@@ -0,0 +1,8 @@
# Ignore Gradle project-specific cache directory
.gradle
# Ignore Gradle build output directory
build
# Ignore Kotlin plugin data
.kotlin
+21
View File
@@ -0,0 +1,21 @@
/*
* This file was generated by the Gradle 'init' task.
*
* This is a general purpose Gradle build.
* Learn more about Gradle by exploring our Samples at https://docs.gradle.org/9.4.1/samples
*/
plugins {
id("base")
id("de.infolektuell.typst") version "0.8.0"
}
typst.version = "v0.14.2"
typst.sourceSets {
// Sources in src/main
val main by registering {
// The files to compile (without .typ extension) in src/main/typst
documents = listOf("kontor") // src/main/typst/document.typst
}
}
+5
View File
@@ -0,0 +1,5 @@
# This file was generated by the Gradle 'init' task.
# https://docs.gradle.org/current/userguide/build_environment.html#sec:gradle_configuration_properties
org.gradle.configuration-cache=true
+2
View File
@@ -0,0 +1,2 @@
# This file was generated by the Gradle 'init' task.
# https://docs.gradle.org/current/userguide/version_catalogs.html#sec::toml-dependencies-format
Binary file not shown.
+7
View File
@@ -0,0 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
Vendored Executable
+248
View File
@@ -0,0 +1,248 @@
#!/bin/sh
#
# Copyright © 2015 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/2d6327017519d23b96af35865dc997fcb544fb40/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"
+93
View File
@@ -0,0 +1,93 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:execute
@rem Setup the command line
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
+8
View File
@@ -0,0 +1,8 @@
/*
* This file was generated by the Gradle 'init' task.
*
* The settings file is used to specify which projects to include in your build.
* For more detailed information on multi-project builds, please refer to https://docs.gradle.org/9.4.1/userguide/multi_project_builds.html in the Gradle documentation.
*/
rootProject.name = "kontor-doc"
+25
View File
@@ -0,0 +1,25 @@
= Anforderungen der Domäne
== Systemfunktionen
=== Anwendungsfälle
=== Akteure
=== Zielgruppen
== Anforderungen
=== Anforderungen an externe Schnittstellen
=== Funktionale Anforderungen
=== Qualitätsanforderungen
=== Randbedingungen
=== Weitere Anforderungen
=== Wartungs- und Supportinformationen
== Verifikation
+99
View File
@@ -0,0 +1,99 @@
//#set page("a4")
#import "@preview/basic-report:0.4.0": *
#import "@preview/in-dexter:0.7.2": *
#import "@preview/pintorita:0.1.4"
#set text(fill: white)
#show raw.where(lang: "pintora"): it => pintorita.render(it.text)
#show: it => basic-report(
doc-category: "Entwicklungs- und Projekthandbuch",
doc-title: "Projekt kontor",
author: "Thomas Peetz",
//affiliation: "MouseTec, Entenhausen",
//logo: image("assets/aerospace-engineering.png", width: 2cm),
// <a href="https://www.flaticon.com/free-icons/aerospace" title="aerospace icons">Aerospace icons created by gravisio - Flaticon</a>
language: "de",
compact-mode: false,
it
)
= Allgemeines
#figure(
table(
columns: 4,
[Version], [Datum], [Autor], [Änderungsgrund / Bemerkungen],
[1.0.0], [16.05.2022], [Thomas Peetz], [Ersterstellung],
),
caption: [Dokumenthistorie],
)
== Zweck des Dokumentes
Das Entwicklungshandbuch beschreibt die Werkzeuge und die Vorgehensweise bei der Entwicklung
im Projekt kontor-spring und der Erstellung der Dokumentation.
== Verwendete Tools
=== Gitea
Für die Verwaltung des Sourcecode kommt Gitea #index[Gitea]@gitea zum Einsatz.
Mit Gitea werden auch die Projektaufgaben verwaltet.
Das Projekt und das dazugehörige Git Repository sind unter der Adresse @gitea-kontor zu finden.
= Erstellung der Dokumentation
Die Dokumentation des Projektes wird mit Asciidoctor #index[Asciidoctor]@asciidoctor geschrieben.
Die Dokumente erhalten ihre Namen nach dem jeweiligen Hauptdokument.
== Quellcode Verwaltung
Die Asciidoctor-Dateien haben die Endung `.adoc`.
== Buildsystem
Zur Erstellung der PDF-Dateien aus den Asciidoctor-Dateien wird das Buildsystem Gradle #index[Gradle]@gradle verwendet.
Die Dateien für die Dokumente liegen im Verzeichnis `src/docs/asciidoc`.
Der Gradle Build wird über die Datei `build.gradle` definiert.
#pagebreak()
= Einführung
== Zweck
== Stakeholder des Systems
== Systemumfang
=== Zielsetzung des Systems
#pagebreak()
#include "system.typ"
#include "domain.typ"
#include "projekt.typ"
= Verschiedenes
== Erreichbarkeiten
= Referenzen
#bibliography("reference.yaml", title: none, full: true)
= Glossar
= Index
#make-index(title: none, outlined: true, use-page-counter: true)
= Verzeichnisse
== Abbildungsverzeichnis
#outline(title: none, target: figure.where(kind: image))
== Tabellenverzeichnis
#outline(title: none, target: figure.where(kind: table))
+46
View File
@@ -0,0 +1,46 @@
= Projektbeschreibung
== Ausgangslage
//==== Rechtliche Vorgaben und Rahmenbedingungen
//=== Rahmenbedingungen
//==== Vorhandene Regelungen
== Projektziele
== Projektabgrenzung
//=== Voraussichtliche Kosten
//=== Projektrisiken
//==== Produktivität
//==== Finanzielle Risiken
//==== Akzeptanz
= Projektorganisation
== Projekt-Aufbauorganisation
== Rollendefinition
//==== Projektauftraggeber
//==== Projektausschuss
//==== Beratung / Qualitätssicherung
=== Projekteiter
=== Projektteam
=== Liste der Stakeholder
== Projektablauforganisation
=== Projekt-Phasen
== Erstellung der Projektdokumentation
+15
View File
@@ -0,0 +1,15 @@
asciidoctor:
type: web
url: http://asciidoctor.org
gitea:
type: web
url: http://www.gitea.org
gradle:
type: web
url: http://www.gradle.org
jenkins:
type: web
url: http://jenkins-ci.org
gitea-kontor:
type: web
url: https://gitea.thpeetz.de/kontor/kontor-spring
+336
View File
@@ -0,0 +1,336 @@
= Systemübersicht
== Systemkontext
== Systemarchitektur
== Systemschnittstellen
=== Realisierte Schnittstellen
=== Verwendete Schnittstellen
== Logisches Datenmodell
#figure(
kind: image,
```pintora
erDiagram
USER {
string id PK
datetime created_date
datetime last_modified_date
int version
string email
boolean enabled
string firstName
string lastName
string password
string token
boolean tokenExpired
string userName UNIQUE
}
USER ||--o{ AUTHORIZATION_MATRIX : "matrix"
ROLE {
string id PK
datetime created_date
datetime last_modified_date
int version
string name
}
ROLE ||--o{ AUTHORIZATION_MATRIX : "matrix"
AUTHORIZATION_MATRIX {
string id PK
datetime created_date
datetime last_modified_date
int version
string user_id FK
string role_id FK
}
```,
caption: [Benutzer ER-Diagramm]
)
#figure(
kind: image,
```pintora
erDiagram
comic {
string id PK
datetime created_date
datetime last_modified_date
int version
boolean completed
boolean currentOrder
string title
string publisher_id FK
}
comic ||--o{ comic_work : "1"
comic ||--o{ issue : "1"
comic ||--o{ volume : "1"
comic ||--o{ story_arc : "1"
comic ||--o{ trade_paperback : "1"
volume {
string id PK
datetime created_date
datetime last_modified_date
int version
string name
string comic_id FK
}
volume ||--o{ issue : "1"
issue {
string id PK
datetime created_date
datetime last_modified_date
int version
boolean in_stock
boolean is_read
string issue_number
string comic_id FK
string volume_id FK
}
publisher {
string id PK
datetime created_date
datetime last_modified_date
int version
string name
}
publisher ||--o{ comic : "1"
artist {
string id PK
datetime created_date
datetime last_modified_date
int version
string name
}
artist ||--o{ comic_work : "1"
story_arc {
string id PK
datetime created_date
datetime last_modified_date
int version
string name
string comic_id FK
}
trade_paperback {
string id PK
datetime created_date
datetime last_modified_date
int version
int issueStart
int issueEnd
string name
string comic_id FK
}
worktype {
string id PK
datetime created_date
datetime last_modified_date
int version
string name
}
worktype ||--o{ comic-work : "1"
comic_work {
string id PK
datetime created_date
datetime last_modified_date
int version
string artist_id FK
string comic_id FK
string worktype_id FK
}
```,
caption: [Comics ER-Diagramm]
)
#figure(
kind: image,
```pintora
erDiagram
sport {
string id PK
datetime created_date
datetime last_modified_date
int version
string name
}
team {
string id PK
datetime created_date
datetime last_modified_date
int version
string name
string short_name
string sport_id FK
}
field_position {
string id PK
datetime created_date
datetime last_modified_date
int version
string name
string short_name
string sport_id FK
}
rooster {
string id PK
datetime created_date
datetime last_modified_date
int version
int year
string player_id FK
string position_id FK
string team_id FK
}
player {
string id PK
datetime created_date
datetime last_modified_date
int version
string first_name
string last_name
}
vendor {
string id PK
datetime created_date
datetime last_modified_date
int version
string name
}
card_set {
string id PK
datetime created_date
datetime last_modified_date
int version
boolean insert_set
string name
boolean parallel_set
string vendor_id FK
}
card {
string id PK
datetime created_date
datetime last_modified_date
int version
int cardNumber
int year
string card_set FK
string rooster_id FK
string vendor_id FK
}
sport ||--o{ team : "1"
sport ||--o{ field_position : "1"
field_position ||--o{ rooster : "1"
player ||--o{ rooster : "1"
team ||--o{ rooster : "1"
vendor ||--o{ card : "1"
card_set ||--o{ card : "1"
rooster ||--o{ card : "1"
```,
caption: [TYSC ER-Diagramm]
)
#figure(
kind: image,
```pintora
erDiagram
book {
string id PK
datetime created_date
datetime last_modified_date
int version
string isbn UNIQUE
string title
int year
string publisher_id FK
}
bookshelf_publisher {
string id PK
datetime created_date
datetime last_modified_date
int version
string name UNIQUE
}
author {
string id PK
datetime created_date
datetime last_modified_date
int version
string first_name
string last_name
}
article {
string id PK
datetime created_date
datetime last_modified_date
int version
string title
}
article_author {
string id PK
datetime created_date
datetime last_modified_date
int version
string article_id FK
string author_id FK
}
book_author {
string id PK
datetime created_date
datetime last_modified_date
int version
string book_id FK
string author_id FK
}
bookshelf_publisher ||--o{ book : "1"
article ||--o{ article_author : "1"
author ||--o{ article_author : "1"
book ||--o{ book_author : "1"
author ||--o{ book_author : "1"
```,
caption: [Bookshelf ER-Diagramm]
)
#figure(
kind: image,
```pintora
erDiagram
mail {
string id PK
datetime created_date
datetime last_modified_date
int version
string subject
string content
datetime received_date
datetime sent_date
}
mail_account {
string id PK
datetime created_date
datetime last_modified_date
int version
string host
string password
int port
string protocol
boolean start_tls
string user_name
}
mail_address {
string id PK
datetime created_date
datetime last_modified_date
int version
string internet_address UNIQUE
string personal
string user_id FK
}
user ||--o{ mail_address : "1"
```,
caption: [Mail ER-Diagramm]
)
=== Einschränkungen
-15
View File
@@ -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 .
-369
View File
@@ -1,369 +0,0 @@
"""
add actors
"""
import logging.config
import requests
import re
from argparse import ArgumentParser, ArgumentDefaultsHelpFormatter
from bs4 import BeautifulSoup
parser = ArgumentParser(formatter_class=ArgumentDefaultsHelpFormatter)
parser.add_argument('--verbose', '-v', action='count', default=0)
args = parser.parse_args()
def get_logger(level: int) -> logging.Logger:
logging.config.dictConfig({
'version': 1,
'disable_existing_loggers': False,
'formatters': {
'simple': {
'format': '[%(asctime)s] {%(filename)s:%(lineno)d} %(levelname)s - %(message)s',
'datefmt': '%Y-%m-%d %H:%M:%S',
},
},
'handlers': {
'console': {
'class': logging.StreamHandler,
'level': logging.DEBUG,
'formatter': 'simple',
'stream': 'ext://sys.stdout'
},
},
'loggers': {
'urllib3.connectionpool': {
'level': 'WARNING',
'propagate': False,
},
'root': {
'level': 'DEBUG',
'handlers': ['console'],
},
},
})
logger = logging.getLogger(__file__)
if level is not None:
match level:
case 0:
logger.setLevel(logging.WARNING)
case 1:
logger.setLevel(logging.INFO)
case 2:
logger.setLevel(logging.DEBUG)
case _:
logger.setLevel(logging.CRITICAL)
return logger
if __name__ == '__main__':
log = get_logger(args.verbose)
log.warning('kontor.add_actors started')
log.debug('get all actors')
response = requests.get("http://127.0.0.1:8800/api/media/actors")
data = response.json()
actors = {}
for item in data:
actor = {}
actor['id'] = item['id']
actor['name'] = item['name']
actor['url'] = item['url']
actors[item['url']] = actor
log.debug(f'all actors: {actors}')
new_actor_list = [
{ 'name': 'Herschel Savage', 'url': 'https://ge.xhamster.com/pornstars/herschel-savage'},
{ 'name': 'Janey Robbins', 'url': 'https://ge.xhamster.com/pornstars/janey-robbins'},
{ 'name': 'Kimberly Carson', 'url': 'https://ge.xhamster.com/pornstars/kimberly-carson'},
{ 'name': 'Paul Thomas', 'url': 'https://ge.xhamster.com/pornstars/paul-thomas'},
{ 'name': 'Shauna Grant', 'url': 'https://ge.xhamster.com/pornstars/shauna-grant'},
{ 'name': 'Tara Aire', 'url': 'https://ge.xhamster.com/pornstars/tara-aire'},
{ 'name': 'Kari Milla', 'url': 'https://ge.xhamster.com/pornstars/kari-milla'},
{ 'name': 'Drago Gvozdik', 'url': 'https://ge.xhamster.com/pornstars/drago-gvozdik'},
{ 'name': 'Cheyne Collins', 'url': 'https://ge.xhamster.com/pornstars/cheyne-collins'},
{ 'name': 'Christian XXX', 'url': 'https://ge.xhamster.com/pornstars/christian-xxx'},
{ 'name': 'Derrick Pierce', 'url': 'https://ge.xhamster.com/pornstars/derrick-pierce'},
{ 'name': 'Holly Halston', 'url': 'https://ge.xhamster.com/pornstars/holly-halston'},
{ 'name': 'Holly West', 'url': 'https://ge.xhamster.com/pornstars/holly-west'},
{ 'name': 'Jarod Diamond', 'url': 'https://ge.xhamster.com/pornstars/jarod-diamond'},
{ 'name': 'Kristina Cross', 'url': 'https://ge.xhamster.com/pornstars/kristina-cross'},
{ 'name': 'Lee Stone', 'url': 'https://ge.xhamster.com/pornstars/lee-stone'},
{ 'name': 'Monica Mayhem', 'url': 'https://ge.xhamster.com/pornstars/monica-mayhem'},
{ 'name': 'Sienna West', 'url': 'https://ge.xhamster.com/pornstars/sienna-west'},
{ 'name': 'Aria Carson', 'url': 'https://ge.xhamster.com/pornstars/aria-carson'},
{ 'name': 'Britney Amber', 'url': 'https://ge.xhamster.com/pornstars/britney-amber'},
{ 'name': 'Kit Mercer', 'url': 'https://ge.xhamster.com/pornstars/kit-mercer'},
{ 'name': 'Riley Reyes', 'url': 'https://ge.xhamster.com/pornstars/riley-reyes'},
{ 'name': 'Ryan Keely', 'url': 'https://ge.xhamster.com/pornstars/ryan-keely'},
{ 'name': 'Alexandra Diamond', 'url': 'https://ge.xhamster.com/pornstars/alexandra-diamond'},
{ 'name': 'Amanda Twice', 'url': 'https://ge.xhamster.com/pornstars/amanda-twice'},
{ 'name': 'Choky Ice', 'url': 'https://ge.xhamster.com/pornstars/choky-ice'},
{ 'name': 'Cindy Cox', 'url': 'https://ge.xhamster.com/pornstars/cindy-cox'},
{ 'name': 'Kelly White', 'url': 'https://ge.xhamster.com/pornstars/kelly-white'},
{ 'name': 'Mike Foster', 'url': 'https://ge.xhamster.com/pornstars/mike-foster'},
{ 'name': 'Susie Sorrento', 'url': 'https://ge.xhamster.com/pornstars/susie-sorrento'},
{ 'name': 'Lara Sanchez', 'url': 'https://ge.xhamster.com/pornstars/lara-sanchez'},
{ 'name': 'Alicia Williams', 'url': 'https://ge.xhamster.com/pornstars/alicia-williams'},
{ 'name': 'Dirty Tina', 'url': 'https://ge.xhamster.com/pornstars/dirty-tina'},
{ 'name': 'Andrew Andretti', 'url': 'https://ge.xhamster.com/pornstars/andrew-andretti'},
{ 'name': 'Dick Nasty', 'url': 'https://ge.xhamster.com/pornstars/dick-nasty'},
{ 'name': 'Vicky Vette', 'url': 'https://ge.xhamster.com/pornstars/vicky-vette'},
{ 'name': 'Alex Gonz', 'url': 'https://ge.xhamster.com/pornstars/alex-gonz'},
{ 'name': 'Audrey Hollander', 'url': 'https://ge.xhamster.com/pornstars/audrey-hollander'},
{ 'name': 'Desiree Diamond', 'url': 'https://ge.xhamster.com/pornstars/desiree-diamond'},
{ 'name': 'Lexi Bardot', 'url': 'https://ge.xhamster.com/pornstars/lexi-bardot'},
{ 'name': 'Adam Ocelot', 'url': 'https://ge.xhamster.com/pornstars/adam-ocelot'},
{ 'name': 'Scarlett Hampton', 'url': 'https://ge.xhamster.com/pornstars/scarlett-hampton'},
{ 'name': 'Laetitia', 'url': 'https://ge.xhamster.com/pornstars/laetitia'},
{ 'name': 'Raffaela Anderson', 'url': 'https://ge.xhamster.com/pornstars/raffaela-anderson'},
{ 'name': 'Michael Swayze', 'url': 'https://ge.xhamster.com/pornstars/michael-swayze'},
{ 'name': 'Winter Jade', 'url': 'https://ge.xhamster.com/pornstars/winter-jade'},
{ 'name': 'Celine Noiret', 'url': 'https://ge.xhamster.com/pornstars/celine-noiret'},
{ 'name': 'Aurora Snow', 'url': 'https://ge.xhamster.com/pornstars/aurora-snow'},
{ 'name': 'Bree Brooks', 'url': 'https://ge.xhamster.com/pornstars/bree-brooks'},
{ 'name': 'Flick Shagwell', 'url': 'https://ge.xhamster.com/pornstars/flick-shagwell'},
{ 'name': 'Noname Jane', 'url': 'https://ge.xhamster.com/pornstars/noname-jane'},
{ 'name': 'Alex Magni', 'url': 'https://ge.xhamster.com/pornstars/alex-magni'},
{ 'name': 'Maeva Dream', 'url': 'https://ge.xhamster.com/pornstars/maeva-dream'},
{ 'name': 'Squirty Alice', 'url': 'https://ge.xhamster.com/pornstars/squirty-alice'},
{ 'name': 'Mandy Rhea', 'url': 'https://ge.xhamster.com/pornstars/mandy-rhea'},
{ 'name': 'Alessia Donati', 'url': 'https://ge.xhamster.com/pornstars/alessia-donati'},
{ 'name': 'Pierre DJ', 'url': 'https://ge.xhamster.com/pornstars/pierre-dj'},
{ 'name': 'Veronica Belli', 'url': 'https://ge.xhamster.com/pornstars/veronica-belli'},
{ 'name': 'Christiana Cinn', 'url': 'https://ge.xhamster.com/pornstars/christiana-cinn'},
{ 'name': 'Jasmine Jae', 'url': 'https://ge.xhamster.com/pornstars/jasmine-jae'},
{ 'name': 'Jay Smooth', 'url': 'https://ge.xhamster.com/pornstars/jay-smooth'},
{ 'name': 'Natalie Knight', 'url': 'https://ge.xhamster.com/pornstars/natalie-knight'},
{ 'name': 'Joshua Lewis', 'url': 'https://ge.xhamster.com/pornstars/joshua-lewis'},
{ 'name': 'Lauren Phillips', 'url': 'https://ge.xhamster.com/pornstars/lauren-phillips'},
{ 'name': 'Matt Cash', 'url': 'https://ge.xhamster.com/pornstars/matt-cash'},
{ 'name': 'Nickey Huntsman', 'url': 'https://ge.xhamster.com/pornstars/nickey-huntsman'},
{ 'name': 'Jade Sin', 'url': 'https://ge.xhamster.com/pornstars/jade-sin'},
{ 'name': 'Wein Lewis', 'url': 'https://ge.xhamster.com/pornstars/wein-lewis'},
{ 'name': 'Megan Murkovski', 'url': 'https://ge.xhamster.com/pornstars/megan-murkovski'},
{ 'name': 'Tara Wild', 'url': 'https://ge.xhamster.com/pornstars/tara-wild'},
{ 'name': 'Lana Roy', 'url': 'https://ge.xhamster.com/pornstars/lana-roy'},
{ 'name': 'Nick Moreno', 'url': 'https://ge.xhamster.com/pornstars/nick-moreno'},
{ 'name': 'Brad Armstrong', 'url': 'https://ge.xhamster.com/pornstars/brad-armstrong'},
{ 'name': 'Kaylani Lei', 'url': 'https://ge.xhamster.com/pornstars/kaylani-lei'},
{ 'name': 'Deborah Wells', 'url': 'https://ge.xhamster.com/pornstars/deborah-wells'},
{ 'name': 'Judith Ramirez', 'url': 'https://ge.xhamster.com/pornstars/judith-ramirez'},
{ 'name': 'Richard Langin', 'url': 'https://ge.xhamster.com/pornstars/richard-langin'},
{ 'name': 'Simona Valli', 'url': 'https://ge.xhamster.com/pornstars/simona-valli'},
{ 'name': 'Mandy Dee', 'url': 'https://ge.xhamster.com/pornstars/mandy-dee'},
{ 'name': 'Allison Wyte', 'url': 'https://ge.xhamster.com/pornstars/allison-wyte'},
{ 'name': 'Ben English', 'url': 'https://ge.xhamster.com/pornstars/ben-english'},
{ 'name': 'Chuck Martino', 'url': 'https://ge.xhamster.com/pornstars/chuck-martino'},
{ 'name': 'Erika Kole', 'url': 'https://ge.xhamster.com/pornstars/erika-kole'},
{ 'name': 'Katie Morgan', 'url': 'https://ge.xhamster.com/pornstars/katie-morgan'},
{ 'name': 'Misty Parks', 'url': 'https://ge.xhamster.com/pornstars/misty-parks'},
{ 'name': 'Mr. Pete', 'url': 'https://ge.xhamster.com/pornstars/mr-pete'},
{ 'name': 'Candie Luciani', 'url': 'https://ge.xhamster.com/pornstars/candie-luciani'},
{ 'name': 'Clara Mia', 'url': 'https://ge.xhamster.com/pornstars/clara-mia'},
{ 'name': 'Tommy Cabrio', 'url': 'https://ge.xhamster.com/pornstars/tommy-cabrio'},
{ 'name': 'Nathan Bronson', 'url': 'https://ge.xhamster.com/pornstars/nathan-bronson'},
{ 'name': 'Octavia Red', 'url': 'https://ge.xhamster.com/pornstars/octavia-red'},
{ 'name': 'Big George', 'url': 'https://ge.xhamster.com/pornstars/big-george'},
{ 'name': 'Cassy Young', 'url': 'https://ge.xhamster.com/pornstars/cassy-young'},
{ 'name': 'Egon Kowalski', 'url': 'https://ge.xhamster.com/pornstars/egon-kowalski'},
{ 'name': 'Mark Aurel', 'url': 'https://ge.xhamster.com/pornstars/mark-aurel'},
{ 'name': 'Samy Fox', 'url': 'https://ge.xhamster.com/pornstars/samy-fox'},
{ 'name': 'Valeria Jones', 'url': 'https://ge.xhamster.com/pornstars/valeria-jones'},
{ 'name': 'Dieter Von Stein', 'url': 'https://ge.xhamster.com/pornstars/dieter-von-stein'},
{ 'name': 'Donna Bell', 'url': 'https://ge.xhamster.com/pornstars/donna-bell'},
{ 'name': 'Molly Jane', 'url': 'https://ge.xhamster.com/pornstars/molly-jane'},
{ 'name': 'Mark Zane', 'url': 'https://ge.xhamster.com/pornstars/mark-zane'},
{ 'name': 'Titus Steel', 'url': 'https://ge.xhamster.com/pornstars/titus-steel'},
{ 'name': 'Mackenzie Mace', 'url': 'https://ge.xhamster.com/pornstars/mackenzie-mace'},
{ 'name': 'Christina Lang', 'url': 'https://ge.xhamster.com/pornstars/christina-lang'},
{ 'name': 'Jens Modena', 'url': 'https://ge.xhamster.com/pornstars/jens-modena'},
{ 'name': 'Karma Rosenberg', 'url': 'https://ge.xhamster.com/pornstars/karma-rosenberg'},
{ 'name': 'Dani Jensen', 'url': 'https://ge.xhamster.com/pornstars/dani-jensen'},
{ 'name': 'Crystal Ray', 'url': 'https://ge.xhamster.com/pornstars/crystal-ray'},
{ 'name': 'Andrea Szabo', 'url': 'https://ge.xhamster.com/pornstars/andrea-szabo'},
{ 'name': 'Desiree Barclay', 'url': 'https://ge.xhamster.com/pornstars/desiree-barclay'},
{ 'name': 'Jada Stevens', 'url': 'https://ge.xhamster.com/pornstars/jada-stevens'},
{ 'name': 'Syren Demer', 'url': 'https://ge.xhamster.com/pornstars/syren-demer'},
{ 'name': 'Ara Mix', 'url': 'https://ge.xhamster.com/pornstars/ara-mix'},
{ 'name': 'Redneck John', 'url': 'https://ge.xhamster.com/pornstars/redneck-john'},
{ 'name': 'Allysin Chaynes', 'url': 'https://ge.xhamster.com/pornstars/allysin-chaynes'},
{ 'name': 'Aspen Brock', 'url': 'https://ge.xhamster.com/pornstars/aspen-brock'},
{ 'name': 'Kaylynn', 'url': 'https://ge.xhamster.com/pornstars/kaylynn'},
{ 'name': 'Monica Cameron', 'url': 'https://ge.xhamster.com/pornstars/monica-cameron'},
{ 'name': 'Sheila Rossi', 'url': 'https://ge.xhamster.com/pornstars/sheila-rossi'},
{ 'name': 'Alex Ginger', 'url': 'https://ge.xhamster.com/pornstars/alex-ginger'},
{ 'name': 'Ally Style', 'url': 'https://ge.xhamster.com/pornstars/ally-style'},
{ 'name': 'Paola Mike', 'url': 'https://ge.xhamster.com/pornstars/paola-mike'},
{ 'name': 'Jeanette Littledove', 'url': 'https://ge.xhamster.com/pornstars/jeanette-littledove'},
{ 'name': 'Melissa Melendez', 'url': 'https://ge.xhamster.com/pornstars/melissa-melendez'},
{ 'name': 'Peter North', 'url': 'https://ge.xhamster.com/pornstars/peter-north'},
{ 'name': 'Siobhan Hunter', 'url': 'https://ge.xhamster.com/pornstars/siobhan-hunter'},
{ 'name': 'Tami White', 'url': 'https://ge.xhamster.com/pornstars/tami-white'},
{ 'name': 'Tracey Adams', 'url': 'https://ge.xhamster.com/pornstars/tracey-adams'},
{ 'name': 'Ashley Haze', 'url': 'https://ge.xhamster.com/pornstars/ashley-haze'},
{ 'name': 'Cailey Taylor', 'url': 'https://ge.xhamster.com/pornstars/cailey-taylor'},
{ 'name': 'Eve Laurence', 'url': 'https://ge.xhamster.com/pornstars/eve-laurence'},
{ 'name': 'Naudia Nyce', 'url': 'https://ge.xhamster.com/pornstars/naudia-nyce'},
{ 'name': 'Candy Apples', 'url': 'https://ge.xhamster.com/pornstars/candy-apples'},
{ 'name': 'Kate Rich', 'url': 'https://ge.xhamster.com/pornstars/kate-rich'},
{ 'name': 'Aniko Kaposi', 'url': 'https://ge.xhamster.com/pornstars/aniko-kaposi'},
{ 'name': 'Beatrice Poggi', 'url': 'https://ge.xhamster.com/pornstars/beatrice-poggi'},
{ 'name': 'Erika Bella', 'url': 'https://ge.xhamster.com/pornstars/erika-bella'},
{ 'name': 'Nikita Gross', 'url': 'https://ge.xhamster.com/pornstars/nikita-gross'},
{ 'name': 'Ursula Moore', 'url': 'https://ge.xhamster.com/pornstars/ursula-moore'},
{ 'name': 'Caty Kiss', 'url': 'https://ge.xhamster.com/pornstars/caty-kiss'},
{ 'name': 'Light Fairy', 'url': 'https://ge.xhamster.com/pornstars/light-fairy'},
{ 'name': 'Flame', 'url': 'https://ge.xhamster.com/pornstars/flame'},
{ 'name': 'Tiffany Tatum', 'url': 'https://ge.xhamster.com/pornstars/tiffany-tatum'},
{ 'name': 'Alyson Sykes', 'url': 'https://ge.xhamster.com/pornstars/alyson-sykes'},
{ 'name': 'Jenifer Stone', 'url': 'https://ge.xhamster.com/pornstars/jenifer-stone'},
{ 'name': 'Lucy Love', 'url': 'https://ge.xhamster.com/pornstars/lucy-love'},
{ 'name': 'Thomas Stone', 'url': 'https://ge.xhamster.com/pornstars/thomas-stone'},
{ 'name': 'Nikki Babe', 'url': 'https://ge.xhamster.com/pornstars/nikki-babe'},
{ 'name': 'Tyra Misoux', 'url': 'https://ge.xhamster.com/pornstars/tyra-misoux'},
{ 'name': 'Kenzie Reeves', 'url': 'https://ge.xhamster.com/pornstars/kenzie-reeves'},
{ 'name': 'Anissa Kate', 'url': 'https://ge.xhamster.com/pornstars/anissa-kate'},
{ 'name': 'Anna Polina', 'url': 'https://ge.xhamster.com/pornstars/anna-polina'},
{ 'name': 'Kimber Delice', 'url': 'https://ge.xhamster.com/pornstars/kimber-delice'},
{ 'name': 'Lucy Heart', 'url': 'https://ge.xhamster.com/pornstars/lucy-heart'},
{ 'name': 'John Strong', 'url': 'https://ge.xhamster.com/pornstars/john-strong'},
{ 'name': 'Markus Dupree', 'url': 'https://ge.xhamster.com/pornstars/markus-dupree'},
{ 'name': 'Mick Blue', 'url': 'https://ge.xhamster.com/pornstars/mick-blue'},
{ 'name': 'Natasha Nice', 'url': 'https://ge.xhamster.com/pornstars/natasha-nice'},
{ 'name': 'Lena Reif', 'url': 'https://ge.xhamster.com/pornstars/lena-reif'},
{ 'name': 'Sonia Paganini', 'url': 'https://ge.xhamster.com/pornstars/sonia-paganini'},
{ 'name': 'Demi Hawks', 'url': 'https://ge.xhamster.com/pornstars/demi-hawks'},
{ 'name': 'Juan El Caballo Loco', 'url': 'https://ge.xhamster.com/pornstars/juan-el-caballo-loco'},
{ 'name': 'Mike Mancini', 'url': 'https://ge.xhamster.com/pornstars/mike-mancini'},
{ 'name': 'Millie Morgan', 'url': 'https://ge.xhamster.com/pornstars/millie-morgan'},
{ 'name': 'Richard Allan', 'url': 'https://ge.xhamster.com/pornstars/richard-allan'},
{ 'name': 'Damon Dice', 'url': 'https://ge.xhamster.com/pornstars/damon-dice'},
{ 'name': 'Sera Ryder', 'url': 'https://ge.xhamster.com/pornstars/sera-ryder'},
{ 'name': 'Zoltan Kaabai', 'url': 'https://ge.xhamster.com/pornstars/zoltan-kabai'},
{ 'name': 'Cathy Heaven', 'url': 'https://ge.xhamster.com/pornstars/cathy-heaven'},
{ 'name': 'Coco Lovelock', 'url': 'https://ge.xhamster.com/pornstars/coco-lovelock'},
{ 'name': 'Percy Sires', 'url': 'https://ge.xhamster.com/pornstars/percy-sires'},
{ 'name': 'Meridian', 'url': 'https://ge.xhamster.com/pornstars/meridian'},
{ 'name': 'Pascal St. James', 'url': 'https://ge.xhamster.com/pornstars/pascal-st-james'},
{ 'name': 'Red Fox', 'url': 'https://ge.xhamster.com/pornstars/red-fox'},
{ 'name': 'Tony Art', 'url': 'https://ge.xhamster.com/pornstars/tony-art'},
{ 'name': 'Addison Lee', 'url': 'https://ge.xhamster.com/pornstars/addison-lee'},
{ 'name': 'Daria Glover', 'url': 'https://ge.xhamster.com/pornstars/daria-glover'},
{ 'name': 'Mandy Bright', 'url': 'https://ge.xhamster.com/pornstars/mandy-bright'},
{ 'name': 'Antonia Sainz', 'url': 'https://ge.xhamster.com/pornstars/antonia-sainz'},
{ 'name': 'Nicole Love', 'url': 'https://ge.xhamster.com/pornstars/nicole-love'},
{ 'name': 'Sarah Kay', 'url': 'https://ge.xhamster.com/pornstars/sarah-kay'},
{ 'name': 'Judith Kostner', 'url': 'https://ge.xhamster.com/pornstars/judith-kostner'},
{ 'name': 'Maria Bellucci', 'url': 'https://ge.xhamster.com/pornstars/maria-bellucci'},
{ 'name': 'Melissa Monet', 'url': 'https://ge.xhamster.com/pornstars/melissa-monet'},
{ 'name': 'Stephanie Cane', 'url': 'https://ge.xhamster.com/pornstars/stephanie-cane'},
{ 'name': 'Will Steiger', 'url': 'https://ge.xhamster.com/pornstars/will-steiger'},
{ 'name': 'Katty West', 'url': 'https://ge.xhamster.com/pornstars/katty-west'},
{ 'name': 'Jean Pallett', 'url': 'https://ge.xhamster.com/pornstars/jean-pallett'},
{ 'name': 'Conny Dachs', 'url': 'https://ge.xhamster.com/pornstars/conny-dachs'},
{ 'name': 'Juicy Leyla', 'url': 'https://ge.xhamster.com/pornstars/juicy-leyla'},
{ 'name': 'Mandy Mystery', 'url': 'https://ge.xhamster.com/pornstars/mandy-mystery'},
{ 'name': 'Jack Vegas', 'url': 'https://ge.xhamster.com/pornstars/jack-vegas'},
{ 'name': 'Jessica Ryan', 'url': 'https://ge.xhamster.com/pornstars/jessica-ryan'},
{ 'name': 'Nathan Bronson', 'url': 'https://ge.xhamster.com/pornstars/nathan-bronson'},
{ 'name': 'Lexi Lore', 'url': 'https://ge.xhamster.com/pornstars/lexi-lore'},
{ 'name': 'Molly Little', 'url': 'https://ge.xhamster.com/pornstars/molly-little'},
{ 'name': 'Dolly Leigh', 'url': 'https://ge.xhamster.com/pornstars/dolly-leigh'},
{ 'name': 'Marcus London', 'url': 'https://ge.xhamster.com/pornstars/marcus-london'},
{ 'name': 'Kendra Sunderland', 'url': 'https://ge.xhamster.com/pornstars/kendra-sunderland'},
{ 'name': 'Manuel Ferrara', 'url': 'https://ge.xhamster.com/pornstars/manuel-ferrara'},
{ 'name': 'Kelly Trump', 'url': 'https://ge.xhamster.com/pornstars/kelly-trump'},
{ 'name': 'Angelica Heaven', 'url': 'https://ge.xhamster.com/pornstars/angelica-heaven'},
{ 'name': 'Luna Lynx', 'url': 'https://ge.xhamster.com/pornstars/luna-lynx'},
{ 'name': 'Princess Lili', 'url': 'https://ge.xhamster.com/pornstars/princess-lili'},
{ 'name': 'Alexa Wild', 'url': 'https://ge.xhamster.com/pornstars/alexa-wild'},
{ 'name': 'Jessyka Swan', 'url': 'https://ge.xhamster.com/pornstars/jessyka-swan'},
{ 'name': 'Nikky Thorne', 'url': 'https://ge.xhamster.com/pornstars/nikky-thorne'},
{ 'name': 'Tristan Summers', 'url': 'https://ge.xhamster.com/pornstars/tristan-summers'},
{ 'name': 'Rhaya Shyne', 'url': 'https://ge.xhamster.com/pornstars/rhaya-shyne'},
{ 'name': 'Desiree West', 'url': 'https://ge.xhamster.com/pornstars/desiree-west'},
{ 'name': 'Joan Devlon', 'url': 'https://ge.xhamster.com/pornstars/joan-devlon'},
{ 'name': 'Jodi Thorpe', 'url': 'https://ge.xhamster.com/pornstars/jodi-thorpe'},
{ 'name': 'Laurien Dominique', 'url': 'https://ge.xhamster.com/pornstars/laurien-dominique'},
{ 'name': 'Paul Scharf', 'url': 'https://ge.xhamster.com/pornstars/paul-scharf'},
{ 'name': 'Ray Wells', 'url': 'https://ge.xhamster.com/pornstars/ray-wells'},
{ 'name': 'Sandy Carey', 'url': 'https://ge.xhamster.com/pornstars/sandy-carey'},
{ 'name': 'Spender Travis', 'url': 'https://ge.xhamster.com/pornstars/spender-travis'},
{ 'name': 'Starlyn Simone', 'url': 'https://ge.xhamster.com/pornstars/starlyn-simone'},
{ 'name': 'Uschi Digard', 'url': 'https://ge.xhamster.com/pornstars/uschi-digard'},
{ 'name': 'Katarina Muti', 'url': 'https://ge.xhamster.com/pornstars/katarina-muti'},
{ 'name': 'Julia Power', 'url': 'https://ge.xhamster.com/pornstars/julia-power'},
{ 'name': 'Salma De Nora', 'url': 'https://ge.xhamster.com/pornstars/salma-de-nora'},
{ 'name': 'Valeria Jones', 'url': 'https://ge.xhamster.com/pornstars/valeria-jones'},
{ 'name': 'Brandy Canyon', 'url': 'https://ge.xhamster.com/pornstars/brandy-canyon'},
{ 'name': 'Marie Berger', 'url': 'https://ge.xhamster.com/pornstars/marie-berger'},
{ 'name': 'Luna Rishi', 'url': 'https://ge.xhamster.com/pornstars/luna-rishi'},
{ 'name': 'Colleen Brennan', 'url': 'https://ge.xhamster.com/pornstars/colleen-brennan'},
{ 'name': 'Roxanne Brewer', 'url': 'https://ge.xhamster.com/pornstars/roxanne-brewer'},
{ 'name': 'Elle Denay', 'url': 'https://ge.xhamster.com/pornstars/elle-denay'},
{ 'name': 'Juan Largo', 'url': 'https://ge.xhamster.com/pornstars/juan-largo'},
{ 'name': 'Codey Steele', 'url': 'https://ge.xhamster.com/pornstars/codey-steele'},
{ 'name': 'Laney Grey', 'url': 'https://ge.xhamster.com/pornstars/laney-grey'},
{ 'name': 'Stirling Cooper', 'url': 'https://ge.xhamster.com/pornstars/stirling-cooper'},
{ 'name': 'Lana Smalls', 'url': 'https://ge.xhamster.com/pornstars/lana-smalls'},
{ 'name': 'Alex Sanders', 'url':'https://ge.xhamster.com/pornstars/alex-sanders'},
{ 'name': 'Felecia Danay', 'url':'https://ge.xhamster.com/pornstars/felecia-danay'},
{ 'name': 'Lita Chase', 'url':'https://ge.xhamster.com/pornstars/lita-chase'},
{ 'name': 'Mark Ashley', 'url':'https://ge.xhamster.com/pornstars/mark-ashley'},
{ 'name': 'Phyllisha Anne', 'url':'https://ge.xhamster.com/pornstars/phyllisha-anne'},
{ 'name': 'Ryan Conner', 'url':'https://ge.xhamster.com/pornstars/ryan-conner'},
{ 'name': 'Tanya Danielle', 'url':'https://ge.xhamster.com/pornstars/tanya-danielle'},
{ 'name': 'Jessica Moore', 'url':'https://ge.xhamster.com/pornstars/jessica-moore'},
{ 'name': 'Mike Angelo', 'url':'https://ge.xhamster.com/pornstars/mike-angelo'},
{ 'name': 'Morgan Moon', 'url':'https://ge.xhamster.com/pornstars/morgan-moon'},
{ 'name': 'Tyler Steel', 'url':'https://ge.xhamster.com/pornstars/tyler-steel'},
{ 'name': 'Abella Danger', 'url':'https://ge.xhamster.com/pornstars/abella-danger'},
{ 'name': 'Alex Jett', 'url':'https://ge.xhamster.com/pornstars/alex-jett'},
{ 'name': 'Alyson Queen', 'url':'https://ge.xhamster.com/pornstars/alyson-queen'},
{ 'name': 'Antynia Rouge', 'url':'https://ge.xhamster.com/pornstars/antynia-rouge'},
{ 'name': 'Bea Dumas', 'url':'https://ge.xhamster.com/pornstars/bea-dumas'},
{ 'name': 'Callie Black', 'url':'https://ge.xhamster.com/pornstars/callie-black'},
{ 'name': 'Caroline Cage', 'url':'https://ge.xhamster.com/pornstars/caroline-cage'},
{ 'name': 'Cindy Dollar', 'url':'https://ge.xhamster.com/pornstars/cindy-dollar'},
{ 'name': 'Crystal Frost', 'url':'https://ge.xhamster.com/pornstars/crystal-frost'},
{ 'name': 'Fanny Steel', 'url':'https://ge.xhamster.com/pornstars/fanny-steel'},
{ 'name': 'Gia Derza', 'url':'https://ge.xhamster.com/pornstars/gia-derza'},
{ 'name': 'Horst Baron', 'url':'https://ge.xhamster.com/pornstars/horst-baron'},
{ 'name': 'Jasmine Rouge', 'url':'https://ge.xhamster.com/pornstars/jasmine-rouge'},
{ 'name': 'Jean-Pierre Armand', 'url':'https://ge.xhamster.com/pornstars/jean-pierre-armand'},
{ 'name': 'Jessa Rhodes', 'url':'https://ge.xhamster.com/pornstars/jessa-rhodes'},
{ 'name': 'Leonie Saint', 'url':'https://ge.xhamster.com/pornstars/leonie-saint'},
{ 'name': 'Linda Ray', 'url':'https://ge.xhamster.com/pornstars/linda-ray'},
{ 'name': 'Luca Ferrero', 'url':'https://ge.xhamster.com/pornstars/luca-ferrero'},
{ 'name': 'Paris Pink', 'url':'https://ge.xhamster.com/pornstars/paris-pink'},
{ 'name': 'Pavlina Stejskalova', 'url':'https://ge.xhamster.com/pornstars/pavlina-stejskalova'},
{ 'name': 'Phoenix Marie', 'url':'https://ge.xhamster.com/pornstars/phoenix-marie'},
{ 'name': 'Ricky Spanish', 'url':'https://ge.xhamster.com/pornstars/ricky-spanish'},
{ 'name': 'Rumika Powers', 'url':'https://ge.xhamster.com/pornstars/rumika-powers'},
{ 'name': 'Sara Blonde', 'url':'https://ge.xhamster.com/pornstars/sara-blonde'},
{ 'name': 'Sean Lawless', 'url':'https://ge.xhamster.com/pornstars/sean-lawless'},
{ 'name': 'Seth Gamble', 'url':'https://ge.xhamster.com/pornstars/seth-gamble'},
{ 'name': 'Siri Dahl', 'url':'https://ge.xhamster.com/pornstars/siri-dahl'},
{ 'name': 'Stephie Staar', 'url':'https://ge.xhamster.com/pornstars/stephie-staar'},
{ 'name': 'Steve Holmes', 'url':'https://ge.xhamster.com/pornstars/steve-holmes'},
{ 'name': 'Suzette Dale', 'url':'https://ge.xhamster.com/pornstars/suzette-dale'},
{ 'name': 'Uncle George', 'url':'https://ge.xhamster.com/pornstars/uncle-george'},
{ 'name': 'Winnie', 'url':'https://ge.xhamster.com/pornstars/winnie'},
{ 'name': 'Zenza Raggi', 'url':'https://ge.xhamster.com/pornstars/zenza-raggi'},
{ 'name': 'Zorah White', 'url':'https://ge.xhamster.com/pornstars/zorah-white'},
{ 'name': 'Marilyn Jess', 'url':'https://ge.xhamster.com/pornstars/marilyn-jess'},
{ 'name': 'Alexis Capri', 'url':'https://ge.xhamster.com/pornstars/alexis-capri'},
]
for new_actor in new_actor_list:
if new_actor['url'] in actors:
log.warning(f"Actor {new_actor['url']} already persisted")
continue
actor_response = requests.post(f"http://127.0.0.1:8800/api/media/actors", json=new_actor)
log.warning(f"add status: {actor_response.status_code}")
if actor_response.status_code == 201:
log.warning(f"add Actor {new_actor['url']} to existing actor list")
actors[new_actor['url']] = new_actor
actor_data = actor_response.json()
log.warning(f"Actor {actor_data} persisted")
log.warning('kontor.add_actors finished')
+166
View File
@@ -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
-52
View File
@@ -1,52 +0,0 @@
"""
copy data from SQLite to MariaDB
"""
import sqlite3
from argparse import ArgumentParser, ArgumentDefaultsHelpFormatter
from config import get_logger, get_database_cursors, get_meta_data, get_scripts
parser = ArgumentParser(formatter_class=ArgumentDefaultsHelpFormatter)
parser.add_argument('--recreate-db', action='store_true')
parser.add_argument('--verbose', '-v', action='count', default=0)
args = parser.parse_args()
def copy_data(mariadb_conn, sqlite_conn, table_scripts):
mariadb_cursor = mariadb_conn.cursor()
sqlite_cursor = sqlite_conn.cursor()
# logger.info(table_scripts)
for table_id in scripts:
select_statement = scripts[table_id]['select_sqlite']
# logger.info(select_statement)
insert_statement = scripts[table_id]['insert_mariadb']
mariadb_cursor.execute("SET FOREIGN_KEY_CHECKS = 0")
mariadb_cursor.execute(scripts[table_id]['truncate_mariadb'])
try:
sqlite_cursor.execute(select_statement)
rows = sqlite_cursor.fetchall()
for row in rows:
try:
mariadb_cursor.execute(insert_statement, row)
except sqlite3.Error as error:
logger.info('insert failed with %s\n%s\n%s', error, insert_statement, row)
mariadb_conn.commit()
mariadb_cursor.execute(scripts[table_id]['count'])
(number_of_rows,) = mariadb_cursor.fetchone()
row = sqlite_cursor.execute(scripts[table_id]['count']).fetchone()
logger.info('%s contains %d : %d entries', scripts[table_id]['name'], number_of_rows, row[0])
except sqlite3.Error as error:
logger.info('select failed with %s', error)
if __name__ == '__main__':
logger = get_logger(args.verbose)
logger.info('kontor.copy_to_sqlite started')
s_conn, m_conn = get_database_cursors(logger)
meta_data_tables = get_meta_data(m_conn)
# logger.info(meta_data_tables)
scripts = get_scripts(meta_data_tables, logger)
copy_data(m_conn, s_conn, scripts)
s_conn.close()
m_conn.close()
logger.info('kontor.copy_to_sqlite finished')
-51
View File
@@ -1,51 +0,0 @@
"""
copy data from MariaDB to SQLite
"""
import sqlite3
import mariadb
from argparse import ArgumentParser, ArgumentDefaultsHelpFormatter
from config import get_database_cursors, create_tables, get_logger, get_meta_data, get_scripts
parser = ArgumentParser(formatter_class=ArgumentDefaultsHelpFormatter)
parser.add_argument('--recreate-db', action='store_true')
parser.add_argument('--verbose', '-v', action='count', default=0)
args = parser.parse_args()
def copy_data(mariadb_conn, sqlite_conn, table_scripts):
mariadb_cursor = mariadb_conn.cursor()
sqlite_cursor = sqlite_conn.cursor()
# logger.info(table_scripts)
for table_id in table_scripts:
select_statement = scripts[table_id]['select_mariadb']
# logger.info(select_statement)
insert_statement = scripts[table_id]['insert_sqlite']
try:
mariadb_cursor.execute(select_statement)
rows = mariadb_cursor.fetchall()
for row in rows:
try:
sqlite_cursor.execute(insert_statement, row)
except sqlite3.Error as error:
logger.info('insert failed with %s\n%s\n%s', error, insert_statement, row)
sqlite_conn.commit()
mariadb_cursor.execute(scripts[table_id]['count'])
(number_of_rows,) = mariadb_cursor.fetchone()
row = sqlite_cursor.execute(scripts[table_id]['count']).fetchone()
logger.info('%s contains %d : %d entries', scripts[table_id]['name'], number_of_rows, row[0])
except mariadb.Error as error:
logger.info('select failed with %s', error)
if __name__ == '__main__':
logger = get_logger(args.verbose)
logger.info('kontor.copy_to_sqlite started')
s_conn, m_conn = get_database_cursors(logger)
meta_data_tables = get_meta_data(m_conn)
# logger.info(meta_data_tables)
scripts = get_scripts(meta_data_tables, logger)
create_tables(s_conn, logger, args.recreate_db, scripts)
copy_data(m_conn, s_conn, scripts)
s_conn.close()
m_conn.close()
logger.info('kontor.copy_to_sqlite finished')
-67
View File
@@ -1,67 +0,0 @@
"""
Prints the database kontor structure
"""
import mariadb
from argparse import ArgumentParser, ArgumentDefaultsHelpFormatter
from config import get_database_cursors, get_logger
parser = ArgumentParser(formatter_class=ArgumentDefaultsHelpFormatter)
parser.add_argument('--verbose', '-v', action='count', default=0)
args = parser.parse_args()
def show_tables(cur, log):
"""
Retrieves the list of tables from the database
:param cur:
:param log:
:return:
"""
log.info('get list of tables')
table_list = []
cur.execute("SHOW TABLES")
for (tablename,) in cur.fetchall():
table_list.append(tablename)
return table_list
def get_field_info(cur):
"""
Retrieves the field info associated with a cursor
:param cur:
:return:
"""
field_info = mariadb.fieldinfo()
field_info_text_list = []
for column in cur.description:
column_name = column[0]
column_type = field_info.type(column)
column_flags = field_info.flag(column)
field_info_text_list.append(f"{column_name}: {column_type} {column_flags}")
return field_info_text_list
def get_table_field_info(cur, tablename):
"""
Retrieves the field info associated with a table
:param cur:
:param tablename:
:return:
"""
cur.execute(f"SELECT * FROM {tablename} LIMIT 1")
field_info = get_field_info(cur)
return field_info
if __name__ == '__main__':
logger = get_logger(args.verbose)
logger.info("kontor.db_structure started")
_, mariadb_conn = get_database_cursors(logger)
tables = show_tables(mariadb_conn.cursor(), logger)
for table in tables:
field_info_text = get_table_field_info(mariadb_conn.cursor(), table)
print(f"Columns in table {table}:")
print("\n".join(field_info_text))
print("\n")
mariadb_conn.close()
logger.info("kontor.db_structure finished")
+17 -22
View File
@@ -2,7 +2,6 @@
download files with URLs from DB download files with URLs from DB
""" """
import os
import re import re
import subprocess import subprocess
import sys import sys
@@ -10,16 +9,17 @@ from argparse import ArgumentDefaultsHelpFormatter, ArgumentParser
from datetime import datetime from datetime import datetime
from enum import Enum, auto from enum import Enum, auto
from pathlib import Path from pathlib import Path
from typing import Any, Dict from typing import Dict
from logging import Logger
from uuid import UUID from uuid import UUID
import requests 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 = ArgumentParser(formatter_class=ArgumentDefaultsHelpFormatter)
parser.add_argument("--verbose", "-v", action="count", default=0) 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("--dir", "-d", default="/data/media")
parser.add_argument("--limit", "-l", type=int, help="maximum number of links to check") parser.add_argument("--limit", "-l", type=int, help="maximum number of links to check")
parser.add_argument("--tool", "-t", default="yt-dlp") parser.add_argument("--tool", "-t", default="yt-dlp")
@@ -104,12 +104,8 @@ def is_file_downloaded(media_file: dict, dir: Path) -> FileStatus:
return FileStatus.UNKNOWN return FileStatus.UNKNOWN
def update_status(item_id: UUID, file_info: dict, api_data: Dict[str, Any]): def update_status(item_id: UUID, file_info: dict, server: Server, log: Logger):
host = api_data["host"] update = server.update(log, "media_file", item_id, file_info)
token = api_data["token"]
url: str = f"http://{host}:{port}/api/media/files/{item_id}"
headers: Dict[str, str] = {"Authorization": f"Bearer {token}"}
update = requests.put(url, headers=headers, json=file_info)
log.info(f"update status: {update.status_code}") log.info(f"update status: {update.status_code}")
log.info(f"update result: {update.json()}") log.info(f"update result: {update.json()}")
@@ -132,15 +128,14 @@ def rename_file(file_info: dict):
if __name__ == "__main__": if __name__ == "__main__":
log = get_logger(args.verbose, args.config) log = get_logger(args.verbose, args.config)
log.info("kontor.download started") log.info("kontor.download started")
api_data = get_api_config(log, args.config) apiConfig = get_api_config(log, args.config)
host = api_data["host"] server: Server = apiConfig.server[0]
port = api_data["port"] data = server.request(log=log, table="media_file", param="download=true")
token = api_data["token"] # url: str = f"http://{host}:{port}/api/media/files?download=true"
url: str = f"http://{host}:{port}/api/media/files?download=true" # headers: Dict[str, str] = {"Authorization": f"Bearer {token}"}
headers: Dict[str, str] = {"Authorization": f"Bearer {token}"} # response = requests.get(url, headers=headers)
response = requests.get(url, headers=headers) # log.info(f"Status: {response.status_code}")
log.info(f"Status: {response.status_code}") # data = response.json()
data = response.json()
entries_count = len(data) entries_count = len(data)
log.info(f"data: {entries_count}") log.info(f"data: {entries_count}")
mediafile_index = 1 mediafile_index = 1
@@ -158,15 +153,15 @@ if __name__ == "__main__":
match download_status: match download_status:
case FileStatus.DOWNLOADED: case FileStatus.DOWNLOADED:
rename_file(item) rename_file(item)
update_status(file_id, item, api_data) update_status(file_id, item, server=server, log=log)
case FileStatus.RENAMED: case FileStatus.RENAMED:
log.info("update status") log.info("update status")
update_status(file_id, item, api_data) update_status(file_id, item, server=server, log=log)
case FileStatus.UNKNOWN: case FileStatus.UNKNOWN:
download_file(link, item, args.dir) download_file(link, item, args.dir)
rename_file(item) rename_file(item)
log.info(f"{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}") log.warning(f"processed {mediafile_index}/{entries_count}")
if args.limit and args.limit <= mediafile_index: if args.limit and args.limit <= mediafile_index:
break break
-58
View File
@@ -1,58 +0,0 @@
"""
copy data from JSON to MariaDB
"""
from argparse import ArgumentParser, ArgumentDefaultsHelpFormatter
from pathlib import Path
from config import get_logger, get_database_cursors
import mariadb
import json
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('--file', '-f', default='~/.sync/media/data.json')
args = parser.parse_args()
def copy_data(mariadb_conn, data_file: Path, log):
mariadb_cursor = mariadb_conn.cursor()
import_file = Path(data_file)
if not import_file.exists():
log.info(f"File {data_file} does not exist. Do nothing.")
return
log.info("read json file")
with open(data_file, 'r') as json_file:
json_load = json.load(json_file)
for table in json_load:
log.info(f"{table}: {len(json_load[table])}")
# result[table] = import_table(table, json_load[table])
truncate_statement = 'TRUNCATE {}'.format(table)
#log.info(f"truncate: {truncate_statement}")
mariadb_cursor.execute("SET FOREIGN_KEY_CHECKS = 0")
mariadb_cursor.execute(truncate_statement)
items = json_load[table]
for item in items:
#log.info(f"item: {item}")
values = []
columns = []
for (key, value) in item.items():
columns.append(key)
values.append(value)
row = tuple(values)
log.info(f"values: {row}")
insert_statement = 'INSERT INTO {}({}) VALUES({})'.format(table, ', '.join(columns), ', '.join(['?']*len(columns)))
#log.info(f"statement: {insert_statement}")
mariadb_cursor.execute(insert_statement, row)
try:
mariadb_conn.commit()
except mariadb.Error as error:
log.info('insert failed with %s', error)
if __name__ == '__main__':
logger = get_logger(args.verbose, args.config)
logger.info('kontor.json_to_mariadb started')
_, m_conn = get_database_cursors(logger, args.config)
copy_data(m_conn, args.file, logger)
m_conn.close()
logger.info('kontor.json_to_mariadb finished')
-152
View File
@@ -1,152 +0,0 @@
"""
copy data from JSON to Postgres
"""
from argparse import ArgumentParser, ArgumentDefaultsHelpFormatter
from pathlib import Path
from typing import Dict, List
from config import get_logger, get_database_cursors
import json
from psycopg2.sql import SQL
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('--file', '-f', default='~/.sync/media/data.json')
args = parser.parse_args()
def copy_data(postgres_conn, data_file: Path, log):
postgres_cursor = postgres_conn.cursor()
import_file = Path(data_file)
if not import_file.exists():
log.info(f"File {data_file} does not exist. Do nothing.")
return
log.info("read json file")
with open(data_file, 'r') as json_file:
json_load = json.load(json_file)
postgres_cursor.execute("SET session_replication_role='replica'")
for table in json_load:
log.info(f"{table}: {len(json_load[table])}")
# result[table] = import_table(table, json_load[table])
truncate_statement = 'TRUNCATE {} CASCADE'.format(table)
#log.info(f"truncate: {truncate_statement}")
try:
postgres_cursor.execute(truncate_statement)
except:
log.info(f"statement: {insert_statement} FAILED")
items = json_load[table]
for item in items:
#log.info(f"item: {item}")
values = []
columns = []
for (key, value) in item.items():
columns.append(key)
values.append(value)
row = tuple(values)
#log.info(f"values: {row}")
insert_statement = 'INSERT INTO {}({}) VALUES({})'.format(table, ', '.join(columns), ', '.join(['%s']*len(columns)))
#log.info(f"statement: {insert_statement}")
try:
postgres_cursor.execute(SQL(insert_statement), row)
postgres_conn.commit()
except:
log.info(f'insert failed with {insert_statement}')
postgres_cursor.execute("SET session_replication_role='origin'")
def load_json(data_file, log) -> dict:
import_file = Path(data_file)
if not import_file.exists():
log.info(f"File {data_file} does not exist. Do nothing.")
return
log.info("read json file")
with open(data_file, 'r') as json_file:
json_load = json.load(json_file)
return json_load
def insert_data(postgres_conn, data: dict, log):
postgres_cursor = postgres_conn.cursor()
log.info("insert data")
table_list = []
table_list = ['worktype', 'artist', 'publisher', 'volume', 'comic', 'issue', 'story_arc', 'trade_paperback', 'comic_work']
table_list.extend(['sport', 'team', 'field_position', 'vendor', 'player', 'rooster', 'card_set', 'card'])
#table_list.extend(['card'])
table_list.extend(['media_file', 'media_video', 'media_actor', 'media_article', 'media_actor_file'])
#table_list.extend(['media_actor_file'])
table_list.extend(['profile', 'permission', 'token', 'assignment'])
table_list.extend(['mail', 'mail_account', 'module_data', 'meta_data_table', 'meta_data_column'])
table_list.extend(['meta_data_table', 'meta_data_column'])
table_list.extend(['book', 'author', 'article', 'bookshelf_publisher', 'book_author', 'article_author'])
#if len(table_list) != 37:
# log.info(f"number of tables incorrect: {len(table_list)}")
# return
for table in table_list:
log.info(f"{table}: {len(data[table])}")
truncate_statement = 'DELETE FROM {}'.format(table)
log.info(f"truncate: {truncate_statement}")
try:
postgres_cursor.execute(truncate_statement)
postgres_conn.commit()
except:
log.info(f"statement: {truncate_statement} FAILED")
items = data[table]
for item in items:
# log.info(f"item: {item}")
values = []
columns = []
for (key, value) in item.items():
columns.append(key)
values.append(value)
row = tuple(values)
# log.info(f"values: {row}")
insert_statement = 'INSERT INTO {}({}) VALUES({})'.format(table, ', '.join(columns),
', '.join(['%s'] * len(columns)))
# log.info(f"statement: {insert_statement}")
try:
postgres_cursor.execute(SQL(insert_statement), row)
postgres_conn.commit()
except:
log.info(f'insert failed with {insert_statement}')
def parse_table_order(data: dict, log):
log.info("parse_table_order")
table_refs: Dict[str, List[str]] = {}
for table in data:
log.info(f"{table}: {len(data[table])}")
items = data[table]
table_refs[table] = []
if len(items) == 0:
continue
item = items[0]
for key, _ in item.items():
if key.endswith('_id'):
ref = key[0:-3]
log.info(f"table {table} has reference to {ref}")
if table in table_refs:
table_refs[table].append(ref)
else:
table_refs[table] = [ref]
log.info(f"parsed refs: {table_refs}")
table_order = []
for table in table_refs:
if len(table_refs[table]) == 0:
log.info(f"insert {table} at beginning")
table_order.insert(0, table)
else:
log.info(f"insert {table} at end")
table_order.append(table)
log.info(f"table_list: {len(table_order)}: {table_order}")
if __name__ == '__main__':
logger = get_logger(args.verbose, args.config)
logger.info('kontor.json_to_postgres started')
_, _, p_conn = get_database_cursors(logger, args.config)
data = load_json(args.file, logger)
#parse_table_order(data, logger)
insert_data(p_conn, data, logger)
#copy_data(p_conn, args.file, logger)
p_conn.close()
logger.info('kontor.json_to_postgres finished')
-64
View File
@@ -1,64 +0,0 @@
from argparse import ArgumentParser, ArgumentDefaultsHelpFormatter
from pathlib import Path
import logging.config
import yaml
from platformdirs import PlatformDirs
from sqlalchemy import create_engine, select
from sqlalchemy.orm import sessionmaker
from schema import Comic, Publisher, Base
parser = ArgumentParser(formatter_class=ArgumentDefaultsHelpFormatter)
parser.add_argument('--recreate-db', action='store_true')
parser.add_argument('--verbose', '-v', action='count', default=0)
parser.add_argument('--file', '-f', default='~/data.json')
parser.add_argument('--config', '-c', default='kontor-docker')
args = parser.parse_args()
def get_logger(level: int, config: str):
dirs = PlatformDirs(config)
logging_config = Path(dirs.user_config_dir, 'logging-config.yaml')
with open(logging_config, 'rt') as f:
configDict = yaml.safe_load(f.read())
logging.config.dictConfig(configDict)
logger = logging.getLogger('development')
if level is not None:
match level:
case 0:
logger.setLevel(logging.INFO)
case 1:
logger.setLevel(logging.DEBUG)
case _:
logger.setLevel(logging.CRITICAL)
return logger
if __name__ == '__main__':
log = get_logger(args.verbose, args.config)
log.info('kontor started')
dirs = PlatformDirs(args.config)
database_config = Path(dirs.user_config_dir, 'database-config.yaml')
with open(database_config, 'rt') as f:
db_config = yaml.safe_load(f.read())
print(db_config)
connect_string = ('mariadb+mariadbconnector://{}:{}@{}:{}/{}'.format(
db_config['mariadb']['user'],
db_config['mariadb']['password'],
db_config['mariadb']['host'],
db_config['mariadb']['port'],
db_config['mariadb']['database']
))
engine = create_engine(connect_string)
Base.metadata.create_all(bind=engine, checkfirst=True)
__session__ = sessionmaker(engine)
with __session__() as session:
comics = session.scalars(select(Comic)).all()
for comic in comics:
print(comic)
publishers = session.scalars(select(Publisher)).all()
for publisher in publishers:
print(publisher)
print(len(publisher.comics))
log.info('kontor finished')
-80
View File
@@ -1,80 +0,0 @@
"""
read file with URLs and store in DB
"""
from argparse import ArgumentParser, ArgumentDefaultsHelpFormatter
import logging
import json
from proton import Message, Event
from proton.handlers import MessagingHandler
from proton.reactor import Container
parser = ArgumentParser(formatter_class=ArgumentDefaultsHelpFormatter)
parser.add_argument('-f', '--links', help='file with links')
parser.add_argument('--verbose', '-v', action='count', default=0)
args = parser.parse_args()
def read_links_file(links_file):
with open(links_file, 'r') as input_file:
lines = input_file.readlines()
return lines
class AddLinkMessage(MessagingHandler):
def __init__(self, server, url, log):
super(AddLinkMessage, self).__init__()
log.info("create AddLinkMessage")
self.server = server
self.address = "KontorMediaFile::add_link_file"
self.url = url
self.log = log
def on_start(self, event: Event):
self.log.info("Connection...")
conn = event.container.connect(self.server, user="artemis", password="artemis")
event.container.create_sender(conn, self.address)
def on_connection_opened(self, event: Event) -> None:
self.log.debug("connection open")
def on_connection_error(self, event: Event) -> None:
self.log.info(f"error: {event}")
def on_disconnected(self, event: Event) -> None:
self.log.debug(f"disconnected: {repr(event)}")
def on_sendable(self, event: Event):
self.log.info("send message")
message = Message(body=self.url, address=self.address, content_type="application/json", durable=True)
delivery = event.sender.send(message)
self.log.info(f"Delivery {delivery} sent")
event.connection.close()
def on_accepted(self, event: Event) -> None:
self.log.info(f"accepted Delivery: {event.delivery.remote_state}")
def on_rejected(self, event: Event) -> None:
self.log.info(f"rejected Delivery: {event.delivery}")
if __name__ == '__main__':
logging.basicConfig(level=logging.INFO, format='[%(asctime)s] {%(filename)s:%(lineno)d} %(levelname)s - %(message)s')
logging.info('kontor.read_list started')
#conn = stomp.Connection([('127.0.0.1', '61616')])
#conn.connect('artemis', 'artemis', wait=True)
if args.links:
logging.info("read links from file")
links = read_links_file(args.links)
for link in links:
data_dict = {'url': link.strip()}
data = json.dumps(data_dict)
logging.info("send link message")
handler = AddLinkMessage("amqp://127.0.0.1:5672", data, logging)
container = Container(handler)
container.container_id = "process_add_links"
container.run()
# conn.send(body=data, destination='KontorMediaFile::add_link_file', headers={'content-type': 'application/json'})
#conn.disconnect()
logging.info('kontor.read_list finished')
-9
View File
@@ -1,9 +0,0 @@
sqlalchemy
pathlib
platformdirs
pyyaml
beautifulsoup4
sqlmodel
requests
fastapi[standard]
+21
View File
@@ -0,0 +1,21 @@
from argparse import ArgumentDefaultsHelpFormatter, ArgumentParser
from dataclasses import dataclass
from api import 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("--cleanup", "-d", action="store_true")
args = parser.parse_args()
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))
logger.info("kontor.sync finished")
+1
View File
@@ -30,6 +30,7 @@ plugins {
alias(libs.plugins.asciidoctorPdf) alias(libs.plugins.asciidoctorPdf)
alias(libs.plugins.asciidoctorConvert) alias(libs.plugins.asciidoctorConvert)
alias(libs.plugins.asciidoctorGems) alias(libs.plugins.asciidoctorGems)
id "de.infolektuell.typst" version "0.8.0"
} }
repositories { repositories {
+3 -2
View File
@@ -1,4 +1,4 @@
FROM node:22.18.0-bookworm-slim AS build FROM docker.io/node:20-alpine AS build
WORKDIR /app WORKDIR /app
ENV PATH=/app/node_modules/.bin:$PATH ENV PATH=/app/node_modules/.bin:$PATH
COPY package.json /app/package.json COPY package.json /app/package.json
@@ -7,7 +7,8 @@ RUN npm install -g @vue/cli
COPY . /app COPY . /app
RUN npm run build RUN npm run build
FROM nginx:1.29.0-alpine FROM docker.io/library/nginx:stable-alpine
COPY nginx.conf /etc/nginx/conf.d/default.conf
COPY --from=build /app/dist /usr/share/nginx/html COPY --from=build /app/dist /usr/share/nginx/html
EXPOSE 80 EXPOSE 80
CMD ["nginx", "-g", "daemon off;"] CMD ["nginx", "-g", "daemon off;"]
+38
View File
@@ -0,0 +1,38 @@
server {
listen 8700;
# Root-Verzeichnis für den Server setzen (wir kopieren unsere Anwendung hierher)
root /usr/share/nginx/html;
# Definieren der Standard-Indexdatei (Angular erstellt die Datei index.html für uns und sie befindet sich im oben genannten Verzeichnis)
index index.html;
# Cache-Header für Medien-ASsets
location ~* \.(?:cur|jpe?g|gif|htc|ico|png|xml|otf|ttf|eot|woff|woff2|svg)$ {
access_log off;
add_header Pragma "must-revalidate, public";
add_header Cache-Control "must-revalidate, public";
expires max;
tcp_nodelay off;
}
# Cache-Header für HTML, CSS und JS-Dateien
location ~* \.(?:css|js|html)$ {
access_log off;
add_header Pragma "must-revalidate, public";
add_header Cache-Control "must-revalidate, public";
expires 2d;
tcp_nodelay off;
}
# Konfiguration für den /-Pfad
location / {
# Zunächst versuchen wir die angeforderte URI auzuliefern
# Klappt das nicht, versuchen wir es mit einem abschließenden Slash
# Klappt auch das nicht, liefern wir die index.html aus.
# Das ist nötig, damit Angular-Routen korrekt augeflöst und ausgeliefert werden
try_files $uri $uri/ /index.html;
}
}
+3 -3
View File
@@ -5,12 +5,12 @@ const API_URL = "http://localhost:8200/";
class AuthService { class AuthService {
login(user) { login(user) {
return axios return axios
.post(API_URL + "login", { .post(API_URL + "api/login/token", {
email: user.username, username: user.username,
password: user.password, password: user.password,
}) })
.then((response) => { .then((response) => {
if (response.data.accessToken) { if (response.data.access_token) {
localStorage.setItem("user", JSON.stringify(response.data)); localStorage.setItem("user", JSON.stringify(response.data));
} }
return response.data; return response.data;
+2
View File
@@ -20,4 +20,6 @@ echo " => Baue Image localhost/kontor-javalin:0.3.0"
buildah build -t kontor-javalin:0.3.0 kontor-javalin buildah build -t kontor-javalin:0.3.0 kontor-javalin
echo " => Baue Image localhost/kontor-vue:0.3.0" echo " => Baue Image localhost/kontor-vue:0.3.0"
buildah build -t kontor-vue:0.3.0 kontor-vue buildah build -t kontor-vue:0.3.0 kontor-vue
echo " => Baue Image localhost/kontor-angular:0.3.0"
buildah build -t kontor-angular:0.3.0 kontor-angular
Executable
+11
View File
@@ -0,0 +1,11 @@
#!/bin/sh
# script/service: Setup user systemd scripts
set -e
cd "$(dirname "$0")/.."
echo "==> Setting up systemd user services"
cp service/container/*.service $HOME/.config/systemd/user/
+21 -2
View File
@@ -8,7 +8,7 @@ cd "$(dirname "$0")/.."
script/bootstrap script/bootstrap
script/cibuild #script/cibuild
echo "==> Setting up pod kontor" echo "==> Setting up pod kontor"
@@ -29,7 +29,8 @@ else
-p 8400:8400 \ -p 8400:8400 \
-p 8500:8500 \ -p 8500:8500 \
-p 8600:8600 \ -p 8600:8600 \
-p 8700:80 \ -p 8700:8700 \
-p 8800:8800 \
-p 8900:8080 \ -p 8900:8080 \
-p 61616:61616 \ -p 61616:61616 \
-p 8161:8161 \ -p 8161:8161 \
@@ -235,3 +236,21 @@ else
localhost/kontor-vue:0.3.0 localhost/kontor-vue:0.3.0
fi fi
echo "==> Setting up container kontor-angular"
if podman image exists localhost/kontor-angular:0.3.0; then
echo " => Image localhost/kontor-angular:0.3.0 available"
fi
if podman container exists kontor-angular; then
if podman ps -q --filter "name=kontor-angular"; then
echo " => kontor-angular is running"
fi
else
podman run -d \
--replace \
--pod kontor \
--name kontor-angular \
--label "io.containers.autoupdate=local" \
--label "PODMAN_SYSTEMD_UNIT=container-kontor-angular.service" \
localhost/kontor-angular:0.3.0
fi
+3 -1
View File
@@ -9,7 +9,9 @@ cd "$(dirname "$0")/.."
echo "==> Stopping and removing pod kontor" echo "==> Stopping and removing pod kontor"
if podman pod exists kontor; then if podman pod exists kontor; then
podman pod stop kontor podman pod stop kontor
podman pod rm kontor
fi fi
script/cibuild
script/setup script/setup
@@ -1,12 +1,11 @@
# container-activemq.service # kontor-activemq.service
# autogenerated by Podman 4.9.3 # autogenerated by Podman 4.9.3
# Fri Feb 6 17:23:12 CET 2026 # Sun Apr 19 16:28:14 CEST 2026
[Unit] [Unit]
Description=Podman container-activemq.service Description=Podman kontor-activemq.service
Documentation=man:podman-generate-systemd(1) Documentation=man:podman-generate-systemd(1)
Wants=network-online.target Wants=network-online.target
After=network-online.target
RequiresMountsFor=%t/containers RequiresMountsFor=%t/containers
BindsTo=pod-kontor.service BindsTo=pod-kontor.service
After=pod-kontor.service After=pod-kontor.service
@@ -1,12 +1,11 @@
# container-adminer.service # kontor-adminer.service
# autogenerated by Podman 4.9.3 # autogenerated by Podman 4.9.3
# Fri Feb 6 17:23:12 CET 2026 # Sun Apr 19 16:28:14 CEST 2026
[Unit] [Unit]
Description=Podman container-adminer.service Description=Podman kontor-adminer.service
Documentation=man:podman-generate-systemd(1) Documentation=man:podman-generate-systemd(1)
Wants=network-online.target Wants=network-online.target
After=network-online.target
RequiresMountsFor=%t/containers RequiresMountsFor=%t/containers
BindsTo=pod-kontor.service BindsTo=pod-kontor.service
After=pod-kontor.service After=pod-kontor.service
@@ -23,7 +22,8 @@ ExecStart=/usr/bin/podman run \
--sdnotify=conmon \ --sdnotify=conmon \
-d \ -d \
--replace \ --replace \
--name adminer docker.io/library/adminer --name kontor-adminer \
docker.io/library/adminer
ExecStop=/usr/bin/podman stop \ ExecStop=/usr/bin/podman stop \
--ignore -t 10 \ --ignore -t 10 \
--cidfile=%t/%n.ctr-id --cidfile=%t/%n.ctr-id
+41
View File
@@ -0,0 +1,41 @@
# kontor-angular.service
# autogenerated by Podman 4.9.3
# Sun Apr 19 16:28:14 CEST 2026
[Unit]
Description=Podman kontor-angular.service
Documentation=man:podman-generate-systemd(1)
Wants=network-online.target
After=network-online.target
RequiresMountsFor=%t/containers
BindsTo=pod-kontor.service
After=pod-kontor.service
[Service]
Environment=PODMAN_SYSTEMD_UNIT=%n
Restart=on-failure
TimeoutStopSec=70
ExecStart=/usr/bin/podman run \
--cidfile=%t/%n.ctr-id \
--cgroups=no-conmon \
--rm \
--pod-id-file %t/pod-kontor.pod-id \
--sdnotify=conmon \
-d \
--replace \
--name kontor-angular \
--label io.containers.autoupdate=local \
--label PODMAN_SYSTEMD_UNIT=kontor-angular.service \
localhost/kontor-angular:0.3.0
ExecStop=/usr/bin/podman stop \
--ignore -t 10 \
--cidfile=%t/%n.ctr-id
ExecStopPost=/usr/bin/podman rm \
-f \
--ignore -t 10 \
--cidfile=%t/%n.ctr-id
Type=notify
NotifyAccess=all
[Install]
WantedBy=default.target
@@ -1,12 +1,11 @@
# container-kontor-api.service # kontor-api.service
# autogenerated by Podman 4.9.3 # autogenerated by Podman 4.9.3
# Fri Feb 6 17:23:12 CET 2026 # Sun Apr 19 16:28:14 CEST 2026
[Unit] [Unit]
Description=Podman container-kontor-api.service Description=Podmanco kontor-api.service
Documentation=man:podman-generate-systemd(1) Documentation=man:podman-generate-systemd(1)
Wants=network-online.target Wants=network-online.target
After=network-online.target
RequiresMountsFor=%t/containers RequiresMountsFor=%t/containers
BindsTo=pod-kontor.service BindsTo=pod-kontor.service
After=pod-kontor.service After=pod-kontor.service
@@ -25,10 +24,12 @@ ExecStart=/usr/bin/podman run \
--replace \ --replace \
--name kontor-api \ --name kontor-api \
--label io.containers.autoupdate=local \ --label io.containers.autoupdate=local \
--label PODMAN_SYSTEMD_UNIT=container-kontor-api.service "--health-cmd=curl -f http://kontor-api:8200/health || exit 1" \ --label PODMAN_SYSTEMD_UNIT=kontor-api.service \
--health-cmd="curl -f http://kontor-api:8200/health || exit 1" \
--health-interval=1s \ --health-interval=1s \
--health-timeout=5s \ --health-timeout=5s \
--health-retries=10 localhost/kontor-api:0.3.0 --health-retries=10 \
localhost/kontor-api:0.3.0
ExecStop=/usr/bin/podman stop \ ExecStop=/usr/bin/podman stop \
--ignore -t 10 \ --ignore -t 10 \
--cidfile=%t/%n.ctr-id --cidfile=%t/%n.ctr-id
@@ -1,9 +1,9 @@
# container-kontor-echo.service # kontor-echo.service
# autogenerated by Podman 4.9.3 # autogenerated by Podman 4.9.3
# Fri Feb 6 17:23:12 CET 2026 # Sun Apr 19 16:28:14 CEST 2026
[Unit] [Unit]
Description=Podman container-kontor-echo.service Description=Podman kontor-echo.service
Documentation=man:podman-generate-systemd(1) Documentation=man:podman-generate-systemd(1)
Wants=network-online.target Wants=network-online.target
After=network-online.target After=network-online.target
@@ -25,10 +25,12 @@ ExecStart=/usr/bin/podman run \
--replace \ --replace \
--name kontor-echo \ --name kontor-echo \
--label io.containers.autoupdate=local \ --label io.containers.autoupdate=local \
--label PODMAN_SYSTEMD_UNIT=container-kontor-echo.service "--health-cmd=curl -f http://kontor-echo:8400/health || exit 1" \ --label PODMAN_SYSTEMD_UNIT=kontor-echo.service \
--health-cmd="curl -f http://kontor-echo:8400/health || exit 1" \
--health-interval=1s \ --health-interval=1s \
--health-timeout=5s \ --health-timeout=5s \
--health-retries=10 localhost/kontor-echo:0.3.0 --health-retries=10 \
localhost/kontor-echo:0.3.0
ExecStop=/usr/bin/podman stop \ ExecStop=/usr/bin/podman stop \
--ignore -t 10 \ --ignore -t 10 \
--cidfile=%t/%n.ctr-id --cidfile=%t/%n.ctr-id
@@ -1,9 +1,9 @@
# container-kontor-fiber.service # kontor-fiber.service
# autogenerated by Podman 4.9.3 # autogenerated by Podman 4.9.3
# Fri Feb 6 17:23:12 CET 2026 # Sun Apr 19 16:28:14 CEST 2026
[Unit] [Unit]
Description=Podman container-kontor-fiber.service Description=Podman kontor-fiber.service
Documentation=man:podman-generate-systemd(1) Documentation=man:podman-generate-systemd(1)
Wants=network-online.target Wants=network-online.target
After=network-online.target After=network-online.target
@@ -25,10 +25,12 @@ ExecStart=/usr/bin/podman run \
--replace \ --replace \
--name kontor-fiber \ --name kontor-fiber \
--label io.containers.autoupdate=local \ --label io.containers.autoupdate=local \
--label PODMAN_SYSTEMD_UNIT=container-kontor-fiber.service "--health-cmd=curl -f http://kontor-echo:8500/health || exit 1" \ --label PODMAN_SYSTEMD_UNIT=kontor-fiber.service \
--health-cmd="curl -f http://kontor-echo:8500/health || exit 1" \
--health-interval=1s \ --health-interval=1s \
--health-timeout=5s \ --health-timeout=5s \
--health-retries=10 localhost/kontor-fiber:0.3.0 --health-retries=10 \
localhost/kontor-fiber:0.3.0
ExecStop=/usr/bin/podman stop \ ExecStop=/usr/bin/podman stop \
--ignore -t 10 \ --ignore -t 10 \
--cidfile=%t/%n.ctr-id --cidfile=%t/%n.ctr-id
@@ -1,9 +1,9 @@
# container-kontor-javalin.service # kontor-javalin.service
# autogenerated by Podman 4.9.3 # autogenerated by Podman 4.9.3
# Fri Feb 6 17:23:12 CET 2026 # Sun Apr 19 16:28:14 CEST 2026
[Unit] [Unit]
Description=Podman container-kontor-javalin.service Description=Podman kontor-javalin.service
Documentation=man:podman-generate-systemd(1) Documentation=man:podman-generate-systemd(1)
Wants=network-online.target Wants=network-online.target
After=network-online.target After=network-online.target
@@ -25,10 +25,12 @@ ExecStart=/usr/bin/podman run \
--replace \ --replace \
--name kontor-javalin \ --name kontor-javalin \
--label io.containers.autoupdate=local \ --label io.containers.autoupdate=local \
--label PODMAN_SYSTEMD_UNIT=container-kontor-javalin.service "--health-cmd=curl -f http://kontor-echo:8600/health || exit 1" \ --label PODMAN_SYSTEMD_UNIT=kontor-javalin.service \
--health-cmd="curl -f http://kontor-echo:8600/health || exit 1" \
--health-interval=1s \ --health-interval=1s \
--health-timeout=5s \ --health-timeout=5s \
--health-retries=10 localhost/kontor-javalin:0.3.0 --health-retries=10 \
localhost/kontor-javalin:0.3.0
ExecStop=/usr/bin/podman stop \ ExecStop=/usr/bin/podman stop \
--ignore -t 10 \ --ignore -t 10 \
--cidfile=%t/%n.ctr-id --cidfile=%t/%n.ctr-id
@@ -1,9 +1,9 @@
# container-postgres.service # kontor-postgres.service
# autogenerated by Podman 4.9.3 # autogenerated by Podman 4.9.3
# Fri Feb 6 17:23:12 CET 2026 # Sun Apr 19 16:28:14 CEST 2026
[Unit] [Unit]
Description=Podman container-postgres.service Description=Podman kontor-postgres.service
Documentation=man:podman-generate-systemd(1) Documentation=man:podman-generate-systemd(1)
Wants=network-online.target Wants=network-online.target
After=network-online.target After=network-online.target
@@ -27,10 +27,12 @@ ExecStart=/usr/bin/podman run \
-e POSTGRES_PASSWORD=kontor \ -e POSTGRES_PASSWORD=kontor \
-e POSTGRES_DB=kontor \ -e POSTGRES_DB=kontor \
-e POSTGRES_USER=kontor \ -e POSTGRES_USER=kontor \
-v kontor-db:/var/lib/postgresql/data "--health-cmd=pg_isready -U kontor || exit 1" \ -v kontor-db:/var/lib/postgresql/data \
--health-cmd="pg_isready -U kontor || exit 1" \
--health-interval=1s \ --health-interval=1s \
--health-timeout=5s \ --health-timeout=5s \
--health-retries=10 docker.io/library/postgres:17 --health-retries=10 \
docker.io/library/postgres:17
ExecStop=/usr/bin/podman stop \ ExecStop=/usr/bin/podman stop \
--ignore -t 10 \ --ignore -t 10 \
--cidfile=%t/%n.ctr-id --cidfile=%t/%n.ctr-id
@@ -1,9 +1,9 @@
# container-kontor-quarkus.service # kontor-quarkus.service
# autogenerated by Podman 4.9.3 # autogenerated by Podman 4.9.3
# Fri Feb 6 17:23:12 CET 2026 # Sun Apr 19 16:28:14 CEST 2026
[Unit] [Unit]
Description=Podman container-kontor-quarkus.service Description=Podman kontor-quarkus.service
Documentation=man:podman-generate-systemd(1) Documentation=man:podman-generate-systemd(1)
Wants=network-online.target Wants=network-online.target
After=network-online.target After=network-online.target
@@ -25,10 +25,12 @@ ExecStart=/usr/bin/podman run \
--replace \ --replace \
--name kontor-quarkus \ --name kontor-quarkus \
--label io.containers.autoupdate=local \ --label io.containers.autoupdate=local \
--label PODMAN_SYSTEMD_UNIT=container-kontor-quarkus.service "--health-cmd=curl -f http://kontor-quarkus:8300/q/health || exit 1" \ --label PODMAN_SYSTEMD_UNIT=kontor-quarkus.service \
--health-cmd="curl -f http://kontor-quarkus:8300/q/health || exit 1" \
--health-interval=1s \ --health-interval=1s \
--health-timeout=5s \ --health-timeout=5s \
--health-retries=10 localhost/kontor-quarkus:0.3.0 --health-retries=10 \
localhost/kontor-quarkus:0.3.0
ExecStop=/usr/bin/podman stop \ ExecStop=/usr/bin/podman stop \
--ignore -t 10 \ --ignore -t 10 \
--cidfile=%t/%n.ctr-id --cidfile=%t/%n.ctr-id
@@ -1,9 +1,9 @@
# container-kontor-spring.service # kontor-spring.service
# autogenerated by Podman 4.9.3 # autogenerated by Podman 4.9.3
# Fri Feb 6 17:23:12 CET 2026 # Sun Apr 19 16:28:14 CEST 2026
[Unit] [Unit]
Description=Podman container-kontor-spring.service Description=Podman kontor-spring.service
Documentation=man:podman-generate-systemd(1) Documentation=man:podman-generate-systemd(1)
Wants=network-online.target Wants=network-online.target
After=network-online.target After=network-online.target
@@ -25,7 +25,8 @@ ExecStart=/usr/bin/podman run \
--replace \ --replace \
--name kontor-spring \ --name kontor-spring \
--label io.containers.autoupdate=local \ --label io.containers.autoupdate=local \
--label PODMAN_SYSTEMD_UNIT=container-kontor-spring.service localhost/kontor-spring:0.3.0 --label PODMAN_SYSTEMD_UNIT=kontor-spring.service \
localhost/kontor-spring:0.3.0
ExecStop=/usr/bin/podman stop \ ExecStop=/usr/bin/podman stop \
--ignore -t 10 \ --ignore -t 10 \
--cidfile=%t/%n.ctr-id --cidfile=%t/%n.ctr-id
+41
View File
@@ -0,0 +1,41 @@
# kontor-vue.service
# autogenerated by Podman 4.9.3
# Sun Apr 19 16:28:14 CEST 2026
[Unit]
Description=Podman kontor-vue.service
Documentation=man:podman-generate-systemd(1)
Wants=network-online.target
After=network-online.target
RequiresMountsFor=%t/containers
BindsTo=pod-kontor.service
After=pod-kontor.service
[Service]
Environment=PODMAN_SYSTEMD_UNIT=%n
Restart=on-failure
TimeoutStopSec=70
ExecStart=/usr/bin/podman run \
--cidfile=%t/%n.ctr-id \
--cgroups=no-conmon \
--rm \
--pod-id-file %t/pod-kontor.pod-id \
--sdnotify=conmon \
-d \
--replace \
--name kontor-vue \
--label io.containers.autoupdate=local \
--label PODMAN_SYSTEMD_UNIT=kontor-vue.service \
localhost/kontor-vue:0.3.0
ExecStop=/usr/bin/podman stop \
--ignore -t 10 \
--cidfile=%t/%n.ctr-id
ExecStopPost=/usr/bin/podman rm \
-f \
--ignore -t 10 \
--cidfile=%t/%n.ctr-id
Type=notify
NotifyAccess=all
[Install]
WantedBy=default.target
@@ -1,6 +1,6 @@
# pod-kontor.service # pod-kontor.service
# autogenerated by Podman 4.9.3 # autogenerated by Podman 4.9.3
# Fri Feb 6 17:23:12 CET 2026 # Sun Apr 19 16:28:14 CEST 2026
[Unit] [Unit]
Description=Podman pod-kontor.service Description=Podman pod-kontor.service
@@ -8,8 +8,8 @@ Documentation=man:podman-generate-systemd(1)
Wants=network-online.target Wants=network-online.target
After=network-online.target After=network-online.target
RequiresMountsFor=/run/user/1000/containers RequiresMountsFor=/run/user/1000/containers
Wants=container-activemq.service container-adminer.service container-kontor-api.service container-kontor-echo.service container-kontor-fiber.service container-kontor-javalin.service container-kontor-quarkus.service container-kontor-spring.service container-postgres.service Wants=kontor-activemq.service kontor-adminer.service kontor-angular.service kontor-api.service kontor-echo.service kontor-fiber.service kontor-javalin.service kontor-quarkus.service kontor-spring.service kontor-vue.service kontor-postgres.service
Before=container-activemq.service container-adminer.service container-kontor-api.service container-kontor-echo.service container-kontor-fiber.service container-kontor-javalin.service container-kontor-quarkus.service container-kontor-spring.service container-postgres.service Before=kontor-activemq.service kontor-adminer.service kontor-angular.service kontor-api.service kontor-echo.service kontor-fiber.service kontor-javalin.service kontor-quarkus.service kontor-spring.service kontor-vue.service kontor-postgres.service
[Service] [Service]
Environment=PODMAN_SYSTEMD_UNIT=%n Environment=PODMAN_SYSTEMD_UNIT=%n
@@ -28,6 +28,8 @@ ExecStartPre=/usr/bin/podman pod create \
-p 8400:8400 \ -p 8400:8400 \
-p 8500:8500 \ -p 8500:8500 \
-p 8600:8600 \ -p 8600:8600 \
-p 8700:8700 \
-p 8800:8800 \
-p 8900:8080 \ -p 8900:8080 \
-p 61616:61616 \ -p 61616:61616 \
-p 8161:8161 \ -p 8161:8161 \
+16
View File
@@ -0,0 +1,16 @@
[Unit]
Description=kontor-api via UV
After=network.target
[Service]
Type=simple
User=tpeetz
Group=tpeetz
Environment="DB_SERVER=localhost"
WorkingDirectory=/home/tpeetz/projects/kontor/kontor-api
ExecStart=/home/tpeetz/projects/kontor/kontor-api/.venv/bin/uvicorn src.main:kontor --log-level info --host 127.0.0.1 --port 8200
Restart=on-failure
[Install]
WantedBy=multi-user.target