Compare commits
78 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e2ff26f6bf | |||
| 0d99f383fb | |||
| c6fd80408b | |||
| d2d4deb350 | |||
| ee280e0b8e | |||
| 1c2c2f38a4 | |||
| b5cca50960 | |||
| 0a505fabcf | |||
| 4b8053c6e2 | |||
| 5df3c1c47e | |||
| adad4f88da | |||
| 957c7a702e | |||
| b5344a9ed1 | |||
| 16ad701eed | |||
| 2ba7465675 | |||
| c703dfc6e3 | |||
| b62cc89f71 | |||
| e741a46f69 | |||
| 4f87ff83ce | |||
| 4871f56320 | |||
| ec404c9956 | |||
| fc9db8200f | |||
| 69caa825de | |||
| 123be2e9c0 | |||
| 928536b414 | |||
| 8ed8599f17 | |||
| 435de2ec92 | |||
| 529a199262 | |||
| 510e2f8130 | |||
| da4f286180 | |||
| d9136e45f6 | |||
| b610947403 | |||
| 6e520a46f0 | |||
| b4a0c2d7a5 | |||
| ea9f596abe | |||
| 4bb7d61f80 | |||
| cc8a166f5c | |||
| a70bf8ae96 | |||
| 902ee03e3f | |||
| f1f49ab014 | |||
| 7a16225c3b | |||
| 8e13af5b8c | |||
| 767c069404 | |||
| dd5281e2a1 | |||
| aefd56d1ff | |||
| 293a0b3478 | |||
| 076466b895 | |||
| 370738ff14 | |||
| 1151b0e45e | |||
| 7c5c571716 | |||
| 1b636efdb7 | |||
| 374c242890 | |||
| 87b1c24783 | |||
| 456162da44 | |||
| 8cfb60f9a1 | |||
| 4c70046a32 | |||
| 546f8ebbdf | |||
| bd86379d07 | |||
| edcaed1b1a | |||
| 20ed0b2f40 | |||
| 3197288eee | |||
| 84d64f04d2 | |||
| 1ecf64a228 | |||
| 41d513e402 | |||
| aa4b47e032 | |||
| 3537642df9 | |||
| 06a48a03ac | |||
| d60e606663 | |||
| 13dad3961c | |||
| 4cf1941f44 | |||
| ace568a800 | |||
| c77adb0e04 | |||
| 7ff2bf912d | |||
| 72c1a7d265 | |||
| 2efacf6d67 | |||
| d6549132ea | |||
| bbf422cc5d | |||
| 4c96de27db |
@@ -13,4 +13,9 @@ kontor-schema/kontor_schema.egg-info
|
||||
kontor-gui/.pdm-python
|
||||
kontor-gui/dist
|
||||
fastapi/.coverage
|
||||
kontor-api/.coverage
|
||||
db-password.txt
|
||||
kontor-api/tests/test_main.py
|
||||
kontor-api/tests/test_db.db
|
||||
kontor-api/test_db.db
|
||||
couchdb-password.txt
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
kontor_api := kontor-api
|
||||
kontor_spring := kontor-spring
|
||||
kontor_servicemix := kontor-servicemix
|
||||
|
||||
.PHONY: all $(kontor_spring) $(kontor_api)
|
||||
all: $(kontor_spring) $(kontor_api)
|
||||
all: $(kontor_spring) $(kontor_api) $(kontor_servicemix)
|
||||
|
||||
$(kontor_spring) $(kontor_api):
|
||||
$(kontor_spring) $(kontor_api) $(kontor_servicemix):
|
||||
$(MAKE) --directory=$@ $(TARGET)
|
||||
|
||||
|
||||
+72
-15
@@ -1,44 +1,101 @@
|
||||
|
||||
services:
|
||||
mariadb:
|
||||
image: mariadb
|
||||
postgres:
|
||||
image: postgres
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: kontor
|
||||
MYSQL_USER: kontor
|
||||
MYSQL_PASSWORD: kontor
|
||||
MYSQL_DATABASE: kontor
|
||||
- POSTGRES_DB=kontor
|
||||
- POSTGRES_USER=kontor
|
||||
#- POSTGRES_PASSWORD_FILE=/run/secrets/db-password
|
||||
- POSTGRES_PASSWORD=kontor
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U kontor"]
|
||||
interval: 1s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
ports:
|
||||
- 3316:3306
|
||||
- 5432:5432
|
||||
networks:
|
||||
- database
|
||||
volumes:
|
||||
- mariadb-storage:/var/lib/mysql:rw
|
||||
- postgres-data:/var/lib/postgresql/data:rw
|
||||
secrets:
|
||||
- db-password
|
||||
adminer:
|
||||
image: adminer
|
||||
ports:
|
||||
- 8090:8080
|
||||
networks:
|
||||
- database
|
||||
- frontend
|
||||
couchdb:
|
||||
image: couchdb
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- COUCHDB_USER=admin
|
||||
- COUCHDB_PASSWORD=admin
|
||||
ports:
|
||||
- 5984:5984
|
||||
networks:
|
||||
- database
|
||||
- frontend
|
||||
volumes:
|
||||
- couchdb-data:/opt/couchdb/data
|
||||
secrets:
|
||||
- couchdb-password
|
||||
activemq:
|
||||
image: apache/activemq-artemis:latest-alpine
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- 61616:61616
|
||||
- 8161:8161
|
||||
- 5672:5672
|
||||
networks:
|
||||
- integration
|
||||
- frontend
|
||||
volumes:
|
||||
- activemq-data:/var/lib/artemis-instance
|
||||
kontor:
|
||||
image: kontor
|
||||
image: kontor:0.2.0-SNAPSHOT
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- database
|
||||
- integration
|
||||
- frontend
|
||||
ports:
|
||||
- 8000:8000
|
||||
volumes:
|
||||
- images-data:/data/images
|
||||
depends_on:
|
||||
- mariadb
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
kontor-api:
|
||||
image: kontor-api
|
||||
image: kontor-api:0.2.0-SNAPSHOT
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- database
|
||||
- integration
|
||||
- frontend
|
||||
ports:
|
||||
- 8800:8800
|
||||
volumes:
|
||||
- images-data:/data/images
|
||||
depends_on:
|
||||
- mariadb
|
||||
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
networks:
|
||||
database:
|
||||
integration:
|
||||
name: integration
|
||||
frontend:
|
||||
|
||||
volumes:
|
||||
mariadb-storage:
|
||||
postgres-data:
|
||||
couchdb-data:
|
||||
activemq-data:
|
||||
images-data:
|
||||
secrets:
|
||||
db-password:
|
||||
file: db-password.txt
|
||||
couchdb-password:
|
||||
file: couchdb-password.txt
|
||||
|
||||
|
||||
Binary file not shown.
@@ -1 +1,4 @@
|
||||
.env
|
||||
.coverage
|
||||
app.log
|
||||
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
## ------------------------------- Builder Stage ------------------------------ ##
|
||||
FROM python:3.13-bookworm AS builder
|
||||
|
||||
RUN apt-get update && apt-get install --no-install-recommends -y \
|
||||
build-essential libmariadb-dev && \
|
||||
RUN apt-get update && apt-get install --no-install-recommends -y build-essential && \
|
||||
apt-get clean && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Download the latest installer, install it and then remove it
|
||||
@@ -42,7 +41,6 @@ WORKDIR /app
|
||||
|
||||
COPY /src src
|
||||
COPY --from=builder /app/.venv .venv
|
||||
COPY --from=builder /usr/lib/x86_64-linux-gnu/libmariadb.so.3 /usr/lib/x86_64-linux-gnu
|
||||
|
||||
# Set up environment variables for production
|
||||
ENV PATH="/app/.venv/bin:$PATH"
|
||||
|
||||
+3
-3
@@ -4,11 +4,11 @@ clean:
|
||||
find . -name '*.py[co]' -delete
|
||||
|
||||
test:
|
||||
DB_HOST=localhost uv run pytest -v --cov --cov-report=term --cov-report=html:coverage-report
|
||||
DB_SERVER=localhost uv run pytest -v --cov --cov-report=term --cov-report=html:coverage-report
|
||||
|
||||
docker: clean
|
||||
docker build --target=production -t kontor-api -t kontor-api:0.1.0 -t kontor-api .
|
||||
docker build --target=production -t kontor-api:0.2.0-SNAPSHOT .
|
||||
|
||||
dev:
|
||||
MARIADB_SERVER=localhost uv run fastapi dev src/main.py --port 8008
|
||||
DB_SERVER=127.0.0.1 uv run fastapi dev src/main.py --port 8008
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ dependencies = [
|
||||
"beautifulsoup4>=4.13.4",
|
||||
"fastapi[standard]>=0.115.12",
|
||||
"httpx==0.24.1",
|
||||
"mariadb>=1.1.12",
|
||||
"sqlalchemy>=2.0.40",
|
||||
"platformdirs>=4.3.7",
|
||||
"pathlib>=1.0.1",
|
||||
@@ -22,4 +21,11 @@ dependencies = [
|
||||
"python-jose>=3.4.0",
|
||||
"python-multipart>=0.0.20",
|
||||
"natsort>=8.4.0",
|
||||
"psycopg2-binary>=2.9.10",
|
||||
"pytest-cov>=6.1.1",
|
||||
"databases[sqlite]>=0.9.0",
|
||||
"pydantic[email]>=2.11.3",
|
||||
"jinja2>=3.1.6",
|
||||
"asyncpg>=0.30.0",
|
||||
"bcrypt>=4.3.0",
|
||||
]
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
from src.apis.version1 import comic, media, tysc
|
||||
from src.apis.version1 import comic, media, tysc, admin
|
||||
|
||||
api_router = APIRouter(prefix="/api")
|
||||
api_router.include_router(comic.router, prefix="/comics", tags=["comics"])
|
||||
api_router.include_router(media.router, prefix="/media", tags=["media"])
|
||||
api_router.include_router(tysc.router, prefix="/tysc", tags=["tysc"])
|
||||
api_router.include_router(admin.router, prefix="/login", tags=["login"])
|
||||
|
||||
@@ -1,4 +1,13 @@
|
||||
from typing import Annotated
|
||||
from typing import Dict
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import HTTPException
|
||||
from fastapi import Request
|
||||
from fastapi import status
|
||||
from fastapi.openapi.models import OAuthFlows as OAuthFlowsModel
|
||||
from fastapi.security import OAuth2
|
||||
from fastapi.security.utils import get_authorization_scheme_param
|
||||
|
||||
from fastapi import Depends
|
||||
from sqlalchemy.orm import Session
|
||||
@@ -6,3 +15,34 @@ from sqlalchemy.orm import Session
|
||||
from src.db.session import get_db
|
||||
|
||||
SessionDep = Annotated[Session, Depends(get_db)]
|
||||
|
||||
|
||||
class OAuth2PasswordBearerWithCookie(OAuth2):
|
||||
def __init__(
|
||||
self,
|
||||
tokenUrl: str,
|
||||
scheme_name: Optional[str] = None,
|
||||
scopes: Optional[Dict[str, str]] = None,
|
||||
auto_error: bool = True,
|
||||
):
|
||||
if not scopes:
|
||||
scopes = {}
|
||||
flows = OAuthFlowsModel(password={"tokenUrl": tokenUrl, "scopes": scopes})
|
||||
super().__init__(flows=flows, scheme_name=scheme_name, auto_error=auto_error)
|
||||
|
||||
async def __call__(self, request: Request) -> Optional[str]:
|
||||
authorization: str = request.cookies.get(
|
||||
"access_token"
|
||||
) # changed to accept access token from httpOnly Cookie
|
||||
|
||||
scheme, param = get_authorization_scheme_param(authorization)
|
||||
if not authorization or scheme.lower() != "bearer":
|
||||
if self.auto_error:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Not authenticated",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
else:
|
||||
return None
|
||||
return param
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
import bcrypt
|
||||
from fastapi import APIRouter, HTTPException, status, Response, Depends
|
||||
from fastapi.security import OAuth2PasswordRequestForm
|
||||
from jose import jwt, JWTError
|
||||
from src.apis.utils import SessionDep, OAuth2PasswordBearerWithCookie
|
||||
from src.core.config import settings
|
||||
from src.core.security import create_access_token
|
||||
from src.db.models.admin import Profile
|
||||
from src.db.repository.admin import get_profile
|
||||
from src.schema.admin import Token
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
def authenticate_user(username: str, password: str, db: SessionDep) -> Profile | None:
|
||||
user = get_profile(username=username, db=db)
|
||||
print(user)
|
||||
if not user:
|
||||
return None
|
||||
if bcrypt.checkpw(password.encode(), user.password.encode()):
|
||||
print("User successful authenticated")
|
||||
else:
|
||||
logging.info("Authentication failed!")
|
||||
return user
|
||||
|
||||
|
||||
@router.post("/token", response_model=Token)
|
||||
def login_for_access_token(response: Response, db: SessionDep, form_data: OAuth2PasswordRequestForm = Depends()):
|
||||
user = authenticate_user(form_data.username, form_data.password, db)
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Incorrect username or password",
|
||||
)
|
||||
access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
|
||||
access_token = create_access_token(
|
||||
data={"sub": user.email}, expires_delta=access_token_expires
|
||||
)
|
||||
response.set_cookie(
|
||||
key="access_token", value=f"Bearer {access_token}", httponly=True
|
||||
)
|
||||
return {"access_token": access_token, "token_type": "bearer"}
|
||||
|
||||
|
||||
oauth2_scheme = OAuth2PasswordBearerWithCookie(tokenUrl="/api/login/token")
|
||||
|
||||
|
||||
def get_current_user_from_token(db: SessionDep, token: str = Depends(oauth2_scheme)):
|
||||
credentials_exception = HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Could not validate credentials",
|
||||
)
|
||||
try:
|
||||
payload = jwt.decode(
|
||||
token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM]
|
||||
)
|
||||
username: str = payload.get("sub")
|
||||
print("username/email extracted is ", username)
|
||||
if username is None:
|
||||
raise credentials_exception
|
||||
except JWTError:
|
||||
raise credentials_exception
|
||||
user = get_profile(username=username, db=db)
|
||||
if user is None:
|
||||
raise credentials_exception
|
||||
return user
|
||||
@@ -1,31 +1,28 @@
|
||||
from uuid import UUID
|
||||
from typing import List
|
||||
from typing import List, AnyStr
|
||||
from fastapi import APIRouter, HTTPException, status
|
||||
from sqlalchemy import select
|
||||
|
||||
from src.apis.utils import SessionDep
|
||||
from src.db.repository.comics.artist import get_artist_details
|
||||
from src.db.repository.comics.comic import list_comics, get_issue_details
|
||||
from src.schema.comics.comic import ComicResponse, ComicDetailsResponse, get_comic_details, get_short_info
|
||||
from src.schema.comics.artist import ArtistCreation, ArtistDetailResponse, ArtistResponse, get_artist_details
|
||||
from src.db.models.comic import Comic, Artist
|
||||
from src.schema.comics.artist import ArtistCreation, ArtistDetailResponse, ArtistResponse
|
||||
from src.db.models.comic import Comic, Artist, Issue
|
||||
from src.schema.comics.issue import IssueDetailsResponse
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/comic",
|
||||
tags=["comics"],
|
||||
responses={404: {"description": "Not found"}},
|
||||
)
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/comics")
|
||||
def get_all_comics(db: SessionDep) -> List[ComicResponse]:
|
||||
results: List[ComicResponse] = []
|
||||
comics = db.scalars(select(Comic)).all()
|
||||
comics = list_comics(db)
|
||||
for comic in comics:
|
||||
response = get_short_info(comic)
|
||||
results.append(response)
|
||||
return results
|
||||
|
||||
@router.get("/comics/{comic_id}", response_model=ComicDetailsResponse)
|
||||
def get_comic(comic_id: UUID, db: SessionDep) -> ComicDetailsResponse:
|
||||
def get_comic(comic_id: AnyStr, db: SessionDep) -> ComicDetailsResponse:
|
||||
comic = db.get(Comic, comic_id)
|
||||
if comic is None:
|
||||
raise HTTPException(status_code=404, detail="Comic could not be found")
|
||||
@@ -41,7 +38,7 @@ def get_all_artists(db: SessionDep) -> List[ArtistResponse]:
|
||||
return results
|
||||
|
||||
@router.get("/artists/{artist_id}", response_model=ArtistDetailResponse)
|
||||
def get_artist(artist_id: UUID, db: SessionDep) -> ArtistDetailResponse:
|
||||
def get_artist(artist_id: AnyStr, db: SessionDep) -> ArtistDetailResponse:
|
||||
artist = db.get(Artist, artist_id)
|
||||
if artist is None:
|
||||
raise HTTPException(status_code=404, detail="Artist could not be found")
|
||||
@@ -60,3 +57,11 @@ def add_artist(db: SessionDep, artist_creation: ArtistCreation) -> ArtistRespons
|
||||
response = ArtistResponse(id=artist.id, name=artist.name)
|
||||
return response
|
||||
|
||||
@router.get("/issues", response_model=List[IssueDetailsResponse])
|
||||
def get_issues(db: SessionDep) -> List[IssueDetailsResponse]:
|
||||
results: List[IssueDetailsResponse] = []
|
||||
issues = db.query(Issue).all()
|
||||
for issue in issues:
|
||||
results.append(get_issue_details(issue))
|
||||
return results
|
||||
|
||||
|
||||
@@ -1,22 +1,19 @@
|
||||
from typing import List
|
||||
from uuid import UUID
|
||||
from typing import List, AnyStr
|
||||
|
||||
from fastapi import APIRouter, status, HTTPException
|
||||
from fastapi import APIRouter, status, HTTPException, Depends
|
||||
from sqlalchemy import select, Sequence
|
||||
|
||||
from src.core.log_conf import logger
|
||||
from src.apis.utils import SessionDep
|
||||
from src.db.repository.media import create_new_mediafile
|
||||
from src.schema.media.file import MediaFileResponse, Link, get_file_details, set_file
|
||||
from src.db.models.media import MediaFile
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/media",
|
||||
tags=["media"]
|
||||
)
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/update-titles")
|
||||
def update_titles(db: SessionDep) -> list[MediaFileResponse]:
|
||||
results: list[MediaFileResponse] = []
|
||||
files = db.query(MediaFile).filter(MediaFile.review == 1).all()
|
||||
files = db.query(MediaFile).filter(MediaFile.review == True).all()
|
||||
for mediafile in files:
|
||||
mediafile.update_title()
|
||||
db.add(mediafile)
|
||||
@@ -27,13 +24,14 @@ def update_titles(db: SessionDep) -> list[MediaFileResponse]:
|
||||
|
||||
|
||||
@router.get("/files", response_model=List[MediaFileResponse])
|
||||
#def get_all_files(db: SessionDep, review: bool = False, download: bool = False, current_user: Profile = Depends(get_current_user_from_token)) -> List[MediaFileResponse]:
|
||||
def get_all_files(db: SessionDep, review: bool = False, download: bool = False) -> List[MediaFileResponse]:
|
||||
results: list[MediaFileResponse] = []
|
||||
files: Sequence[MediaFile]
|
||||
if review:
|
||||
files = db.query(MediaFile).filter(MediaFile.review == 1).all()
|
||||
files = db.query(MediaFile).filter(MediaFile.review == True).all()
|
||||
elif download:
|
||||
files = db.query(MediaFile).filter(MediaFile.should_download == 1).all()
|
||||
files = db.query(MediaFile).filter(MediaFile.should_download == True).all()
|
||||
else:
|
||||
files = db.scalars(select(MediaFile)).all()
|
||||
for mediafile in files:
|
||||
@@ -42,7 +40,7 @@ def get_all_files(db: SessionDep, review: bool = False, download: bool = False)
|
||||
return results
|
||||
|
||||
@router.get("/files/{file_id}", response_model=MediaFileResponse)
|
||||
def get_file(file_id: UUID, db: SessionDep) -> MediaFileResponse:
|
||||
def get_file(file_id: AnyStr, db: SessionDep) -> MediaFileResponse:
|
||||
mediafile = db.get(MediaFile, file_id)
|
||||
if not mediafile:
|
||||
raise HTTPException(status_code=404, detail="MediaFile could not be found")
|
||||
@@ -50,7 +48,7 @@ def get_file(file_id: UUID, db: SessionDep) -> MediaFileResponse:
|
||||
return response
|
||||
|
||||
@router.put("/files/{file_id}", response_model=MediaFileResponse)
|
||||
def update_file(file_id: UUID, db: SessionDep, info: MediaFileResponse) -> MediaFileResponse:
|
||||
def update_file(file_id: AnyStr, db: SessionDep, info: MediaFileResponse) -> MediaFileResponse:
|
||||
mediaFile = db.get(MediaFile, file_id)
|
||||
if not mediaFile:
|
||||
raise HTTPException(status_code=404, detail="MediaFile could not be found")
|
||||
@@ -62,14 +60,9 @@ def update_file(file_id: UUID, db: SessionDep, info: MediaFileResponse) -> Media
|
||||
|
||||
@router.post("/files", status_code=status.HTTP_201_CREATED)
|
||||
def add_file(new_link: Link, db: SessionDep) -> MediaFileResponse:
|
||||
print(new_link.url)
|
||||
logger.info(f"add url {new_link.url}")
|
||||
try:
|
||||
mediaFile: MediaFile = MediaFile()
|
||||
setattr(mediaFile, "url", new_link.url)
|
||||
setattr(mediaFile, "review", 1)
|
||||
setattr(mediaFile, "should_download", 1)
|
||||
db.add(mediaFile)
|
||||
db.commit()
|
||||
mediaFile: MediaFile = create_new_mediafile(new_link.url, db)
|
||||
except:
|
||||
raise HTTPException(status_code=409, detail="Link duplicate")
|
||||
response = get_file_details(mediaFile)
|
||||
|
||||
@@ -5,11 +5,7 @@ from src.apis.utils import SessionDep
|
||||
from src.schema.tysc.sport import SportResponse
|
||||
from src.db.models.tysc import Sport
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/tysc",
|
||||
tags=["tysc"],
|
||||
responses={404: {"description": "Not found"}},
|
||||
)
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/sports")
|
||||
def get_all_sports(db: SessionDep) -> List[SportResponse]:
|
||||
|
||||
@@ -9,15 +9,17 @@ load_dotenv(dotenv_path=env_path)
|
||||
|
||||
class Settings:
|
||||
PROJECT_NAME: str = "Kontor"
|
||||
PROJECT_VERSION: str = "0.1.0"
|
||||
|
||||
MARIADB_USER: str = os.getenv("MARIADB_USER", "kontor")
|
||||
MARIADB_PASSWORD: str = os.getenv("MARIADB_PASSWORD", "kontor")
|
||||
MARIADB_SERVER: str = os.getenv("MARIADB_SERVER", "mariadb")
|
||||
MARIADB_PORT: str = os.getenv("MARIADB_PORT", 3306)
|
||||
MARIADB_DB: str = os.getenv("MARIADB_DB", "kontor")
|
||||
DATABASE_URL: str = f"mariadb+mariadbconnector://{MARIADB_USER}:{MARIADB_PASSWORD}@{MARIADB_SERVER}:{MARIADB_PORT}/{MARIADB_DB}"
|
||||
PROJECT_VERSION: str = "0.2.0"
|
||||
|
||||
DB_USER: str = os.getenv("DB_USER", "kontor")
|
||||
DB_PASSWORD: str = os.getenv("DB_PASSWORD", "kontor")
|
||||
DB_SERVER: str = os.getenv("DB_SERVER", "postgres")
|
||||
DB_PORT: str = os.getenv("DB_PORT", 5432)
|
||||
DB_DBNAME: str = os.getenv("DB_DBNAME", "kontor")
|
||||
DATABASE_URL: str = f"postgresql://{DB_USER}:{DB_PASSWORD}@{DB_SERVER}:{DB_PORT}/{DB_DBNAME}"
|
||||
SECRET_KEY: str = os.getenv("SECRET_KEY", "J6GOtcwC2NJI1l0VkHu20PacPFGTxpirBxWwynoHjsc=")
|
||||
ALGORITHM = "HS256"
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES = 600 # in mins
|
||||
|
||||
settings = Settings()
|
||||
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import logging
|
||||
import logging.config
|
||||
from typing import Any
|
||||
|
||||
LOGGING_CONFIG: dict[str, Any] = {
|
||||
"version": 1,
|
||||
"disable_existing_loggers": False,
|
||||
"formatters": {
|
||||
"default": {
|
||||
"()": "uvicorn.logging.DefaultFormatter",
|
||||
"fmt": "%(asctime)s - %(name)s - %(levelprefix)s %(message)s"
|
||||
},
|
||||
"access": {
|
||||
"()": "uvicorn.logging.AccessFormatter",
|
||||
"fmt": '%(asctime)s - %(name)s - %(levelprefix)s %(client_addr)s - "%(request_line)s" %(status_code)s', # noqa: E501
|
||||
},
|
||||
"access_file": {
|
||||
"()": "uvicorn.logging.AccessFormatter",
|
||||
"fmt": '%(asctime)s - %(name)s - %(levelprefix)s %(client_addr)s - "%(request_line)s" %(status_code)s', # noqa: E501
|
||||
"use_colors": False,
|
||||
},
|
||||
},
|
||||
"handlers": {
|
||||
"default": {
|
||||
"formatter": "default",
|
||||
"class": "logging.StreamHandler",
|
||||
"stream": "ext://sys.stdout",
|
||||
},
|
||||
"error": {
|
||||
"formatter": "access",
|
||||
"class": "logging.StreamHandler",
|
||||
"stream": "ext://sys.stderr",
|
||||
},
|
||||
},
|
||||
"loggers": {
|
||||
"root": {"handlers": ["default"], "level": "INFO", "propagate": False},
|
||||
"uvicorn": {"handlers": ["default"], "level": "INFO", "propagate": False},
|
||||
"uvicorn.error": {"level": "INFO"},
|
||||
"uvicorn.access": {"handlers": ["default"], "level": "INFO", "propagate": False},
|
||||
},
|
||||
}
|
||||
|
||||
logging.config.dictConfig(LOGGING_CONFIG)
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -0,0 +1,21 @@
|
||||
from datetime import datetime
|
||||
from datetime import timedelta
|
||||
from typing import Optional
|
||||
|
||||
from src.core.config import settings
|
||||
from jose import jwt
|
||||
|
||||
|
||||
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
|
||||
to_encode = data.copy()
|
||||
if expires_delta:
|
||||
expire = datetime.utcnow() + expires_delta
|
||||
else:
|
||||
expire = datetime.utcnow() + timedelta(
|
||||
minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES
|
||||
)
|
||||
to_encode.update({"exp": expire})
|
||||
encoded_jwt = jwt.encode(
|
||||
to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM
|
||||
)
|
||||
return encoded_jwt
|
||||
@@ -1,7 +1,6 @@
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import Column, ForeignKey, Integer, String
|
||||
from sqlalchemy.dialects.mysql import BIT
|
||||
from sqlalchemy import Column, ForeignKey, Integer, String, Boolean
|
||||
from sqlalchemy.orm import relationship, mapped_column, Mapped
|
||||
|
||||
from src.db.models.base import Base, BaseMixin
|
||||
@@ -9,12 +8,12 @@ from src.db.models.base import Base, BaseMixin
|
||||
|
||||
class Profile(Base, BaseMixin):
|
||||
__tablename__ = 'profile'
|
||||
first_name = Column(String(255))
|
||||
last_name = Column(String(255))
|
||||
user_name = Column(String(255), nullable=False)
|
||||
email = Column(String(255))
|
||||
password = Column(String(255))
|
||||
enabled = Column(BIT(1))
|
||||
first_name = Column(String)
|
||||
last_name = Column(String)
|
||||
user_name = Column(String, nullable=False)
|
||||
email = Column(String)
|
||||
password = Column(String)
|
||||
enabled = Column(Boolean)
|
||||
assignments = relationship("Assignment")
|
||||
tokens = relationship("Token")
|
||||
|
||||
@@ -28,20 +27,23 @@ class Profile(Base, BaseMixin):
|
||||
full_name += self.last_name
|
||||
return full_name
|
||||
|
||||
def __str__(self):
|
||||
return f"Profile({self.id} {self.user_name}, {self.email})"
|
||||
|
||||
|
||||
class Token(Base, BaseMixin):
|
||||
__tablename__ = "token"
|
||||
token = Column(String(255), nullable=False, unique=True)
|
||||
name = Column(String(255))
|
||||
token = Column(String, nullable=False, unique=True)
|
||||
name = Column(String)
|
||||
last_used_date: Mapped[datetime] = mapped_column()
|
||||
enabled = Column(BIT(1))
|
||||
profile_id = Column(String(255), ForeignKey("profile.id"), nullable=False)
|
||||
enabled = Column(Boolean)
|
||||
profile_id = Column(String, ForeignKey("profile.id"), nullable=False)
|
||||
profile = relationship("Profile", back_populates="tokens")
|
||||
|
||||
|
||||
class Permission(Base, BaseMixin):
|
||||
__tablename__ = "permission"
|
||||
name = Column(String(255), nullable=False)
|
||||
name = Column(String, nullable=False)
|
||||
assignments = relationship("Assignment")
|
||||
|
||||
|
||||
@@ -53,20 +55,14 @@ class Assignment(Base, BaseMixin):
|
||||
permission = relationship("Permission", back_populates="assignments")
|
||||
|
||||
|
||||
class ModuleData(Base, BaseMixin):
|
||||
__tablename__ = "module_data"
|
||||
module_name = Column(String(255), nullable=False)
|
||||
import_data = Column(BIT(1))
|
||||
|
||||
|
||||
class MailAccount(Base, BaseMixin):
|
||||
__tablename__ = "mail_account"
|
||||
host = Column(String(255))
|
||||
host = Column(String)
|
||||
port = Column(Integer)
|
||||
protocol = Column(String(255))
|
||||
user_name = Column(String(255))
|
||||
password = Column(String(255))
|
||||
start_tls = Column(BIT(1))
|
||||
protocol = Column(String)
|
||||
user_name = Column(String)
|
||||
password = Column(String)
|
||||
start_tls = Column(Boolean)
|
||||
|
||||
|
||||
class Mail(Base, BaseMixin):
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import func, Column, String
|
||||
from sqlalchemy.dialects.mysql import BIT
|
||||
from sqlalchemy import func, Column, String, Boolean
|
||||
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
|
||||
|
||||
|
||||
@@ -11,8 +10,8 @@ class Base(DeclarativeBase):
|
||||
|
||||
|
||||
class BaseMixin:
|
||||
id = Column(String(255), primary_key=True, default=uuid.uuid4())
|
||||
# id: Mapped[str] = mapped_column(primary_key=True, default=uuid.uuid4())
|
||||
#id = Column(String, primary_key=True, default=uuid.uuid4)
|
||||
id: Mapped[str] = mapped_column(primary_key=True, default=str(uuid.uuid4()))
|
||||
# created_date = Column(DateTime)
|
||||
created_date: Mapped[datetime] = mapped_column(default=func.now())
|
||||
# last_modified_date = Column(DateTime)
|
||||
@@ -22,10 +21,10 @@ class BaseMixin:
|
||||
|
||||
|
||||
class BaseVideoMixin:
|
||||
cloud_link = Column(String(255))
|
||||
file_name = Column(String(255))
|
||||
path = Column(String(255))
|
||||
review = Column(BIT(1))
|
||||
title = Column(String(255))
|
||||
url = Column(String(255), unique=True)
|
||||
should_download = Column(BIT(1))
|
||||
cloud_link = Column(String)
|
||||
file_name = Column(String)
|
||||
path = Column(String)
|
||||
review = Column(Boolean)
|
||||
title = Column(String)
|
||||
url = Column(String, unique=True)
|
||||
should_download = Column(Boolean)
|
||||
|
||||
@@ -6,28 +6,28 @@ from src.db.models.base import Base, BaseMixin
|
||||
|
||||
class Article(Base, BaseMixin):
|
||||
__tablename__ = 'article'
|
||||
title = Column(String(length=255), unique=True)
|
||||
title = Column(String, unique=True)
|
||||
article_authors = relationship("ArticleAuthor")
|
||||
|
||||
|
||||
class Author(Base, BaseMixin):
|
||||
__tablename__ = 'author'
|
||||
first_name = Column(String(255))
|
||||
last_name = Column(String(255))
|
||||
first_name = Column(String)
|
||||
last_name = Column(String)
|
||||
article_authors = relationship("ArticleAuthor")
|
||||
book_authors = relationship("BookAuthor")
|
||||
|
||||
|
||||
class BookshelfPublisher(Base, BaseMixin):
|
||||
__tablename__ = 'bookshelf_publisher'
|
||||
name = Column(String(length=255), unique=True)
|
||||
name = Column(String, unique=True)
|
||||
books = relationship("Book")
|
||||
|
||||
|
||||
class Book(Base, BaseMixin):
|
||||
__tablename__ = 'book'
|
||||
isbn = Column(String(255), unique=True)
|
||||
title = Column(String(255))
|
||||
isbn = Column(String, unique=True)
|
||||
title = Column(String)
|
||||
year = Column(Integer, nullable=False)
|
||||
publisher_id = Column(String, ForeignKey('bookshelf_publisher.id'), nullable=False)
|
||||
publisher = relationship('BookshelfPublisher', back_populates="books")
|
||||
|
||||
@@ -1,15 +1,24 @@
|
||||
from typing import Dict, List
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from typing import AnyStr, Dict, List, Optional, Any
|
||||
from natsort import natsorted
|
||||
from sqlalchemy import Column, ForeignKey, Integer, String
|
||||
from sqlalchemy.dialects.mysql import BIT
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy import Column, ForeignKey, Integer, String, Boolean, func
|
||||
from sqlalchemy.orm import relationship, Mapped, mapped_column
|
||||
|
||||
from src.db.models.base import Base, BaseMixin
|
||||
|
||||
|
||||
class Publisher(Base, BaseMixin):
|
||||
class Publisher(Base):
|
||||
__tablename__ = "publisher"
|
||||
name = Column(String(length=255), unique=True)
|
||||
id: Mapped[str] = mapped_column(primary_key=True, default=uuid.uuid4)
|
||||
created_date: Mapped[datetime] = mapped_column(default=func.now())
|
||||
last_modified_date: Mapped[datetime] = mapped_column(default=func.now())
|
||||
version: Mapped[int] = mapped_column(default=0)
|
||||
name = Column(String, unique=True)
|
||||
weblink = Column(String, nullable=True)
|
||||
parent_publisher_id: Mapped[Optional[str]] = mapped_column(ForeignKey('publisher.id'))
|
||||
parent_publisher: Mapped[Optional['Publisher']] = relationship("Publisher", back_populates="imprints", remote_side=[id])
|
||||
imprints: Mapped[List['Publisher']] = relationship('Publisher', back_populates="parent_publisher")
|
||||
comics = relationship("Comic")
|
||||
|
||||
def __repr__(self):
|
||||
@@ -21,11 +30,12 @@ class Publisher(Base, BaseMixin):
|
||||
|
||||
class Comic(Base, BaseMixin):
|
||||
__tablename__ = 'comic'
|
||||
title = Column(String(length=255), unique=True)
|
||||
title = Column(String, unique=True)
|
||||
publisher_id = Column(String, ForeignKey('publisher.id'), nullable=False)
|
||||
publisher = relationship("Publisher", back_populates="comics")
|
||||
current_order = Column(BIT(1))
|
||||
completed = Column(BIT(1))
|
||||
current_order = Column(Boolean)
|
||||
completed = Column(Boolean)
|
||||
weblink = Column(String, nullable=True)
|
||||
issues = relationship("Issue", order_by="Issue.issue_number")
|
||||
story_arcs = relationship("StoryArc")
|
||||
trade_paperbacks = relationship("TradePaperback")
|
||||
@@ -38,10 +48,10 @@ class Comic(Base, BaseMixin):
|
||||
def __str__(self):
|
||||
return f'{self.title}({self.id})'
|
||||
|
||||
def get_artists(self) -> Dict[str, List[str]]:
|
||||
works: Dict[str, List[str]] = {}
|
||||
def get_artists(self) -> Dict[Any, List[Any]]:
|
||||
works: Dict[Any, List[Any]] = {}
|
||||
for work in self.comic_works:
|
||||
work_type = work.work_type.name
|
||||
work_type = work.work_type
|
||||
artist = work.artist
|
||||
if work_type in works:
|
||||
works[work_type].append(artist)
|
||||
@@ -56,15 +66,16 @@ class Comic(Base, BaseMixin):
|
||||
|
||||
class Volume(Base, BaseMixin):
|
||||
__tablename__ = "volume"
|
||||
name = Column(String(length=255), nullable=False)
|
||||
name = Column(String, nullable=False)
|
||||
comic_id = Column(String, ForeignKey("comic.id"), nullable=False)
|
||||
comic = relationship("Comic", back_populates="volumes")
|
||||
story_arcs = relationship("StoryArc")
|
||||
issues = relationship("Issue")
|
||||
|
||||
|
||||
class TradePaperback(Base, BaseMixin):
|
||||
__tablename__ = "trade_paperback"
|
||||
name = Column(String(length=255), nullable=False)
|
||||
name = Column(String, nullable=False)
|
||||
issue_start = Column(Integer)
|
||||
issue_end = Column(Integer)
|
||||
comic_id = Column(String, ForeignKey("comic.id"), nullable=False)
|
||||
@@ -73,31 +84,58 @@ class TradePaperback(Base, BaseMixin):
|
||||
|
||||
class StoryArc(Base, BaseMixin):
|
||||
__tablename__ = "story_arc"
|
||||
name = Column(String(length=255), nullable=False)
|
||||
name = Column(String, nullable=False)
|
||||
comic_id = Column(String, ForeignKey("comic.id"), nullable=False)
|
||||
comic = relationship("Comic", back_populates="story_arcs")
|
||||
volume_id = Column(String, ForeignKey("volume.id"), nullable=True)
|
||||
volume = relationship("Volume", back_populates="story_arcs")
|
||||
issues = relationship("Issue")
|
||||
|
||||
|
||||
class Issue(Base, BaseMixin):
|
||||
__tablename__ = "issue"
|
||||
issue_number = Column(String(255))
|
||||
in_stock = Column(BIT(1))
|
||||
is_read = Column(BIT(1))
|
||||
issue_number = Column(String)
|
||||
title = Column(String, nullable=True)
|
||||
published_on: Mapped[datetime] = mapped_column(nullable=True)
|
||||
in_stock = Column(Boolean)
|
||||
is_read = Column(Boolean)
|
||||
comic_id = Column(String, ForeignKey("comic.id"), nullable=False)
|
||||
comic = relationship("Comic", back_populates="issues")
|
||||
volume_id = Column(String, ForeignKey("volume.id"), nullable=True)
|
||||
volume = relationship("Volume", back_populates="issues")
|
||||
story_arc_id = Column(String, ForeignKey("story_arc.id"), nullable=True)
|
||||
story_arc = relationship("StoryArc", back_populates="issues")
|
||||
issue_works = relationship("IssueWork")
|
||||
|
||||
def get_full_title(self) -> AnyStr:
|
||||
full_title: AnyStr = self.issue_number
|
||||
if self.title:
|
||||
full_title += ": " + self.title
|
||||
return full_title
|
||||
|
||||
def get_artists(self) -> Dict[Any, List[Any]]:
|
||||
works: Dict[Any, List[Any]] = {}
|
||||
for work in self.issue_works:
|
||||
work_type = work.work_type
|
||||
artist = work.artist
|
||||
if work_type in works:
|
||||
works[work_type].append(artist)
|
||||
else:
|
||||
works[work_type] = [artist]
|
||||
return works
|
||||
|
||||
|
||||
class Artist(Base, BaseMixin):
|
||||
__tablename__ = "artist"
|
||||
name = Column(String(length=255), nullable=False)
|
||||
name = Column(String, nullable=False)
|
||||
weblink = Column(String, nullable=True)
|
||||
comic_works = relationship("ComicWork")
|
||||
issue_works = relationship("IssueWork")
|
||||
|
||||
def get_comics(self) -> Dict[str, List[str]]:
|
||||
works: Dict[str, List[str]] = {}
|
||||
def get_comics(self) -> Dict[Any, List[Comic]]:
|
||||
works: Dict[Any, List[Comic]] = {}
|
||||
for work in self.comic_works:
|
||||
work_type = work.work_type.name
|
||||
work_type = work.work_type
|
||||
comic = work.comic
|
||||
if work_type in works:
|
||||
works[work_type].append(comic)
|
||||
@@ -108,8 +146,20 @@ class Artist(Base, BaseMixin):
|
||||
|
||||
class WorkType(Base, BaseMixin):
|
||||
__tablename__ = "worktype"
|
||||
name = Column(String(length=255), nullable=False, unique=True)
|
||||
name = Column(String, nullable=False, unique=True)
|
||||
comic_works = relationship("ComicWork")
|
||||
issue_works = relationship("IssueWork")
|
||||
|
||||
def get_artists(self) -> Dict[str, List[str]]:
|
||||
works: Dict[str, List[str]] = {}
|
||||
for work in self.comic_works:
|
||||
comic = work.comic.title
|
||||
artist = work.artist
|
||||
if comic in works:
|
||||
works[comic].append(artist)
|
||||
else:
|
||||
works[comic] = [artist]
|
||||
return works
|
||||
|
||||
def __repr__(self):
|
||||
return f'Worktype({self.id} {self.version} {self.name} {len(self.comic_works)})'
|
||||
@@ -126,3 +176,14 @@ class ComicWork(Base, BaseMixin):
|
||||
artist = relationship("Artist", back_populates="comic_works")
|
||||
work_type_id = Column(String, ForeignKey("worktype.id"), nullable=False)
|
||||
work_type = relationship("WorkType", back_populates="comic_works")
|
||||
|
||||
|
||||
class IssueWork(Base, BaseMixin):
|
||||
__tablename__ = "issue_work"
|
||||
issue_id = Column(String, ForeignKey("issue.id"), nullable=False)
|
||||
issue = relationship("Issue", back_populates="issue_works")
|
||||
artist_id = Column(String, ForeignKey("artist.id"), nullable=False)
|
||||
artist = relationship("Artist", back_populates="issue_works")
|
||||
work_type_id = Column(String, ForeignKey("worktype.id"), nullable=False)
|
||||
work_type = relationship("WorkType", back_populates="issue_works")
|
||||
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import json
|
||||
import logging
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from enum import Enum, auto
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from typing import Any, List
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
@@ -13,7 +12,7 @@ from sqlalchemy.orm import sessionmaker
|
||||
from src.db.models.tysc import Card, CardSet, Rooster, Team, FieldPosition, Player, Vendor, Sport
|
||||
from src.db.models.comic import Issue, TradePaperback, StoryArc, Volume, ComicWork, Artist, Comic, Publisher, WorkType
|
||||
from src.db.models.bookshelf import ArticleAuthor, BookAuthor, BookshelfPublisher, Article, Book, Author
|
||||
from src.db.models.admin import Mail, MailAccount, ModuleData, Role, User, Token, AuthorizationMatrix
|
||||
from src.db.models.admin import Mail, MailAccount, ModuleData, Token, Assignment, Permission, Profile
|
||||
from src.db.models.metadata import MetaDataTable, MetaDataColumn
|
||||
from src.db.models.media import MediaVideo, MediaArticle, MediaFile, MediaActor, MediaActorFile
|
||||
|
||||
@@ -79,10 +78,10 @@ class KontorDB:
|
||||
self.registry[MediaVideo.__tablename__] = MediaVideo
|
||||
self.registry[MetaDataColumn.__tablename__] = MetaDataColumn
|
||||
self.registry[MetaDataTable.__tablename__] = MetaDataTable
|
||||
self.registry[AuthorizationMatrix.__tablename__] = AuthorizationMatrix
|
||||
self.registry[Assignment.__tablename__] = Assignment
|
||||
self.registry[Token.__tablename__] = Token
|
||||
self.registry[User.__tablename__] = User
|
||||
self.registry[Role.__tablename__] = Role
|
||||
self.registry[Profile.__tablename__] = Profile
|
||||
self.registry[Permission.__tablename__] = Permission
|
||||
self.registry[ModuleData.__tablename__] = ModuleData
|
||||
self.registry[MailAccount.__tablename__] = MailAccount
|
||||
self.registry[Mail.__tablename__] = Mail
|
||||
@@ -360,7 +359,7 @@ class KontorDB:
|
||||
update_list[link.id] = link.title
|
||||
return update_list
|
||||
|
||||
def get_download_list(self) -> list[uuid.UUID]:
|
||||
def get_download_list(self) -> List[str]:
|
||||
download_list = []
|
||||
__session__ = sessionmaker(self.engine)
|
||||
_filter = { 'should_download': True}
|
||||
|
||||
@@ -6,8 +6,7 @@ from pathlib import Path
|
||||
|
||||
import requests
|
||||
from bs4 import BeautifulSoup
|
||||
from sqlalchemy import Column, String, ForeignKey
|
||||
from sqlalchemy.dialects.mysql import BIT
|
||||
from sqlalchemy import Column, String, ForeignKey, Boolean
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
from src.db.models.base import Base, BaseMixin, BaseVideoMixin
|
||||
@@ -30,10 +29,10 @@ class MediaFile(Base, BaseMixin, BaseVideoMixin):
|
||||
soup = BeautifulSoup(r.content, "html.parser")
|
||||
title = soup.title.string
|
||||
self.title = title
|
||||
self.review = 0
|
||||
self.review = False
|
||||
except:
|
||||
self.title = None
|
||||
self.review = 1
|
||||
self.review = True
|
||||
self.last_modified_date = datetime.now()
|
||||
|
||||
def download_file(self, download_dir: str, dl_tool: str):
|
||||
@@ -45,12 +44,12 @@ class MediaFile(Base, BaseMixin, BaseVideoMixin):
|
||||
lines_list = output.splitlines()
|
||||
file_name = self.__parse_output__(lines_list)
|
||||
if file_name is None:
|
||||
self.review = 1
|
||||
self.should_download = 1
|
||||
self.review = True
|
||||
self.should_download = True
|
||||
self.file_name = None
|
||||
else:
|
||||
download_file = Path(file_name)
|
||||
self.should_download = 0
|
||||
self.should_download = False
|
||||
self.file_name = download_file.name
|
||||
self.cloud_link = str(download_file.absolute())
|
||||
self.last_modified_date = datetime.now()
|
||||
@@ -71,31 +70,42 @@ class MediaFile(Base, BaseMixin, BaseVideoMixin):
|
||||
|
||||
class MediaActor(Base, BaseMixin):
|
||||
__tablename__ = 'media_actor'
|
||||
name = Column(String(255))
|
||||
name = Column(String)
|
||||
media_actor_files = relationship("MediaActorFile")
|
||||
|
||||
|
||||
class MediaActorFile(Base, BaseMixin):
|
||||
__tablename__ = 'media_actor_file'
|
||||
media_actor_id = Column(String(255), ForeignKey("media_actor.id"), nullable=False)
|
||||
media_actor_id = Column(String, ForeignKey("media_actor.id"), nullable=False)
|
||||
media_actor = relationship("MediaActor", back_populates="media_actor_files")
|
||||
media_file_id = Column(String(255), ForeignKey("media_file.id"), nullable=True)
|
||||
media_file_id = Column(String, ForeignKey("media_file.id"), nullable=True)
|
||||
media_file = relationship("MediaFile", back_populates="media_actor_files")
|
||||
|
||||
|
||||
class MediaArticle(Base, BaseMixin):
|
||||
__tablename__ = 'media_article'
|
||||
review = Column(BIT(1))
|
||||
title = Column(String(255))
|
||||
url = Column(String(255), unique=True)
|
||||
review = Column(Boolean)
|
||||
title = Column(String)
|
||||
url = Column(String, unique=True)
|
||||
|
||||
|
||||
class MediaVideo(Base, BaseMixin):
|
||||
__tablename__ = 'media_video'
|
||||
cloud_link = Column(String(255))
|
||||
file_name = Column(String(255))
|
||||
path = Column(String(255))
|
||||
review = Column(BIT(1))
|
||||
title = Column(String(255))
|
||||
url = Column(String(255), unique=True)
|
||||
should_download = Column(BIT(1))
|
||||
cloud_link = Column(String)
|
||||
file_name = Column(String)
|
||||
path = Column(String)
|
||||
review = Column(Boolean)
|
||||
title = Column(String)
|
||||
url = Column(String, unique=True)
|
||||
should_download = Column(Boolean)
|
||||
|
||||
def __repr__(self):
|
||||
return f'MediaFile({self.id} {self.title} {self.url})'
|
||||
|
||||
def __str__(self):
|
||||
if self.title is None:
|
||||
return f'{self.url}({self.id})'
|
||||
else:
|
||||
return f'{self.title}({self.id})'
|
||||
|
||||
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
from sqlalchemy import Column, String, ForeignKey, Integer
|
||||
from sqlalchemy.dialects.mysql import BIT
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
from src.db.models.base import Base, BaseMixin
|
||||
|
||||
|
||||
class MetaDataTable(Base, BaseMixin):
|
||||
__tablename__ = 'meta_data_table'
|
||||
table_name = Column(String(255), unique=True)
|
||||
table_columns = relationship("MetaDataColumn")
|
||||
|
||||
def __repr__(self):
|
||||
return f'MetaDataTable({self.id} {self.table_name})'
|
||||
|
||||
def __str__(self):
|
||||
return f'{self.table_name}({self.id})'
|
||||
|
||||
|
||||
class MetaDataColumn(Base, BaseMixin):
|
||||
__tablename__ = 'meta_data_column'
|
||||
column_name = Column(String(255), nullable=False)
|
||||
column_sync_name = Column(String(255))
|
||||
column_type = Column(String(255))
|
||||
column_modifier = Column(String(255), nullable=True)
|
||||
column_order = Column(Integer)
|
||||
table_id = Column(String, ForeignKey('meta_data_table.id'))
|
||||
table = relationship("MetaDataTable", back_populates="table_columns")
|
||||
column_label = Column(String(255))
|
||||
filter_label = Column(String(255))
|
||||
is_shown = Column(BIT(1))
|
||||
show_filter = Column(BIT(1))
|
||||
ref_column = Column(String, nullable=True)
|
||||
|
||||
def __repr__(self):
|
||||
if self.column_name is None:
|
||||
return f'MetaDataColumn({self.id} {self.table.table_name}.__)'
|
||||
else:
|
||||
return f'MetaDataColumn({self.id} {self.table.table_name}.{self.column_name})'
|
||||
|
||||
def __str__(self):
|
||||
return f'{self.column_name}({self.id})'
|
||||
@@ -1,5 +1,4 @@
|
||||
from sqlalchemy import Column, Integer, String, ForeignKey, UniqueConstraint
|
||||
from sqlalchemy.dialects.mysql import BIT
|
||||
from sqlalchemy import Column, Integer, String, ForeignKey, UniqueConstraint, Boolean
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
from src.db.models.base import Base, BaseMixin
|
||||
@@ -10,15 +9,15 @@ class Sport(Base, BaseMixin):
|
||||
__table_args__ = (
|
||||
UniqueConstraint("name"),
|
||||
)
|
||||
name = Column(String(255), nullable=False, index=True, unique=True)
|
||||
name = Column(String, nullable=False, index=True, unique=True)
|
||||
teams = relationship("Team")
|
||||
positions = relationship("FieldPosition")
|
||||
|
||||
|
||||
class Team(Base, BaseMixin):
|
||||
__tablename__ = "team"
|
||||
name = Column(String(255), nullable=False, index=True, unique=True)
|
||||
short_name = Column(String(255), nullable=False, )
|
||||
name = Column(String, nullable=False, index=True, unique=True)
|
||||
short_name = Column(String, nullable=False, )
|
||||
sport_id = Column(String, ForeignKey("sport.id"), nullable=False)
|
||||
sport = relationship("Sport", back_populates="teams")
|
||||
roosters = relationship("Rooster")
|
||||
@@ -30,8 +29,8 @@ class FieldPosition(Base, BaseMixin):
|
||||
UniqueConstraint("name", "sport_id"),
|
||||
UniqueConstraint("short_name", "sport_id"),
|
||||
)
|
||||
name = Column(String(255), nullable=False, index=True)
|
||||
short_name = Column(String(255), nullable=False)
|
||||
name = Column(String, nullable=False, index=True)
|
||||
short_name = Column(String, nullable=False)
|
||||
sport_id = Column(String, ForeignKey("sport.id"), nullable=False, index=True)
|
||||
sport = relationship("Sport", back_populates="positions")
|
||||
roosters = relationship("Rooster")
|
||||
@@ -42,8 +41,8 @@ class Player(Base, BaseMixin):
|
||||
__table_args__ = (
|
||||
UniqueConstraint("first_name", "last_name"),
|
||||
)
|
||||
first_name = Column(String(255), nullable=False, index=True)
|
||||
last_name = Column(String(255), nullable=False, index=True)
|
||||
first_name = Column(String, nullable=False, index=True)
|
||||
last_name = Column(String, nullable=False, index=True)
|
||||
roosters = relationship("Rooster")
|
||||
|
||||
def get_full_name(self) -> str:
|
||||
@@ -67,7 +66,7 @@ class Rooster(Base, BaseMixin):
|
||||
|
||||
class Vendor(Base, BaseMixin):
|
||||
__tablename__ = "vendor"
|
||||
name = Column(String(255), nullable=False, unique=True, index=True)
|
||||
name = Column(String, nullable=False, unique=True, index=True)
|
||||
card_sets = relationship("CardSet")
|
||||
cards = relationship("Card")
|
||||
|
||||
@@ -77,9 +76,9 @@ class CardSet(Base, BaseMixin):
|
||||
__table_args__ = (
|
||||
UniqueConstraint("name", "vendor_id"),
|
||||
)
|
||||
name = Column(String(255), index=True)
|
||||
parallel_set = Column(BIT(1))
|
||||
insert_set = Column(BIT(1))
|
||||
name = Column(String, index=True)
|
||||
parallel_set = Column(Boolean)
|
||||
insert_set = Column(Boolean)
|
||||
vendor_id = Column(String, ForeignKey("vendor.id"), nullable=False, index=True)
|
||||
vendor = relationship("Vendor", back_populates="card_sets")
|
||||
cards = relationship("Card")
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
from typing import AnyStr
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from src.db.models.admin import Profile
|
||||
|
||||
|
||||
def get_profile(username: AnyStr, db: Session):
|
||||
profile = db.query(Profile).filter(Profile.email == username).first()
|
||||
return profile
|
||||
@@ -0,0 +1,19 @@
|
||||
from src.db.models.comic import Artist
|
||||
from src.schema.comics.artist import ArtistDetailResponse
|
||||
|
||||
|
||||
def get_artist_details(artist: Artist) -> ArtistDetailResponse:
|
||||
works = {}
|
||||
for work in artist.comic_works:
|
||||
work_type = work.work_type.name
|
||||
comic_title = work.comic.title
|
||||
if work_type in works:
|
||||
works[work_type].append(comic_title)
|
||||
else:
|
||||
works[work_type] = [comic_title]
|
||||
response = ArtistDetailResponse(
|
||||
id=artist.id,
|
||||
name=artist.name,
|
||||
works=works
|
||||
)
|
||||
return response
|
||||
@@ -0,0 +1,31 @@
|
||||
from typing import List, Type, AnyStr
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from src.core.log_conf import logger
|
||||
from src.db.models.comic import Comic, Issue
|
||||
from src.schema.comics.comic import ComicSchema
|
||||
from src.schema.comics.issue import IssueDetailsResponse
|
||||
|
||||
|
||||
def list_comics(db: Session) -> List[Type[Comic]]:
|
||||
comics = db.query(Comic).all()
|
||||
return comics
|
||||
|
||||
|
||||
def get_issue_details(issue: Issue) -> IssueDetailsResponse:
|
||||
response = IssueDetailsResponse(
|
||||
id=issue.id,
|
||||
issue_number=issue.issue_number,
|
||||
in_stock=issue.in_stock,
|
||||
is_read=issue.is_read,
|
||||
comic_id=issue.comic_id,
|
||||
volume_id=issue.volume_id
|
||||
)
|
||||
return response
|
||||
|
||||
|
||||
def update_comic(comic: ComicSchema, comic_id: AnyStr, db: Session) -> type[Comic] | None:
|
||||
logger.info(f"update_comic: {comic} with {comic_id}")
|
||||
comic = db.get(Comic, comic_id)
|
||||
return comic
|
||||
@@ -0,0 +1,32 @@
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from typing import AnyStr
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from src.core.log_conf import logger
|
||||
from src.db.models.comic import WorkType
|
||||
from src.schema.comics.worktype import AddWorkType
|
||||
|
||||
|
||||
def create_new_worktype(work: AddWorkType, db: Session) -> WorkType:
|
||||
worktype = WorkType()
|
||||
worktype.id = str(uuid.uuid4())
|
||||
worktype.created_date = datetime.now()
|
||||
worktype.last_modified_date = datetime.now()
|
||||
worktype.name = work.worktype
|
||||
db.add(worktype)
|
||||
db.commit()
|
||||
db.refresh(worktype)
|
||||
logger.info(f"create_new_worktype: {worktype}")
|
||||
return worktype
|
||||
|
||||
|
||||
def update_worktype(work: AddWorkType, worktype_id: AnyStr, db: Session) -> WorkType:
|
||||
logger.info("update worktype")
|
||||
worktype = db.get(WorkType, worktype_id)
|
||||
worktype.name = work.worktype
|
||||
db.add(worktype)
|
||||
db.commit()
|
||||
db.refresh(worktype)
|
||||
return worktype
|
||||
@@ -0,0 +1,40 @@
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import AnyStr
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from src.core.log_conf import logger
|
||||
from src.db.models.media import MediaFile, MediaVideo
|
||||
from src.webapps.media.forms import AddLinkForm
|
||||
|
||||
|
||||
def create_new_video(video: AddLinkForm, db: Session) -> MediaVideo:
|
||||
print(video.url)
|
||||
media_video = MediaVideo()
|
||||
media_video.id = str(uuid.uuid4())
|
||||
media_video.url = video.url
|
||||
media_video.created_date = datetime.now()
|
||||
media_video.last_modified_date = datetime.now()
|
||||
media_video.review = True
|
||||
media_video.should_download = True
|
||||
db.add(media_video)
|
||||
db.commit()
|
||||
db.refresh(media_video)
|
||||
print(media_video)
|
||||
return media_video
|
||||
|
||||
def create_new_mediafile(link: AnyStr, db: Session) -> MediaFile:
|
||||
logger.info("create MediaFile with url {link}")
|
||||
media_file: MediaFile = MediaFile()
|
||||
media_file.id = str(uuid.uuid4())
|
||||
media_file.url = link
|
||||
media_file.created_date = datetime.now()
|
||||
media_file.last_modified_date = datetime.now()
|
||||
media_file.version = 0
|
||||
media_file.review = True
|
||||
media_file.should_download = True
|
||||
db.add(media_file)
|
||||
db.commit()
|
||||
db.refresh(media_file)
|
||||
logger.info(f"created {media_file}")
|
||||
return media_file
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from typing import Generator, Annotated
|
||||
from typing import Generator
|
||||
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import sessionmaker, Session
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
from src.core.config import settings
|
||||
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import databases
|
||||
from src.db.session import SQLALCHEMY_DATABASE_URL
|
||||
|
||||
|
||||
async def check_db_connected():
|
||||
try:
|
||||
if not str(SQLALCHEMY_DATABASE_URL).__contains__("sqlite"):
|
||||
database = databases.Database(SQLALCHEMY_DATABASE_URL)
|
||||
if not database.is_connected:
|
||||
await database.connect()
|
||||
await database.execute("SELECT 1")
|
||||
print("Database is connected (^_^)")
|
||||
except Exception as e:
|
||||
print(
|
||||
"Looks like db is missing or is there is some problem in connection,see below traceback"
|
||||
)
|
||||
raise e
|
||||
|
||||
|
||||
async def check_db_disconnected():
|
||||
try:
|
||||
if not str(SQLALCHEMY_DATABASE_URL).__contains__("sqlite"):
|
||||
database = databases.Database(SQLALCHEMY_DATABASE_URL)
|
||||
if database.is_connected:
|
||||
await database.disconnect()
|
||||
print("Database is Disconnected (-_-) zZZ")
|
||||
except Exception as e:
|
||||
raise e
|
||||
+17
-5
@@ -1,12 +1,25 @@
|
||||
import logging
|
||||
import logging.config
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
|
||||
from src.apis.base import api_router
|
||||
from src.core.log_conf import LOGGING_CONFIG, logger
|
||||
from src.db.session import engine
|
||||
from src.db.utils import check_db_connected, check_db_disconnected
|
||||
from src.webapps.base import api_router as web_app_router
|
||||
from src.core.config import settings
|
||||
from src.db.models.base import Base
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
await check_db_connected()
|
||||
yield
|
||||
await check_db_disconnected()
|
||||
|
||||
def include_router(app: FastAPI):
|
||||
app.include_router(api_router)
|
||||
app.include_router(web_app_router)
|
||||
@@ -17,13 +30,12 @@ def configure_static(app: FastAPI):
|
||||
def create_tables():
|
||||
Base.metadata.create_all(bind=engine)
|
||||
|
||||
def start_application():
|
||||
app = FastAPI(title=settings.PROJECT_NAME, version=settings.PROJECT_VERSION)
|
||||
def start_application(log):
|
||||
log.info(f"using database: {settings.DATABASE_URL}")
|
||||
app = FastAPI(title=settings.PROJECT_NAME, version=settings.PROJECT_VERSION, lifespan=lifespan)
|
||||
include_router(app)
|
||||
configure_static(app)
|
||||
create_tables()
|
||||
return app
|
||||
|
||||
|
||||
kontor = start_application()
|
||||
|
||||
kontor = start_application(logger)
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class Token(BaseModel):
|
||||
access_token: str
|
||||
token_type: str
|
||||
@@ -1,36 +1,19 @@
|
||||
from typing import List, Dict
|
||||
from uuid import UUID
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from src.db.models.comic import Artist
|
||||
|
||||
|
||||
class ArtistCreation(BaseModel):
|
||||
id: str
|
||||
name: str
|
||||
|
||||
class ArtistResponse(BaseModel):
|
||||
id: UUID
|
||||
id: str
|
||||
name: str
|
||||
|
||||
class ArtistDetailResponse(BaseModel):
|
||||
id: UUID
|
||||
id: str
|
||||
name: str
|
||||
weblink: str
|
||||
works: Dict[str, List[str]]
|
||||
|
||||
def get_artist_details(artist: Artist) -> ArtistDetailResponse:
|
||||
works = {}
|
||||
for work in artist.comic_works:
|
||||
work_type = work.work_type.name
|
||||
comic_title = work.comic.title
|
||||
if work_type in works:
|
||||
works[work_type].append(comic_title)
|
||||
else:
|
||||
works[work_type] = [comic_title]
|
||||
response = ArtistDetailResponse(
|
||||
id=artist.id,
|
||||
name=artist.name,
|
||||
works=works
|
||||
)
|
||||
return response
|
||||
|
||||
|
||||
@@ -1,26 +1,35 @@
|
||||
from typing import List, Dict
|
||||
from uuid import UUID
|
||||
from typing import List, Dict, Optional
|
||||
|
||||
from pydantic import BaseModel
|
||||
from pydantic import BaseModel, AnyUrl
|
||||
|
||||
from src.db.models.comic import Comic
|
||||
|
||||
|
||||
class ComicResponse(BaseModel):
|
||||
id: UUID
|
||||
id: str
|
||||
title: str
|
||||
completed: bool
|
||||
|
||||
class ComicDetailsResponse(BaseModel):
|
||||
id: UUID
|
||||
id: str
|
||||
created: str
|
||||
title: str
|
||||
completed : bool
|
||||
current_order : bool
|
||||
weblink: str
|
||||
publisher: str
|
||||
volumes: List[str]
|
||||
works: Dict[str, List[str]]
|
||||
|
||||
|
||||
class ComicSchema(BaseModel):
|
||||
id: str
|
||||
title: str
|
||||
weblink: Optional[AnyUrl]
|
||||
completed: Optional[bool]
|
||||
current_order: Optional[bool]
|
||||
|
||||
|
||||
def get_short_info(comic: Comic) -> ComicResponse:
|
||||
response = ComicResponse(
|
||||
id=comic.id,
|
||||
@@ -46,11 +55,11 @@ def get_comic_details(comic: Comic) -> ComicDetailsResponse | None:
|
||||
id=comic.id,
|
||||
created=str(comic.created_date),
|
||||
title=comic.title,
|
||||
completed=(comic.completed == 1),
|
||||
current_order=(comic.current_order == 1),
|
||||
completed=comic.completed,
|
||||
current_order=comic.current_order,
|
||||
weblink=comic.weblink,
|
||||
publisher=comic.publisher.name,
|
||||
volumes=volumes,
|
||||
works=works
|
||||
)
|
||||
return response
|
||||
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class IssueDetailsResponse(BaseModel):
|
||||
id: str
|
||||
issue_number: str
|
||||
in_stock: bool
|
||||
is_read: bool
|
||||
comic_id: str
|
||||
volume_id: str | None
|
||||
@@ -0,0 +1,4 @@
|
||||
from pydantic import BaseModel
|
||||
|
||||
class AddWorkType(BaseModel):
|
||||
worktype: str
|
||||
@@ -1,12 +1,11 @@
|
||||
from datetime import datetime
|
||||
from uuid import UUID
|
||||
|
||||
from src.db.models.media import MediaFile
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class MediaFileResponse(BaseModel):
|
||||
id: UUID
|
||||
id: str
|
||||
title: str | None = None
|
||||
file_name: str | None = None
|
||||
cloud_link: str | None = None
|
||||
@@ -23,8 +22,8 @@ def get_file_details(mediafile: MediaFile) -> MediaFileResponse | None:
|
||||
file_name=mediafile.file_name,
|
||||
cloud_link=mediafile.cloud_link,
|
||||
url=str(mediafile.url),
|
||||
review=(mediafile.review == 1),
|
||||
should_download=(mediafile.should_download == 1))
|
||||
review=mediafile.review,
|
||||
should_download=mediafile.should_download)
|
||||
#print(f"id: {mediafile.id}: review: {response.review} <- {mediafile.review}")
|
||||
#print(f"id: {mediafile.id}: download: {response.should_download} <- {mediafile.should_download}")
|
||||
return response
|
||||
@@ -35,11 +34,5 @@ def set_file(model: MediaFileResponse, mediafile: MediaFile) -> None:
|
||||
mediafile.url = model.url
|
||||
mediafile.title = model.title
|
||||
mediafile.last_modified_date = datetime.now()
|
||||
if model.review:
|
||||
mediafile.review = 1
|
||||
else:
|
||||
mediafile.review = 0
|
||||
if model.should_download:
|
||||
mediafile.should_download = 1
|
||||
else:
|
||||
mediafile.should_download = 0
|
||||
mediafile.review = model.review
|
||||
mediafile.should_download = model.should_download
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class AddLink(BaseModel):
|
||||
url: str
|
||||
@@ -1,8 +1,8 @@
|
||||
from uuid import UUID
|
||||
|
||||
from typing import AnyStr
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class SportResponse(BaseModel):
|
||||
id: UUID
|
||||
id: AnyStr
|
||||
name: str
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
{% extends "shared/base.html" %}
|
||||
|
||||
{% block title %}
|
||||
<title>Permissions</title>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% with msg=msg %}
|
||||
{% include "components/alerts.html" %}
|
||||
{% endwith %}
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h1 class="display-5">Permissions..</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<table class="table table-hover">
|
||||
<thead><tr>
|
||||
<th scope="col">Name</th>
|
||||
<th colspan="2">Actions</th>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
{% for permission in permissions %}
|
||||
<tr>
|
||||
<th scope="row"><a href="/admins/permissions/{{permission.id}}">{{permission.name}}</a></th>
|
||||
<td><a href="/admin/permission/edit/{{permission.id}}" class="btn btn-outline-primary btn-sm active" role="button" aria-pressed="true">Edit</a></td>
|
||||
<td><a href="/admin/permission/delete/{{permission.id}}" class="btn btn-outline-danger btn-sm active" role="button" aria-pressed="true">Delete</a></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<div>
|
||||
<a href="/admin/permission/add" class="btn btn-outline-primary btn-sm active" role="button" aria-pressed="true">Add Permission</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,43 @@
|
||||
{% extends "shared/base.html" %}
|
||||
|
||||
{% block title %}
|
||||
<title>Profiles</title>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% with msg=msg %}
|
||||
{% include "components/alerts.html" %}
|
||||
{% endwith %}
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h1 class="display-5">Profiles..</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<table class="table table-hover">
|
||||
<thead><tr>
|
||||
<th scope="col">Username</th>
|
||||
<th scope="col">First Name</th>
|
||||
<th scope="col">Last Name</th>
|
||||
<th colspan="2">Actions</th>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
{% for profile in profiles %}
|
||||
<tr>
|
||||
<th scope="row"><a href="/admins/profiles/{{profile.id}}">{{profile.user_name}}</a></th>
|
||||
<th scope="row">{{profile.first_name}}</th>
|
||||
<th scope="row">{{profile.last_name}}</th>
|
||||
<th scope="row">{{profile.email}}</th>
|
||||
<td><a href="/admin/profile/edit/{{profile.id}}" class="btn btn-outline-primary btn-sm active" role="button" aria-pressed="true">Edit</a></td>
|
||||
<td><a href="/admin/profile/delete/{{profile.id}}" class="btn btn-outline-danger btn-sm active" role="button" aria-pressed="true">Delete</a></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<div>
|
||||
<a href="/admin/profile/add" class="btn btn-outline-primary btn-sm active" role="button" aria-pressed="true">Add Profile</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,40 @@
|
||||
{% extends "shared/base.html" %}
|
||||
|
||||
|
||||
{% block title %}
|
||||
<title>Login</title>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<h5 class="display-5">Login to Kontor</h5>
|
||||
<div class="text-danger font-weight-bold">
|
||||
{% for error in errors %}
|
||||
<li>{{error}}</li>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="text-success font-weight-bold">
|
||||
{% if msg %}
|
||||
<div class="badge bg-success text-wrap font-weight-bold" style="font-size: large;">
|
||||
{{msg}}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row my-5">
|
||||
<form method="POST">
|
||||
<div class="mb-3">
|
||||
<label>Email</label>
|
||||
<input type="text" required placeholder="Your email" name="email" value="{{email}}" class="form-control">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label>Password</label>
|
||||
<input type="password" required placeholder="Choose a secure password" value="{{password}}" name="password" class="form-control">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Submit</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
+1
@@ -1,6 +1,7 @@
|
||||
<div class="card shadow p-3 mb-2 bg-body rounded" style="width: 18rem;">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">{{obj.name}}</h5>
|
||||
<p class="card-text">Link : {{obj.weblink}}</p>
|
||||
<a href="/comic/artists/{{obj.id}}" class="btn btn-primary">Read more</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -20,12 +20,16 @@
|
||||
<th scope="row">Artist Name</th>
|
||||
<td colspan="2">{{artist.name}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Link</th>
|
||||
<td colspan="2">{{artist.weblink}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Works</th>
|
||||
<td colspan="2">
|
||||
{% for work in artist.get_comics() %}
|
||||
<p>
|
||||
{{work}}:
|
||||
<a href="/comic/worktypes/{{work.id}}">{{work.name}}</a>
|
||||
<ul>
|
||||
{% for comic in artist.get_comics()[work] %}
|
||||
<li><a href="/comic/comics/{{comic.id}}">{{comic.title}}</a></li>
|
||||
@@ -43,8 +47,20 @@
|
||||
<th scope="row">Data Modified</th>
|
||||
<td colspan="2">{{artist.last_modified_date}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Data Version</th>
|
||||
<td colspan="2">{{artist.version}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div>
|
||||
<a href="/comic/artists" class="btn btn-outline-primary btn-sm active" role="button" aria-pressed="true">Back to list</a>
|
||||
<a href="/comic/artist/edit/{{artist.id}}" class="btn btn-outline-primary btn-sm active" role="button" aria-pressed="true">Edit</a>
|
||||
<a href="/comic/artist/delete/{{artist.id}}" class="btn btn-outline-danger btn-sm active" role="button" aria-pressed="true">Delete</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
{% extends "shared/base.html" %}
|
||||
|
||||
|
||||
{% block title %}
|
||||
<title>Edit Artist</title>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="text-danger font-weight-bold">
|
||||
{% for error in errors %}
|
||||
<li>{{error}}</li>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row my-5">
|
||||
<h3 class="text-center display-4">Edit an Artist entry</h3>
|
||||
<form method="POST">
|
||||
<div class="mb-3">
|
||||
<input type="text" required class="form-control" name="artist.name" value="{{artist_name}}" placeholder="Artist name here">
|
||||
<input type="text" required class="form-control" name="artist.link" value="{{artist_link}}" placeholder="Web link for artist here">
|
||||
</div>
|
||||
<div>
|
||||
<button type="submit" class="btn btn-primary">Submit</button>
|
||||
<button type="cancel" class="btn btn-primary">Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -18,7 +18,7 @@
|
||||
{% for artist in artists %}
|
||||
<div class="col-lg-4 col-md-3 col-sm-10 mr-auto">
|
||||
{% with obj=artist %}
|
||||
{% include "components/artist_cards.html" %}
|
||||
{% include "comic/artist_cards.html" %}
|
||||
{% endwith %}
|
||||
|
||||
{% if loop.index %3 %}
|
||||
|
||||
@@ -27,12 +27,17 @@
|
||||
{% endwith %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Link</th>
|
||||
<td colspan="2">{{comic.weblink}}</td>
|
||||
</tr>
|
||||
{% if comic.get_artists()|length > 0 %}
|
||||
<tr>
|
||||
<th scope="row">Works</th>
|
||||
<td colspan="2">
|
||||
{% for work in comic.get_artists() %}
|
||||
<p>
|
||||
{{work}}:
|
||||
<a href="/comic/worktypes/{{work.id}}">{{work.name}}</a>
|
||||
<ul>
|
||||
{% for artist in comic.get_artists()[work] %}
|
||||
<li><a href="/comic/artists/{{artist.id}}">{{artist.name}}</a></li>
|
||||
@@ -42,6 +47,29 @@
|
||||
{% endfor %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% if comic.volumes|length > 0 %}
|
||||
<tr>
|
||||
<th scope="row">Volumes</th>
|
||||
<td colspan="2">
|
||||
<ul>
|
||||
{% for volume in comic.volumes %}
|
||||
<li><a href="/comic/volumes/{{volume.id}}">{{volume.name}}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
<tr>
|
||||
<th scope="row">Issues</th>
|
||||
<td colspan="2">
|
||||
<ul>
|
||||
{% for issue in comic.sorted_issues() %}
|
||||
<li><a href="/comic/issues/{{issue.id}}">{{issue.get_full_title()}}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Data Created</th>
|
||||
<td colspan="2">{{comic.created_date}}</td>
|
||||
@@ -54,18 +82,15 @@
|
||||
<th scope="row">Data Version</th>
|
||||
<td colspan="2">{{comic.version}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Issues</th>
|
||||
<td colspan="2">
|
||||
<ul>
|
||||
{% for issue in comic.sorted_issues() %}
|
||||
<li><a href="comic/issues/{{issue.id}}">{{issue.issue_number}}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div>
|
||||
<a href="/comic/comics" class="btn btn-outline-primary btn-sm active" role="button" aria-pressed="true">Back to list</a>
|
||||
<a href="/comic/comic/edit/{{comic.id}}" class="btn btn-outline-primary btn-sm active" role="button" aria-pressed="true">Edit</a>
|
||||
<a href="/comic/comic/delete/{{comic.id}}" class="btn btn-outline-danger btn-sm active" role="button" aria-pressed="true">Delete</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
{% extends "shared/base.html" %}
|
||||
|
||||
|
||||
{% block title %}
|
||||
<title>Edit Comic</title>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="text-danger font-weight-bold">
|
||||
{% for error in errors %}
|
||||
<li>{{error}}</li>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row my-5">
|
||||
<h3 class="text-center display-4">Edit an Comic entry</h3>
|
||||
<form class="form-horizontal" method="POST">
|
||||
<div class="form-group">
|
||||
<label class="control-label col-sm-2" for="title">Title</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" class="form-control" id="title" name="title" value="{{comic_title}}" placeholder="Comic title here">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-sm-2" for="weblink">Link</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" class="form-control" id="weblink" name="weblink" value="{{comic_weblink}}" placeholder="Web link for comic here">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-offset-2 col-sm-10">
|
||||
<div class="checkbox">
|
||||
<label><input type="checkbox" id="completed" name="completed" value="{{comic_completed}}"> Completed</label>
|
||||
</div
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-offset-2 col-sm-10">
|
||||
<div class="checkbox">
|
||||
<label><input type="checkbox" id="current_order" name="current_order" value="{{comic_current_order}}"> Current Order</label>
|
||||
</div
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-offset-2 col-sm-10">
|
||||
<button type="submit" class="btn btn-primary" name="action" value="submit">Submit</button>
|
||||
<button type="cancel" class="btn btn-primary" name="action" value="cancel">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -10,22 +10,39 @@
|
||||
{% endwith %}
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h1 class="display-5">Find Jobs..</h1>
|
||||
</div>
|
||||
<form class="d-flex" action="/comic/comics/">
|
||||
<input class="form-control me-2" name="query" id="autocomplete" type="search" placeholder="Search" aria-label="Search">
|
||||
Completed<input type="checkbox" name="completed" {% if request.query_params.get("completed")=="on" %}checked{% endif %} aria-label="Completed">
|
||||
Order<input type="checkbox" name="order" {% if request.query_params.get("order")=="on" %}checked{% endif %} aria-label="Order">
|
||||
<button class="btn btn-outline-success" type="submit">Search</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="row">
|
||||
{% for comic in comics %}
|
||||
<div class="col-lg-4 col-md-3 col-sm-10 mr-auto">
|
||||
{% with obj=comic %}
|
||||
{% include "components/comic_cards.html" %}
|
||||
{% endwith %}
|
||||
{% if loop.index %3 %}
|
||||
<h1 class="display-5">Comics..</h1>
|
||||
</div>
|
||||
{% else %}
|
||||
</div></div><br><div class="row">
|
||||
{% endif %}
|
||||
<div class="row">
|
||||
<table class="table table-hover">
|
||||
<thead><tr>
|
||||
<th scope="col">Title</th>
|
||||
<th scope="col">Publisher</th>
|
||||
<th scope="col">Completed</th>
|
||||
<th colspan="2">Actions</th>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
{% for comic in comics %}
|
||||
<tr>
|
||||
<th scope="row"><a href="/comic/comics/{{comic.id}}">{{comic.title}}</a></th>
|
||||
<td><a href="/comic/publishers/{{comic.publisher.id}}">{{comic.publisher.name}}</a></td>
|
||||
<td>{% with check=comic.completed %}{% include "components/check.html" %}{% endwith %}</td>
|
||||
<td><a href="/comic/comics/edit/{{comic.id}}" class="btn btn-outline-primary btn-sm active" role="button" aria-pressed="true">Edit</a></td>
|
||||
<td><a href="/comic/comics/delete/{{comic.id}}" class="btn btn-outline-danger btn-sm active" role="button" aria-pressed="true">Delete</a></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<div>
|
||||
<a href="/comic/comic/add" class="btn btn-outline-primary btn-sm active" role="button" aria-pressed="true">Add Comic</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
{% extends "shared/base.html" %}
|
||||
|
||||
|
||||
{% block title %}
|
||||
<title>Issue Detail</title>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h1 class="display-5">Issue Detail</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<table class="table table-striped table-hover">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th scope="row">Issue Number</th>
|
||||
<td colspan="2">{{issue.issue_number}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Full Title</th>
|
||||
<td colspan="2">{{issue.title}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Published</th>
|
||||
<td colspan="2">{{issue.published_on}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Auf Lager</th>
|
||||
<td colspan="2">
|
||||
{% with check=issue.in_stock %}
|
||||
{% include "components/check.html" %}
|
||||
{% endwith %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Gelesen</th>
|
||||
<td colspan="2">
|
||||
{% with check=issue.is_read %}
|
||||
{% include "components/check.html" %}
|
||||
{% endwith %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Comic</th>
|
||||
<td colspan="2">
|
||||
<a href="/comic/comics/{{issue.comic_id}}">{{issue.comic.title}}</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% if issue.volume %}
|
||||
<tr>
|
||||
<th scope="row">Volume</th>
|
||||
<td colspan="2">
|
||||
<a href="/comic/comics/{{issue.volume_id}}">{{issue.volume.name}}</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
<tr>
|
||||
<th scope="row">Works</th>
|
||||
<td colspan="2">
|
||||
{% for work in issue.get_artists() %}
|
||||
<p>
|
||||
<a href="/comic/worktypes/{{work.id}}">{{work.name}}</a>
|
||||
<ul>
|
||||
{% for artist in issue.get_artists()[work] %}
|
||||
<li><a href="/comic/artists/{{artist.id}}">{{artist.name}}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</p>
|
||||
{% endfor %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Data Created</th>
|
||||
<td colspan="2">{{issue.created_date}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Data Modified</th>
|
||||
<td colspan="2">{{issue.last_modified_date}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Data Version</th>
|
||||
<td colspan="2">{{issue.version}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -20,6 +20,24 @@
|
||||
<th scope="row">Publisher Name</th>
|
||||
<td colspan="2">{{publisher.name}}</td>
|
||||
</tr>
|
||||
{% if publisher.parent_publisher_id %}
|
||||
<tr>
|
||||
<th scope="row">Parent Company</th>
|
||||
<td colspan="2"><a href="/comic/publishers/{{publisher.parent_publisher_id}}">{{publisher.parent_publisher.name}}</a></td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% if publisher.imprints|length > 0 %}
|
||||
<tr>
|
||||
<th scope="row">Imprints</th>
|
||||
<td colspan="2">
|
||||
<ul>
|
||||
{% for imprint in publisher.imprints %}
|
||||
<li><a href="/comic/publishers/{{imprint.id}}">{{imprint.name}}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
<tr>
|
||||
<th scope="row">Comics</th>
|
||||
<td colspan="2">
|
||||
@@ -41,5 +59,12 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div>
|
||||
<a href="/comic/publishers" class="btn btn-outline-primary btn-sm active" role="button" aria-pressed="true">Back to list</a>
|
||||
<a href="/comic/publisher/edit/{{publisher.id}}" class="btn btn-outline-primary btn-sm active" role="button" aria-pressed="true">Edit</a>
|
||||
<a href="/comic/publisher/delete/{{publisher.id}}" class="btn btn-outline-danger btn-sm active" role="button" aria-pressed="true">Delete</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
{% for publisher in publishers %}
|
||||
<div class="col-lg-4 col-md-3 col-sm-10 mr-auto">
|
||||
{% with obj=publisher %}
|
||||
{% include "components/publisher_cards.html" %}
|
||||
{% include "comic/publisher_cards.html" %}
|
||||
{% endwith %}
|
||||
{% if loop.index %3 %}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
{% extends "shared/base.html" %}
|
||||
|
||||
|
||||
{% block title %}
|
||||
<title>WorkType Detail</title>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
|
||||
<div class="row">
|
||||
<table class="table table-striped table-hover">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th scope="row">WorkType Name</th>
|
||||
<td colspan="2">{{worktype.name}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Works</th>
|
||||
<td colspan="2">
|
||||
{% for comic in worktype.get_artists() %}
|
||||
<p>
|
||||
{{comic}}:
|
||||
<ul>
|
||||
{% for artist in worktype.get_artists()[comic] %}
|
||||
<li><a href="/comic/artists/{{artist.id}}">{{artist.name}}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</p>
|
||||
{% endfor %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">ID</th>
|
||||
<td colspan="2">{{worktype.id}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Data Created</th>
|
||||
<td colspan="2">{{worktype.created_date}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Data Modified</th>
|
||||
<td colspan="2">{{worktype.last_modified_date}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Data Version</th>
|
||||
<td colspan="2">{{worktype.version}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div>
|
||||
<a href="/comic/worktypes" class="btn btn-outline-primary btn-sm active" role="button" aria-pressed="true">Back to list</a>
|
||||
<a href="/comic/worktype/edit/{{worktype.id}}" class="btn btn-outline-primary btn-sm active" role="button" aria-pressed="true">Edit</a>
|
||||
<a href="/comic/worktype/delete/{{worktype.id}}" class="btn btn-outline-danger btn-sm active" role="button" aria-pressed="true">Delete</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,28 @@
|
||||
{% extends "shared/base.html" %}
|
||||
|
||||
|
||||
{% block title %}
|
||||
<title>Edit WorkType</title>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="text-danger font-weight-bold">
|
||||
{% for error in errors %}
|
||||
<li>{{error}}</li>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row my-5">
|
||||
<h3 class="text-center display-4">Add a WorkType</h3>
|
||||
<form method="POST">
|
||||
<div class="mb-3">
|
||||
<input type="text" required class="form-control" name="worktype" value="{{worktype}}" placeholder="WorkType here">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Submit</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,28 @@
|
||||
{% extends "shared/base.html" %}
|
||||
|
||||
{% block title %}
|
||||
<title>WorkTypes</title>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% with msg=msg %}
|
||||
{% include "components/alerts.html" %}
|
||||
{% endwith %}
|
||||
<div class="container">
|
||||
<table class="table table-hover">
|
||||
<thead><tr>
|
||||
<th scope="col">Name</th>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
{% for worktype in worktypes %}
|
||||
<tr>
|
||||
<th scope="row"><a href="/comic/worktypes/{{worktype.id}}">{{worktype.name}}</a></th>
|
||||
<td><a href="/comic/worktype/edit/{{worktype.id}}" class="btn btn-outline-primary btn-sm active" role="button" aria-pressed="true">Edit</a></td>
|
||||
<td><a href="/comic/worktype/delete/{{worktype.id}}" class="btn btn-outline-danger btn-sm active" role="button" aria-pressed="true">Delete</a></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<a href="/comic/worktype/add" class="btn btn-outline-primary btn-sm active" role="button" aria-pressed="true">Add WorkType</a>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -1,4 +1,4 @@
|
||||
{% if check == 1 %}
|
||||
{% if check %}
|
||||
<img src="{{ url_for('static', path='images/tick.png') }}" alt="" width="24" height="24">
|
||||
{% else %}
|
||||
<img src="{{ url_for('static', path='images/cross.png') }}" alt="" width="24" height="24">
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
<nav class="navbar navbar-expand-lg navbar-light bg-light px-5">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="#">
|
||||
<img src="{{ url_for('static', path='images/logo.png') }}" alt="" width="30" height="24">
|
||||
</a>
|
||||
<a class="navbar-brand" href="#">Kontor</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
@@ -18,7 +16,7 @@
|
||||
<li><a class="dropdown-item" href="/comic/artists/">Artists</a></li>
|
||||
<li><a class="dropdown-item" href="/comic/publishers/">Publishers</a></li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li><a class="dropdown-item" href="#">Something else here</a></li>
|
||||
<li><a class="dropdown-item" href="/comic/worktypes">WorkTypes</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="nav-item dropdown">
|
||||
@@ -26,6 +24,7 @@
|
||||
<ul class="dropdown-menu" aria-labelledby="navbarDropdown">
|
||||
<li><a class="dropdown-item" href="/media/files/">MediaFiles</a></li>
|
||||
<li><a class="dropdown-item" href="/media/actors/">MediaActors</a></li>
|
||||
<li><a class="dropdown-item" href="/media/videos/">MediaVideos</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
@@ -42,18 +41,19 @@
|
||||
<li><a class="dropdown-item" href="/register/">Signup</a></li>
|
||||
<li><a class="dropdown-item" href="/login/">Login</a></li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li><a class="dropdown-item" href="#">Something else here</a></li>
|
||||
<li><a class="dropdown-item" href="/admin/profiles">Profiles</a></li>
|
||||
<li><a class="dropdown-item" href="/admin/permissions">Permissions</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="navbar-nav ml-auto mb-2 mb-lg-0">
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
Jobs
|
||||
Media
|
||||
</a>
|
||||
<ul class="dropdown-menu" aria-labelledby="navbarDropdown">
|
||||
<li><a class="dropdown-item" href="/comics/">Comics</a></li>
|
||||
<li><a class="dropdown-item" href="/comics/">Media</a></li>
|
||||
<li><a class="dropdown-item" href="/media/add-link/">Add Link</a></li>
|
||||
<li><a class="dropdown-item" href="/media/add-file">Add MediaFile</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -41,5 +41,12 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div>
|
||||
<a href="/media/actors" class="btn btn-outline-primary btn-sm active" role="button" aria-pressed="true">Back to list</a>
|
||||
<a href="/media/actor/edit/{{actor.id}}" class="btn btn-outline-primary btn-sm active" role="button" aria-pressed="true">Edit</a>
|
||||
<a href="/media/actor/delete/{{actor.id}}" class="btn btn-outline-danger btn-sm active" role="button" aria-pressed="true">Delete</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
{% for actor in actors %}
|
||||
<div class="col-lg-4 col-md-3 col-sm-10 mr-auto">
|
||||
{% with obj=actor %}
|
||||
{% include "components/actor_cards.html" %}
|
||||
{% include "media/actor_cards.html" %}
|
||||
{% endwith %}
|
||||
|
||||
{% if loop.index %3 %}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
{% extends "shared/base.html" %}
|
||||
|
||||
|
||||
{% block title %}
|
||||
<title>Add a Video Link</title>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="text-danger font-weight-bold">
|
||||
{% for error in errors %}
|
||||
<li>{{error}}</li>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row my-5">
|
||||
<h3 class="text-center display-4">Add a Video Link</h3>
|
||||
<form method="POST">
|
||||
<div class="mb-3">
|
||||
<input type="text" required class="form-control" name="url" value="{{url}}" placeholder="Video Link here">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Submit</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -61,5 +61,12 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div>
|
||||
<a href="/media/files" class="btn btn-outline-primary btn-sm active" role="button" aria-pressed="true">Back to list</a>
|
||||
<a href="/media/file/edit/{{mediafile.id}}" class="btn btn-outline-primary btn-sm active" role="button" aria-pressed="true">Edit</a>
|
||||
<a href="/media/file/delete/{{mediafile.id}}" class="btn btn-outline-danger btn-sm active" role="button" aria-pressed="true">Delete</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -9,21 +9,31 @@
|
||||
{% include "components/alerts.html" %}
|
||||
{% endwith %}
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<form class="d-flex" action="/media/files/">
|
||||
<input class="form-control me-2" name="query" id="autocomplete" type="search" placeholder="Search" aria-label="Search">
|
||||
Review<input type="checkbox" name="review" aria-label="Review">
|
||||
Download<input type="checkbox" name="download" aria-label="Download">
|
||||
<button class="btn btn-outline-success" type="submit">Search</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="row">
|
||||
<table class="table table-hover">
|
||||
<thead><tr>
|
||||
<th scope="col">Titel</th>
|
||||
<th scope="col">URL</th>
|
||||
<th scope="col">Cloudlink</th>
|
||||
<th scope="col">Review</th>
|
||||
<th scope="col">Download</th>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
{% for mediafile in mediafiles %}
|
||||
<tr>
|
||||
<th scope="row"><a href="/media/files/{{mediafile.id}}">{{mediafile.title}}</a></th>
|
||||
<td>{{mediafile.url}}</td>
|
||||
<td>{{mediafile.cloud_link}}</td>
|
||||
<td>{% with check=mediafile.review %}{% include "components/check.html" %}{% endwith %}</td>
|
||||
<td>{% with check=mediafile.should_download %}{% include "components/check.html" %}{% endwith %}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
{% extends "shared/base.html" %}
|
||||
|
||||
|
||||
{% block title %}
|
||||
<title>MediaVideo Detail</title>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h1 class="display-5">MediaVideo Detail</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<table class="table table-striped table-hover">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th scope="row">MediaVideo Title</th>
|
||||
<td colspan="2">{{mediavideo.title}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">MediaVideo URL</th>
|
||||
<td colspan="2">{{mediavideo.url}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">MediaVideo Cloudlink</th>
|
||||
<td colspan="2">{{mediavideo.cloud_link}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">MediaVideo Download?</th>
|
||||
<td colspan="2">{{mediavideo.should_download}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">MediaVideo Review?</th>
|
||||
<td colspan="2">{{mediavideo.review}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Data Created</th>
|
||||
<td colspan="2">{{mediavideo.created_date}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Data Modified</th>
|
||||
<td colspan="2">{{mediavideo.last_modified_date}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Data Version</th>
|
||||
<td colspan="2">{{mediavideo.version}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,29 @@
|
||||
{% extends "shared/base.html" %}
|
||||
|
||||
{% block title %}
|
||||
<title>Video Links</title>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% with msg=msg %}
|
||||
{% include "components/alerts.html" %}
|
||||
{% endwith %}
|
||||
<div class="container">
|
||||
<table class="table table-hover">
|
||||
<thead><tr>
|
||||
<th scope="col">Titel</th>
|
||||
<th scope="col">Review</th>
|
||||
<th scope="col">Download</th>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
{% for mediavideo in mediavideos %}
|
||||
<tr>
|
||||
<th scope="row"><a href="/media/videos/{{mediavideo.id}}">{{mediavideo.title}}</a></th>
|
||||
<td>{% with check=mediavideo.review %}{% include "components/check.html" %}{% endwith %}</td>
|
||||
<td>{% with check=mediavideo.should_download %}{% include "components/check.html" %}{% endwith %}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -1,5 +1,5 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en-us">
|
||||
<html lang="de-DE">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
@@ -21,6 +21,5 @@
|
||||
{% block scripts %}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
from typing import AnyStr
|
||||
from fastapi import APIRouter, Request
|
||||
from fastapi.security.utils import get_authorization_scheme_param
|
||||
from fastapi.templating import Jinja2Templates
|
||||
|
||||
from src.apis.utils import SessionDep
|
||||
from src.apis.version1.admin import get_current_user_from_token
|
||||
from src.db.models.admin import Permission, Profile
|
||||
|
||||
templates = Jinja2Templates(directory="src/templates")
|
||||
router = APIRouter(include_in_schema=False, prefix="/admin")
|
||||
|
||||
@router.get("/profiles")
|
||||
def get_profiles(db: SessionDep, request: Request, msg: str | None = None):
|
||||
profiles = db.query(Profile).all()
|
||||
return templates.TemplateResponse("admin/profiles.html", {"request": request, "msg": msg, "profiles": profiles})
|
||||
|
||||
@router.get("/profiles/{profile_id}")
|
||||
def comic_details(profile_id: AnyStr, request: Request, db: SessionDep):
|
||||
profile = db.get(Profile, profile_id)
|
||||
return templates.TemplateResponse("admin/profile_detail.html", {"request": request, "profile":profile})
|
||||
|
||||
@router.get("/permissions")
|
||||
def get_permissions(db: SessionDep, request: Request, msg: str | None = None):
|
||||
permissions = db.query(Permission).all()
|
||||
return templates.TemplateResponse("admin/permissions.html", {"request": request, "msg": msg, "permissions": permissions})
|
||||
|
||||
@router.get("/permissions/{permission_id}")
|
||||
def artist_detail(permission_id: AnyStr, request: Request, db: SessionDep):
|
||||
permission= db.get(Permission, str(permission_id))
|
||||
return templates.TemplateResponse("comic/permission_detail.html", {"request": request, "permission": permission})
|
||||
@@ -0,0 +1,27 @@
|
||||
from typing import List
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import Request
|
||||
|
||||
|
||||
class LoginForm:
|
||||
def __init__(self, request: Request):
|
||||
self.request: Request = request
|
||||
self.errors: List = []
|
||||
self.username: Optional[str] = None
|
||||
self.password: Optional[str] = None
|
||||
|
||||
async def load_data(self):
|
||||
form = await self.request.form()
|
||||
# since auth works on username field we are considering email as username
|
||||
self.username = form.get("email")
|
||||
self.password = form.get("password")
|
||||
|
||||
async def is_valid(self):
|
||||
if not self.username or not (self.username.__contains__("@")):
|
||||
self.errors.append("Email is required")
|
||||
if not self.password or not len(self.password) >= 4:
|
||||
self.errors.append("A valid password is required")
|
||||
if not self.errors:
|
||||
return True
|
||||
return False
|
||||
@@ -0,0 +1,35 @@
|
||||
from src.apis.version1.admin import login_for_access_token
|
||||
from src.db.session import get_db
|
||||
from fastapi import APIRouter
|
||||
from fastapi import Depends
|
||||
from fastapi import HTTPException
|
||||
from fastapi import Request
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from sqlalchemy.orm import Session
|
||||
from src.webapps.auth.forms import LoginForm
|
||||
|
||||
|
||||
templates = Jinja2Templates(directory="src/templates")
|
||||
router = APIRouter(include_in_schema=False)
|
||||
|
||||
|
||||
@router.get("/login/")
|
||||
def login(request: Request):
|
||||
return templates.TemplateResponse("auth/login.html", {"request": request})
|
||||
|
||||
|
||||
@router.post("/login/")
|
||||
async def login(request: Request, db: Session = Depends(get_db)):
|
||||
form = LoginForm(request)
|
||||
await form.load_data()
|
||||
if await form.is_valid():
|
||||
try:
|
||||
form.__dict__.update(msg="Login Successful :)")
|
||||
response = templates.TemplateResponse("auth/login.html", form.__dict__)
|
||||
login_for_access_token(response=response, form_data=form, db=db)
|
||||
return response
|
||||
except HTTPException:
|
||||
form.__dict__.update(msg="")
|
||||
form.__dict__.get("errors").append("Incorrect Email or Password")
|
||||
return templates.TemplateResponse("auth/login.html", form.__dict__)
|
||||
return templates.TemplateResponse("auth/login.html", form.__dict__)
|
||||
@@ -1,15 +1,22 @@
|
||||
from fastapi import APIRouter, Request
|
||||
from fastapi.templating import Jinja2Templates
|
||||
|
||||
from src.webapps.comic import route_comics
|
||||
from src.webapps.media import route_media
|
||||
from src.webapps.admin import route_admin
|
||||
from src.webapps.auth import route_login
|
||||
from src.webapps.comic import route_comics, route_worktype, route_artists
|
||||
from src.webapps.media import route_media, route_videos
|
||||
|
||||
templates = Jinja2Templates(directory="src/templates")
|
||||
|
||||
api_router = APIRouter()
|
||||
api_router.include_router(route_comics.router)
|
||||
api_router.include_router(route_artists.router)
|
||||
api_router.include_router(route_worktype.router)
|
||||
api_router.include_router(route_media.router)
|
||||
api_router.include_router(route_videos.router)
|
||||
api_router.include_router(route_login.router)
|
||||
api_router.include_router(route_admin.router)
|
||||
|
||||
@api_router.get("/")
|
||||
def home(request: Request, msg: str = None):
|
||||
def home(request: Request, msg: str | None = None):
|
||||
return templates.TemplateResponse("index.html", {"request": request, "msg": msg})
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
from fastapi import Request
|
||||
from typing import List, Optional
|
||||
|
||||
|
||||
class ValidateComicForm:
|
||||
def __init__(self, request: Request, comic_id: str, completed: bool, current_order: bool):
|
||||
self.request = request
|
||||
self.errors: List = []
|
||||
self.id = comic_id
|
||||
self.title: Optional[str] = None
|
||||
self.weblink: Optional[str] = None
|
||||
self.completed = completed
|
||||
self.current_order = current_order
|
||||
|
||||
async def load_data(self):
|
||||
form = await self.request.form()
|
||||
print(f"{form.keys()}")
|
||||
self.title = form.get("title")
|
||||
self.weblink = form.get("weblink")
|
||||
|
||||
def is_valid(self):
|
||||
if not self.errors:
|
||||
return True
|
||||
return False
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.title=}, {self.weblink=}"
|
||||
@@ -0,0 +1,20 @@
|
||||
from fastapi import Request
|
||||
from typing import List, Optional
|
||||
|
||||
|
||||
class AddWorktypeForm:
|
||||
def __init__(self, request: Request):
|
||||
self.request = request
|
||||
self.errors: List = []
|
||||
self.worktype: Optional[str] = None
|
||||
|
||||
async def load_data(self):
|
||||
form = await self.request.form()
|
||||
self.worktype = form.get("worktype")
|
||||
|
||||
def is_valid(self):
|
||||
if not self.worktype or (len(self.worktype) == 0):
|
||||
self.errors.append("WorkType cannot be empty")
|
||||
if not self.errors:
|
||||
return True
|
||||
return False
|
||||
@@ -0,0 +1,53 @@
|
||||
from fastapi import APIRouter, Request, status
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from fastapi.responses import RedirectResponse
|
||||
|
||||
from src.apis.utils import SessionDep
|
||||
from src.db.models.comic import Artist
|
||||
from typing import AnyStr
|
||||
|
||||
#from src.db.repository.comic import create_new_worktype, update_worktype
|
||||
from src.main import logger
|
||||
#from src.schema.comics.worktype import AddWorkType
|
||||
#from src.webapps.comic.forms import AddWorktypeForm
|
||||
|
||||
templates = Jinja2Templates(directory="src/templates")
|
||||
router = APIRouter(include_in_schema=False, prefix="/comic")
|
||||
|
||||
@router.get("/artists")
|
||||
def get_artists(db: SessionDep, request: Request, msg: str | None = None):
|
||||
artists = db.query(Artist).all()
|
||||
return templates.TemplateResponse("comic/artists.html", {"request": request, "msg": msg, "artists": artists})
|
||||
|
||||
@router.get("/artists/{artist_id}")
|
||||
def artist_detail(artist_id: AnyStr, request: Request, db: SessionDep):
|
||||
artist = db.get(Artist, str(artist_id))
|
||||
return templates.TemplateResponse("comic/artist_detail.html", {"request": request, "artist": artist})
|
||||
|
||||
|
||||
@router.get("/artist/edit/{artist_id}")
|
||||
def edit_artist(db: SessionDep, request: Request, artist_id: str):
|
||||
artist = db.get(Artist, artist_id)
|
||||
return templates.TemplateResponse("comic/artist_edit.html", {"request": request, "artist_name": artist.name, "artist_link": artist.weblink})
|
||||
|
||||
@router.post("/artist/edit/{artist_id}")
|
||||
async def edit_artist(request: Request, db: SessionDep, artist_id: str):
|
||||
form = AddArtistForm(request)
|
||||
await form.load_data()
|
||||
if form.is_valid():
|
||||
try:
|
||||
artist = AddArtist(**form.__dict__)
|
||||
artist = update_artist(artist=artist, artist_id=artist_id, db=db)
|
||||
return RedirectResponse(f"/comic/artists/{artist.id}", status_code=status.HTTP_303_SEE_OTHER)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
form.__dict__.get("errors").append("artist already added")
|
||||
return templates.TemplateResponse("comic/artist_edit.html", form.__dict__)
|
||||
return templates.TemplateResponse("comic/artist_edit.html", form.__dict__)
|
||||
|
||||
@router.get("/artist/delete/{artist_id}")
|
||||
async def delete_artist(db: SessionDep, request: Request, artist_id: str):
|
||||
artist = db.get(Artist, artist_id)
|
||||
db.delete(artist)
|
||||
db.commit()
|
||||
return RedirectResponse("/comic/artists", status_code=status.HTTP_303_SEE_OTHER)
|
||||
@@ -1,42 +1,90 @@
|
||||
from uuid import UUID
|
||||
|
||||
from fastapi import APIRouter, Request
|
||||
from fastapi import APIRouter, Form, Request, status
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from fastapi.responses import RedirectResponse
|
||||
|
||||
from src.apis.utils import SessionDep
|
||||
from src.db.models.comic import Comic, Artist, Publisher
|
||||
from src.db.models.comic import Comic, Publisher, Issue
|
||||
from typing import AnyStr
|
||||
from src.core.log_conf import logger
|
||||
from src.db.repository.comics.comic import update_comic
|
||||
from src.schema.comics.comic import ComicSchema
|
||||
from src.webapps.comic.forms.comic import ValidateComicForm
|
||||
|
||||
templates = Jinja2Templates(directory="src/templates")
|
||||
router = APIRouter(include_in_schema=False, prefix="/comic")
|
||||
|
||||
@router.get("/comics")
|
||||
def get_comics(db: SessionDep, request: Request, msg: str = None):
|
||||
def get_comics(db: SessionDep, request: Request, msg: str | None = None):
|
||||
params = request.query_params
|
||||
query = params.get("query")
|
||||
filter = {}
|
||||
completed = params.get('completed') == "on"
|
||||
if completed:
|
||||
filter['completed'] = True
|
||||
order = params.get("order") == "on"
|
||||
if order:
|
||||
filter['current_order'] = True
|
||||
if query is not None and len(query) > 0:
|
||||
filter['title'] = query
|
||||
if len(filter) > 0:
|
||||
if "title" in filter:
|
||||
comics = db.query(Comic).filter(Comic.title.ilike(f'%{query}%'))
|
||||
else:
|
||||
comics = db.query(Comic).filter_by(**filter).all()
|
||||
else:
|
||||
comics = db.query(Comic).all()
|
||||
return templates.TemplateResponse("comic/comics.html", {"request": request, "msg": msg, "comics": comics})
|
||||
|
||||
@router.get("/comics/{comic_id}")
|
||||
def comic_details(comic_id: UUID, request: Request, db: SessionDep):
|
||||
def comic_details(comic_id: AnyStr, request: Request, db: SessionDep):
|
||||
comic = db.get(Comic, comic_id)
|
||||
return templates.TemplateResponse("comic/comic_detail.html", {"request": request, "comic":comic})
|
||||
|
||||
@router.get("/artists")
|
||||
def get_artists(db: SessionDep, request: Request, msg: str = None):
|
||||
artists = db.query(Artist).all()
|
||||
return templates.TemplateResponse("comic/artists.html", {"request": request, "msg": msg, "artists": artists})
|
||||
@router.get("/comic/edit/{comic_id}")
|
||||
def edit_comic(db: SessionDep, request: Request, comic_id: str):
|
||||
comic = db.get(Comic, comic_id)
|
||||
return templates.TemplateResponse("comic/comic_edit.html", {"request": request, "comic_title": comic.title, "comic_weblink": comic.weblink})
|
||||
|
||||
@router.get("/artists/{artist_id}")
|
||||
def artist_detail(artist_id: UUID, request: Request, db: SessionDep):
|
||||
artist = db.get(Artist, artist_id)
|
||||
return templates.TemplateResponse("comic/artist_detail.html", {"request": request, "artist": artist})
|
||||
|
||||
|
||||
@router.post("/comic/edit/{comic_id}")
|
||||
async def validate_comic(request: Request, db: SessionDep, comic_id: str, action: str = Form(...), completed: bool = Form(False), current_order: bool = Form(False)):
|
||||
if action == "cancel":
|
||||
return RedirectResponse(f"/comic/comics/{comic_id}", status_code=status.HTTP_303_SEE_OTHER)
|
||||
form = ValidateComicForm(request, comic_id, completed, current_order)
|
||||
logger.info(f"request: {repr(request)}")
|
||||
await form.load_data()
|
||||
logger.info(f"form: {form}")
|
||||
if form.is_valid():
|
||||
try:
|
||||
comic = ComicSchema(**form.__dict__)
|
||||
comic = update_comic(comic=comic, comic_id=comic_id, db=db)
|
||||
return RedirectResponse(f"/comic/comics/{comic.id}", status_code=status.HTTP_303_SEE_OTHER)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
form.__dict__.get("errors").append("comic already added")
|
||||
return templates.TemplateResponse("comic/comic_edit.html", form.__dict__)
|
||||
return templates.TemplateResponse("comic/comic_edit.html", form.__dict__)
|
||||
|
||||
@router.get("/publishers")
|
||||
def get_publishers(db: SessionDep, request: Request, msg: str = None):
|
||||
def get_publishers(db: SessionDep, request: Request, msg: str | None = None):
|
||||
publishers = db.query(Publisher).all()
|
||||
return templates.TemplateResponse("comic/publishers.html", {"request": request, "publishers": publishers})
|
||||
|
||||
@router.get("/publishers/{publisher_id}")
|
||||
def publisher_details(publisher_id: UUID, request: Request, db: SessionDep, msg: str = None):
|
||||
def publisher_details(publisher_id: AnyStr, request: Request, db: SessionDep, msg: str = None):
|
||||
publisher = db.get(Publisher, publisher_id)
|
||||
if publisher is None:
|
||||
msg = "Could not find Publisher"
|
||||
return templates.TemplateResponse("comic/publisher_detail.html", {"request": request, "msg": msg, "publisher": publisher})
|
||||
|
||||
@router.get("/issues")
|
||||
def get_issues(db: SessionDep, request: Request, msg: str | None = None):
|
||||
issues = db.query(Issue).all()
|
||||
return templates.TemplateResponse("comic/issues.html", {"request": request, "msg": msg, "issues": issues})
|
||||
|
||||
@router.get("/issues/{issue_id}")
|
||||
def issue_details(issue_id: AnyStr, request: Request, db: SessionDep):
|
||||
issue = db.get(Issue, issue_id)
|
||||
return templates.TemplateResponse("comic/issue_detail.html", {"request": request, "issue": issue})
|
||||
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
from fastapi import APIRouter, Request, status
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from fastapi.responses import RedirectResponse
|
||||
|
||||
from src.apis.utils import SessionDep
|
||||
from src.db.models.comic import WorkType
|
||||
from typing import AnyStr
|
||||
|
||||
from src.db.repository.comics.worktype import create_new_worktype, update_worktype
|
||||
from src.main import logger
|
||||
from src.schema.comics.worktype import AddWorkType
|
||||
from src.webapps.comic.forms.worktype import AddWorktypeForm
|
||||
|
||||
templates = Jinja2Templates(directory="src/templates")
|
||||
router = APIRouter(include_in_schema=False, prefix="/comic")
|
||||
|
||||
@router.get("/worktypes")
|
||||
def get_worktypes(db: SessionDep, request: Request, msg: str | None = None):
|
||||
worktypes = db.query(WorkType).all()
|
||||
return templates.TemplateResponse("comic/worktypes.html", {"request": request, "msg": msg, "worktypes": worktypes})
|
||||
|
||||
@router.get("/worktypes/{worktype_id}")
|
||||
def worktype_detail(db: SessionDep, request: Request, worktype_id: AnyStr):
|
||||
worktype = db.get(WorkType, worktype_id)
|
||||
return templates.TemplateResponse("comic/worktype_detail.html", {"request": request, "worktype": worktype})
|
||||
|
||||
@router.get("/worktype/add")
|
||||
def add_worktype(request: Request, db: SessionDep):
|
||||
return templates.TemplateResponse("comic/worktype_edit.html", {"request": request})
|
||||
|
||||
@router.post("/worktype/add")
|
||||
async def add_worktype(db: SessionDep, request: Request):
|
||||
form = AddWorktypeForm(request)
|
||||
await form.load_data()
|
||||
if form.is_valid():
|
||||
try:
|
||||
work = AddWorkType(**form.__dict__)
|
||||
worktype = create_new_worktype(work=work, db=db)
|
||||
logger.info(f"add_worktype: redirect to /comic/worktypes/{worktype.id}")
|
||||
return RedirectResponse(f"/comic/worktypes/{worktype.id}", status_code=status.HTTP_303_SEE_OTHER)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
form.__dict__.get("errors").append("worktype already added")
|
||||
return templates.TemplateResponse("comic/worktype_edit.html", form.__dict__)
|
||||
print("form is not valid")
|
||||
return templates.TemplateResponse("comic/worktype_edit.html", form.__dict__)
|
||||
|
||||
@router.get("/worktype/edit/{worktype_id}")
|
||||
def edit_worktype(db: SessionDep, request: Request, worktype_id: str):
|
||||
worktype = db.get(WorkType, worktype_id)
|
||||
return templates.TemplateResponse("comic/worktype_edit.html", {"request": request, "worktype": worktype.name})
|
||||
|
||||
@router.post("/worktype/edit/{worktype_id}")
|
||||
async def edit_worktype(request: Request, db: SessionDep, worktype_id: str):
|
||||
form = AddWorktypeForm(request)
|
||||
await form.load_data()
|
||||
if form.is_valid():
|
||||
try:
|
||||
work = AddWorkType(**form.__dict__)
|
||||
worktype = update_worktype(work=work, worktype_id=worktype_id, db=db)
|
||||
return RedirectResponse(f"/comic/worktypes/{worktype.id}", status_code=status.HTTP_303_SEE_OTHER)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
form.__dict__.get("errors").append("worktype already added")
|
||||
return templates.TemplateResponse("comic/worktype_edit.html", form.__dict__)
|
||||
return templates.TemplateResponse("comic/worktype_edit.html", form.__dict__)
|
||||
|
||||
@router.get("/worktype/delete/{worktype_id}")
|
||||
async def delete_worktype(db: SessionDep, request: Request, worktype_id: str):
|
||||
worktype = db.get(WorkType, worktype_id)
|
||||
db.delete(worktype)
|
||||
db.commit()
|
||||
return RedirectResponse("/comic/worktypes", status_code=status.HTTP_303_SEE_OTHER)
|
||||
@@ -0,0 +1,20 @@
|
||||
from fastapi import Request
|
||||
from typing import List, Optional
|
||||
|
||||
|
||||
class AddLinkForm:
|
||||
def __init__(self, request: Request):
|
||||
self.request = request
|
||||
self.errors: List = []
|
||||
self.url: Optional[str] = None
|
||||
|
||||
async def load_data(self):
|
||||
form = await self.request.form()
|
||||
self.url = form.get("url")
|
||||
|
||||
def is_valid(self):
|
||||
if not self.url or not (self.url.__contains__("http")):
|
||||
self.errors.append("Valid Url is required e.g. https://example.com")
|
||||
if not self.errors:
|
||||
return True
|
||||
return False
|
||||
@@ -1,22 +1,50 @@
|
||||
from uuid import UUID
|
||||
from typing import AnyStr
|
||||
|
||||
from fastapi import APIRouter, Request
|
||||
from fastapi.security.utils import get_authorization_scheme_param
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from sqlalchemy import or_
|
||||
|
||||
from src.apis.utils import SessionDep
|
||||
from src.apis.version1.admin import get_current_user_from_token
|
||||
from src.db.models.admin import Profile
|
||||
from src.db.models.media import MediaFile, MediaActor
|
||||
#ifrom src.schema.media.comic import get_comic_details
|
||||
|
||||
templates = Jinja2Templates(directory="src/templates")
|
||||
router = APIRouter(include_in_schema=False, prefix="/media")
|
||||
|
||||
@router.get("/files")
|
||||
def get_mediafiles(db: SessionDep, request: Request, msg: str = None):
|
||||
params = request.query_params
|
||||
query = params.get("query")
|
||||
filter = {}
|
||||
review = params.get('review') == "on"
|
||||
if review:
|
||||
filter['review'] = True
|
||||
download = params.get("download") == "on"
|
||||
if download:
|
||||
filter['should_download'] = True
|
||||
if query is not None and len(query) > 0:
|
||||
filter['url'] = query
|
||||
if len(filter) > 0:
|
||||
if "url" in filter:
|
||||
mediafiles = db.query(MediaFile).filter(or_(MediaFile.title.ilike(f'%{query}%'), MediaFile.url.ilike(f"%{query}%")))
|
||||
else:
|
||||
mediafiles = db.query(MediaFile).filter_by(**filter).all()
|
||||
else:
|
||||
mediafiles = db.query(MediaFile).all()
|
||||
try:
|
||||
token = request.cookies.get("access_token")
|
||||
scheme, param = get_authorization_scheme_param(token) # scheme will hold "Bearer" and param will hold actual token value
|
||||
current_user: Profile = get_current_user_from_token(token=param, db=db)
|
||||
return templates.TemplateResponse("media/files.html", {"request": request, "msg": msg, "mediafiles": mediafiles})
|
||||
except Exception as e:
|
||||
print(e)
|
||||
msg = "Nicht berechtigt!!"
|
||||
return templates.TemplateResponse("media/files.html", {"request": request, "msg": msg, "mediafiles": []})
|
||||
|
||||
@router.get("/files/{file_id}")
|
||||
def file_details(file_id: UUID, request: Request, db: SessionDep):
|
||||
def file_details(file_id: AnyStr, request: Request, db: SessionDep):
|
||||
mediafile = db.get(MediaFile, file_id)
|
||||
return templates.TemplateResponse("media/file_detail.html", {"request": request, "mediafile":mediafile})
|
||||
|
||||
@@ -26,7 +54,7 @@ def get_actors(db: SessionDep, request: Request, msg: str = None):
|
||||
return templates.TemplateResponse("media/actors.html", {"request": request, "msg": msg, "actors": actors})
|
||||
|
||||
@router.get("/actors/{actor_id}")
|
||||
def artist_detail(actor_id: UUID, request: Request, db: SessionDep):
|
||||
def artist_detail(actor_id: AnyStr, request: Request, db: SessionDep):
|
||||
actor = db.get(MediaActor, actor_id)
|
||||
return templates.TemplateResponse("media/actor_detail.html", {"request": request, "actor": actor})
|
||||
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
from typing import AnyStr
|
||||
|
||||
from fastapi import APIRouter, Request, status, responses
|
||||
from fastapi.security.utils import get_authorization_scheme_param
|
||||
from fastapi.templating import Jinja2Templates
|
||||
|
||||
from src.apis.utils import SessionDep
|
||||
from src.db.models.media import MediaVideo
|
||||
from src.db.repository.media import create_new_video
|
||||
from src.apis.version1.admin import get_current_user_from_token
|
||||
from src.db.models.admin import Profile
|
||||
from src.schema.media.video import AddLink
|
||||
from src.webapps.media.forms import AddLinkForm
|
||||
|
||||
templates = Jinja2Templates(directory="src/templates")
|
||||
router = APIRouter(include_in_schema=False, prefix="/media")
|
||||
|
||||
@router.get("/videos")
|
||||
def get_mediavideos(db: SessionDep, request: Request, msg: str = None):
|
||||
mediavideos = db.query(MediaVideo).all()
|
||||
try:
|
||||
token = request.cookies.get("access_token")
|
||||
_, param = get_authorization_scheme_param(token) # scheme will hold "Bearer" and param will hold actual token value
|
||||
current_user: Profile = get_current_user_from_token(token=param, db=db)
|
||||
return templates.TemplateResponse("media/videos.html", {"request": request, "msg": msg, "user": current_user, "mediavideos": mediavideos})
|
||||
except Exception as e:
|
||||
print(e)
|
||||
return templates.TemplateResponse("media/videos.html", {"request": request, "msg": msg, "user": None, "mediavideos": mediavideos})
|
||||
|
||||
@router.get("/videos/{video_id}")
|
||||
def video_details(video_id: AnyStr, request: Request, db: SessionDep):
|
||||
mediavideo = db.get(MediaVideo, video_id)
|
||||
return templates.TemplateResponse("media/video_detail.html", {"request": request, "mediavideo":mediavideo})
|
||||
|
||||
@router.get("/add-link")
|
||||
def add_video_link(request: Request, db: SessionDep):
|
||||
return templates.TemplateResponse("media/add_video_link.html", {"request": request})
|
||||
|
||||
@router.post("/add-link")
|
||||
async def post_video_link(request: Request, db: SessionDep):
|
||||
form = AddLinkForm(request)
|
||||
await form.load_data()
|
||||
if form.is_valid():
|
||||
try:
|
||||
video = AddLink(**form.__dict__)
|
||||
mediavideo = create_new_video(video=video, db=db)
|
||||
return responses.RedirectResponse(f"media/videos/{mediavideo.id}", status_code=status.HTTP_302_FOUND)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
form.__dict__.get("errors").append("Link already added")
|
||||
return templates.TemplateResponse("media/add_video_link.html", form.__dict__)
|
||||
return templates.TemplateResponse("media/add_video_link.html", form.__dict__)
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
|
||||
import os
|
||||
import sys
|
||||
from typing import Any
|
||||
from typing import Generator
|
||||
|
||||
import pytest
|
||||
from fastapi import FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
from src.apis.base import api_router
|
||||
from src.db.models.base import Base
|
||||
from src.db.session import get_db
|
||||
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
# this is to include backend dir in sys.path so that we can import from db,main.py
|
||||
|
||||
|
||||
|
||||
def start_application():
|
||||
app = FastAPI()
|
||||
app.include_router(api_router)
|
||||
return app
|
||||
|
||||
|
||||
SQLALCHEMY_DATABASE_URL = "sqlite:///./test_db.db"
|
||||
engine = create_engine(
|
||||
SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}
|
||||
)
|
||||
# Use connect_args parameter only with sqlite
|
||||
SessionTesting = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def app() -> Generator[FastAPI, Any, None]:
|
||||
"""
|
||||
Create a fresh database on each test case.
|
||||
"""
|
||||
Base.metadata.create_all(engine) # Create the tables.
|
||||
_app = start_application()
|
||||
yield _app
|
||||
Base.metadata.drop_all(engine)
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def db_session(app: FastAPI) -> Generator[SessionTesting, Any, None]:
|
||||
connection = engine.connect()
|
||||
transaction = connection.begin()
|
||||
session = SessionTesting(bind=connection)
|
||||
yield session # use the session in tests.
|
||||
session.close()
|
||||
transaction.rollback()
|
||||
connection.close()
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def client(
|
||||
app: FastAPI, db_session: SessionTesting
|
||||
) -> Generator[TestClient, Any, None]:
|
||||
"""
|
||||
Create a new FastAPI TestClient that uses the `db_session` fixture to override
|
||||
the `get_db` dependency that is injected into routes.
|
||||
"""
|
||||
|
||||
def _get_test_db():
|
||||
try:
|
||||
yield db_session
|
||||
finally:
|
||||
pass
|
||||
|
||||
app.dependency_overrides[get_db] = _get_test_db
|
||||
with TestClient(app) as client:
|
||||
yield client
|
||||
@@ -1,15 +1,7 @@
|
||||
from fastapi.testclient import TestClient
|
||||
import pytest
|
||||
from src.main import app
|
||||
|
||||
|
||||
@pytest.fixture(name="client")
|
||||
def client_fixture():
|
||||
client = TestClient(app)
|
||||
yield client
|
||||
|
||||
|
||||
def test_get_artists(client: TestClient):
|
||||
response = client.get("/comic/artists")
|
||||
response = client.get("/api/comic/artists")
|
||||
assert response.status_code == 200
|
||||
assert len(response.json()) == 5
|
||||
assert len(response.json()) == 0
|
||||
|
||||
Generated
+400
-240
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,81 @@
|
||||
"""
|
||||
read file with URLs and store in DB
|
||||
"""
|
||||
import logging.config
|
||||
import requests
|
||||
import yaml
|
||||
from argparse import ArgumentParser, ArgumentDefaultsHelpFormatter
|
||||
from pathlib import Path
|
||||
from platformdirs import PlatformDirs
|
||||
from proton import Message, Event
|
||||
from proton.handlers import MessagingHandler
|
||||
from proton.reactor import Container
|
||||
|
||||
parser = ArgumentParser(formatter_class=ArgumentDefaultsHelpFormatter)
|
||||
parser.add_argument('-u', '--url', help='link')
|
||||
parser.add_argument('--video', help='store Url as VideoFile', action="store_true")
|
||||
parser.add_argument("--api", help="use Kontor API", action="store_true")
|
||||
parser.add_argument('--config', '-c', default='kontor-docker')
|
||||
parser.add_argument('--verbose', '-v', action='count', default=0)
|
||||
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
|
||||
|
||||
class AddLinkMessage(MessagingHandler):
|
||||
def __init__(self, server, url, log):
|
||||
super(AddLinkMessage, self).__init__()
|
||||
log.info("create AddLinkMessage")
|
||||
self.server = server
|
||||
self.address = "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_error(self, event: Event) -> None:
|
||||
self.log.info(f"error: {event}")
|
||||
|
||||
def on_sendable(self, event: Event):
|
||||
self.log.info("send message")
|
||||
event.sender.send(Message(body=self.url, address=self.address, content_type="text/json"))
|
||||
event.connection.close()
|
||||
event.sender.close()
|
||||
|
||||
def on_accepted(self, event: Event) -> None:
|
||||
self.log.info(f"accepted: {event}")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
logger = get_logger(args.verbose, args.config)
|
||||
logger.info('kontor.add_link started')
|
||||
link: str = args.url
|
||||
data = {"url": link}
|
||||
if args.api:
|
||||
if args.video:
|
||||
request: str = "http://127.0.0.1:8800/api/video/files"
|
||||
else:
|
||||
request: str = "http://127.0.0.1:8800/api/media/files"
|
||||
response = requests.post(request, json=data)
|
||||
logger.info(f"Status: {response.status_code}")
|
||||
data = response.json()
|
||||
else:
|
||||
Container(AddLinkMessage("amqp://127.0.0.1:5672", data, logger)).run()
|
||||
logger.info('kontor.add_link finished')
|
||||
@@ -0,0 +1 @@
|
||||
3.13
|
||||
@@ -0,0 +1,50 @@
|
||||
## ------------------------------- Builder Stage ------------------------------ ##
|
||||
FROM python:3.13-bookworm AS builder
|
||||
|
||||
RUN apt-get update && apt-get install --no-install-recommends -y build-essential && \
|
||||
apt-get clean && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Download the latest installer, install it and then remove it
|
||||
ADD https://astral.sh/uv/install.sh /install.sh
|
||||
RUN chmod -R 655 /install.sh && /install.sh && rm /install.sh
|
||||
|
||||
# Set up the UV environment path correctly
|
||||
ENV PATH="/root/.local/bin:${PATH}"
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY ./pyproject.toml .
|
||||
|
||||
RUN uv sync
|
||||
|
||||
# ------------------------------- Production Stage ------------------------------ ##
|
||||
FROM python:3.13-slim-bookworm AS production
|
||||
|
||||
# The following secrets are available during build time
|
||||
#RUN --mount=type=secret,id=DB_PASSWORD \
|
||||
# --mount=type=secret,id=DB_USER \
|
||||
# --mount=type=secret,id=DB_NAME \
|
||||
# --mount=type=secret,id=DB_HOST \
|
||||
# --mount=type=secret,id=DB_PORT \
|
||||
# DB_PASSWORD=/run/secrets/DB_PASSWORD \
|
||||
# DB_USER=$(cat /run/secrets/DB_USER) \
|
||||
# DB_NAME=$(cat /run/secrets/DB_NAME) \
|
||||
# DB_HOST=$(cat /run/secrets/DB_HOST) \
|
||||
# DB_PORT=$(cat /run/secrets/DB_PORT)
|
||||
|
||||
#RUN --mount=type=secret,id=secret-key,target=secrets.json
|
||||
|
||||
RUN useradd --create-home appuser
|
||||
USER appuser
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY /src src
|
||||
COPY --from=builder /app/.venv .venv
|
||||
|
||||
# Set up environment variables for production
|
||||
ENV PATH="/app/.venv/bin:$PATH"
|
||||
|
||||
# Start the application with Uvicorn in production mode, using environment variable references
|
||||
CMD ["python", "src/main.py", "--log-level", "info"]
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
[project]
|
||||
name = "kontor-domain"
|
||||
version = "0.2.0"
|
||||
description = "Example setup of DDD"
|
||||
readme = "README.md"
|
||||
authors = [
|
||||
{name = "Thomas Peetz", email = "thomas.peetz@thpeetz.de"}
|
||||
]
|
||||
maintainers = [
|
||||
{name = "Thomas Peetz", email = "thomas.peetz@thpeetz.de"}
|
||||
]
|
||||
requires-python = ">=3.13"
|
||||
dependencies = [
|
||||
"python-qpid-proton>=0.40.0",
|
||||
]
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
def main():
|
||||
print("Hello from kontor-domain!")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Generated
+57
@@ -0,0 +1,57 @@
|
||||
version = 1
|
||||
revision = 2
|
||||
requires-python = ">=3.13"
|
||||
|
||||
[[package]]
|
||||
name = "cffi"
|
||||
version = "1.17.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pycparser" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621, upload-time = "2024-09-04T20:45:21.852Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/f8/dd6c246b148639254dad4d6803eb6a54e8c85c6e11ec9df2cffa87571dbe/cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", size = 182989, upload-time = "2024-09-04T20:44:28.956Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/f1/672d303ddf17c24fc83afd712316fda78dc6fce1cd53011b839483e1ecc8/cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", size = 178802, upload-time = "2024-09-04T20:44:30.289Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792, upload-time = "2024-09-04T20:44:32.01Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893, upload-time = "2024-09-04T20:44:33.606Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810, upload-time = "2024-09-04T20:44:35.191Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200, upload-time = "2024-09-04T20:44:36.743Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447, upload-time = "2024-09-04T20:44:38.492Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358, upload-time = "2024-09-04T20:44:40.046Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469, upload-time = "2024-09-04T20:44:41.616Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475, upload-time = "2024-09-04T20:44:43.733Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009, upload-time = "2024-09-04T20:44:45.309Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kontor-domain"
|
||||
version = "0.2.0"
|
||||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "python-qpid-proton" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [{ name = "python-qpid-proton", specifier = ">=0.40.0" }]
|
||||
|
||||
[[package]]
|
||||
name = "pycparser"
|
||||
version = "2.22"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736, upload-time = "2024-03-30T13:22:22.564Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552, upload-time = "2024-03-30T13:22:20.476Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "python-qpid-proton"
|
||||
version = "0.40.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "cffi" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d5/dd/e9e5066009517bdfee92374264a2b6794fa0987bfeddcbf4d2a08dccaf36/python_qpid_proton-0.40.0.tar.gz", hash = "sha256:7680d607cf6e9684f97bf5b2ba16cda7d8512aab9e4ff78f98d44a4644fc819a", size = 354215, upload-time = "2025-05-19T18:45:37.932Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/dd/a82c1e377f08d62d83898c1aa9b39aef890e910f683fca6dc5242a123f6b/python_qpid_proton-0.40.0-cp313-cp313-win_amd64.whl", hash = "sha256:a19d8c71c908700ceb38f6cbc1eb4a039428570f96bfc2caeeafdfec804fb94f", size = 277376, upload-time = "2025-05-19T19:39:31.201Z" },
|
||||
]
|
||||
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "kontor-schema"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
description = "Kontor Schema Library"
|
||||
readme = "README.md"
|
||||
authors = [
|
||||
|
||||
Generated
+59
-59
@@ -1,5 +1,5 @@
|
||||
version = 1
|
||||
revision = 1
|
||||
revision = 2
|
||||
requires-python = ">=3.13"
|
||||
|
||||
[[package]]
|
||||
@@ -10,79 +10,79 @@ dependencies = [
|
||||
{ name = "soupsieve" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d8/e4/0c4c39e18fd76d6a628d4dd8da40543d136ce2d1752bd6eeeab0791f4d6b/beautifulsoup4-4.13.4.tar.gz", hash = "sha256:dbb3c4e1ceae6aefebdaf2423247260cd062430a410e38c66f2baa50a8437195", size = 621067 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d8/e4/0c4c39e18fd76d6a628d4dd8da40543d136ce2d1752bd6eeeab0791f4d6b/beautifulsoup4-4.13.4.tar.gz", hash = "sha256:dbb3c4e1ceae6aefebdaf2423247260cd062430a410e38c66f2baa50a8437195", size = 621067, upload_time = "2025-04-15T17:05:13.836Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/50/cd/30110dc0ffcf3b131156077b90e9f60ed75711223f306da4db08eff8403b/beautifulsoup4-4.13.4-py3-none-any.whl", hash = "sha256:9bbbb14bfde9d79f38b8cd5f8c7c85f4b8f2523190ebed90e950a8dea4cb1c4b", size = 187285 },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/cd/30110dc0ffcf3b131156077b90e9f60ed75711223f306da4db08eff8403b/beautifulsoup4-4.13.4-py3-none-any.whl", hash = "sha256:9bbbb14bfde9d79f38b8cd5f8c7c85f4b8f2523190ebed90e950a8dea4cb1c4b", size = 187285, upload_time = "2025-04-15T17:05:12.221Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "certifi"
|
||||
version = "2025.1.31"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/1c/ab/c9f1e32b7b1bf505bf26f0ef697775960db7932abeb7b516de930ba2705f/certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", size = 167577 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/1c/ab/c9f1e32b7b1bf505bf26f0ef697775960db7932abeb7b516de930ba2705f/certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", size = 167577, upload_time = "2025-01-31T02:16:47.166Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe", size = 166393 },
|
||||
{ url = "https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe", size = 166393, upload_time = "2025-01-31T02:16:45.015Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "charset-normalizer"
|
||||
version = "3.4.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/16/b0/572805e227f01586461c80e0fd25d65a2115599cc9dad142fee4b747c357/charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3", size = 123188 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/16/b0/572805e227f01586461c80e0fd25d65a2115599cc9dad142fee4b747c357/charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3", size = 123188, upload_time = "2024-12-24T18:12:35.43Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/38/94/ce8e6f63d18049672c76d07d119304e1e2d7c6098f0841b51c666e9f44a0/charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda", size = 195698 },
|
||||
{ url = "https://files.pythonhosted.org/packages/24/2e/dfdd9770664aae179a96561cc6952ff08f9a8cd09a908f259a9dfa063568/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313", size = 140162 },
|
||||
{ url = "https://files.pythonhosted.org/packages/24/4e/f646b9093cff8fc86f2d60af2de4dc17c759de9d554f130b140ea4738ca6/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9", size = 150263 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5e/67/2937f8d548c3ef6e2f9aab0f6e21001056f692d43282b165e7c56023e6dd/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b", size = 142966 },
|
||||
{ url = "https://files.pythonhosted.org/packages/52/ed/b7f4f07de100bdb95c1756d3a4d17b90c1a3c53715c1a476f8738058e0fa/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11", size = 144992 },
|
||||
{ url = "https://files.pythonhosted.org/packages/96/2c/d49710a6dbcd3776265f4c923bb73ebe83933dfbaa841c5da850fe0fd20b/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f", size = 147162 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b4/41/35ff1f9a6bd380303dea55e44c4933b4cc3c4850988927d4082ada230273/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd", size = 140972 },
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/43/c6a0b685fe6910d08ba971f62cd9c3e862a85770395ba5d9cad4fede33ab/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2", size = 149095 },
|
||||
{ url = "https://files.pythonhosted.org/packages/4c/ff/a9a504662452e2d2878512115638966e75633519ec11f25fca3d2049a94a/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886", size = 152668 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/71/189996b6d9a4b932564701628af5cee6716733e9165af1d5e1b285c530ed/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601", size = 150073 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/93/946a86ce20790e11312c87c75ba68d5f6ad2208cfb52b2d6a2c32840d922/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd", size = 145732 },
|
||||
{ url = "https://files.pythonhosted.org/packages/cd/e5/131d2fb1b0dddafc37be4f3a2fa79aa4c037368be9423061dccadfd90091/charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407", size = 95391 },
|
||||
{ url = "https://files.pythonhosted.org/packages/27/f2/4f9a69cc7712b9b5ad8fdb87039fd89abba997ad5cbe690d1835d40405b0/charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971", size = 102702 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/f6/65ecc6878a89bb1c23a086ea335ad4bf21a588990c3f535a227b9eea9108/charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85", size = 49767 },
|
||||
{ url = "https://files.pythonhosted.org/packages/38/94/ce8e6f63d18049672c76d07d119304e1e2d7c6098f0841b51c666e9f44a0/charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda", size = 195698, upload_time = "2024-12-24T18:11:05.834Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/24/2e/dfdd9770664aae179a96561cc6952ff08f9a8cd09a908f259a9dfa063568/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313", size = 140162, upload_time = "2024-12-24T18:11:07.064Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/24/4e/f646b9093cff8fc86f2d60af2de4dc17c759de9d554f130b140ea4738ca6/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9", size = 150263, upload_time = "2024-12-24T18:11:08.374Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5e/67/2937f8d548c3ef6e2f9aab0f6e21001056f692d43282b165e7c56023e6dd/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b", size = 142966, upload_time = "2024-12-24T18:11:09.831Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/52/ed/b7f4f07de100bdb95c1756d3a4d17b90c1a3c53715c1a476f8738058e0fa/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11", size = 144992, upload_time = "2024-12-24T18:11:12.03Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/96/2c/d49710a6dbcd3776265f4c923bb73ebe83933dfbaa841c5da850fe0fd20b/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f", size = 147162, upload_time = "2024-12-24T18:11:13.372Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b4/41/35ff1f9a6bd380303dea55e44c4933b4cc3c4850988927d4082ada230273/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd", size = 140972, upload_time = "2024-12-24T18:11:14.628Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/43/c6a0b685fe6910d08ba971f62cd9c3e862a85770395ba5d9cad4fede33ab/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2", size = 149095, upload_time = "2024-12-24T18:11:17.672Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4c/ff/a9a504662452e2d2878512115638966e75633519ec11f25fca3d2049a94a/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886", size = 152668, upload_time = "2024-12-24T18:11:18.989Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/71/189996b6d9a4b932564701628af5cee6716733e9165af1d5e1b285c530ed/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601", size = 150073, upload_time = "2024-12-24T18:11:21.507Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/93/946a86ce20790e11312c87c75ba68d5f6ad2208cfb52b2d6a2c32840d922/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd", size = 145732, upload_time = "2024-12-24T18:11:22.774Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cd/e5/131d2fb1b0dddafc37be4f3a2fa79aa4c037368be9423061dccadfd90091/charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407", size = 95391, upload_time = "2024-12-24T18:11:24.139Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/27/f2/4f9a69cc7712b9b5ad8fdb87039fd89abba997ad5cbe690d1835d40405b0/charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971", size = 102702, upload_time = "2024-12-24T18:11:26.535Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/f6/65ecc6878a89bb1c23a086ea335ad4bf21a588990c3f535a227b9eea9108/charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85", size = 49767, upload_time = "2024-12-24T18:12:32.852Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "greenlet"
|
||||
version = "3.2.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b0/9c/666d8c71b18d0189cf801c0e0b31c4bfc609ac823883286045b1f3ae8994/greenlet-3.2.0.tar.gz", hash = "sha256:1d2d43bd711a43db8d9b9187500e6432ddb4fafe112d082ffabca8660a9e01a7", size = 183685 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b0/9c/666d8c71b18d0189cf801c0e0b31c4bfc609ac823883286045b1f3ae8994/greenlet-3.2.0.tar.gz", hash = "sha256:1d2d43bd711a43db8d9b9187500e6432ddb4fafe112d082ffabca8660a9e01a7", size = 183685, upload_time = "2025-04-15T16:21:26.141Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/43/c0b655d4d7eae19282b028bcec449e5c80626ad0d8d0ca3703f9b1c29258/greenlet-3.2.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:b86a3ccc865ae601f446af042707b749eebc297928ea7bd0c5f60c56525850be", size = 269131 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/7d/c8f51c373c7f7ac0f73d04a6fd77ab34f6f643cb41a0d186d05ba96708e7/greenlet-3.2.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:144283ad88ed77f3ebd74710dd419b55dd15d18704b0ae05935766a93f5671c5", size = 637323 },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/65/c3ee41b2e56586737d6e124b250583695628ffa6b324855b3a1267a8d1d9/greenlet-3.2.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5be69cd50994b8465c3ad1467f9e63001f76e53a89440ad4440d1b6d52591280", size = 651430 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/07/33bd7a3dcde1db7259371d026ce76be1eb653d2d892334fc79a500b3c5ee/greenlet-3.2.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:47aeadd1e8fbdef8fdceb8fb4edc0cbb398a57568d56fd68f2bc00d0d809e6b6", size = 645798 },
|
||||
{ url = "https://files.pythonhosted.org/packages/35/5b/33c221a6a867030b0b770513a1b78f6c30e04294131dafdc8da78906bbe6/greenlet-3.2.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18adc14ab154ca6e53eecc9dc50ff17aeb7ba70b7e14779b26e16d71efa90038", size = 648271 },
|
||||
{ url = "https://files.pythonhosted.org/packages/4d/dd/d6452248fa6093504e3b7525dc2bdc4e55a4296ec6ee74ba241a51d852e2/greenlet-3.2.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8622b33d8694ec373ad55050c3d4e49818132b44852158442e1931bb02af336", size = 606779 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9d/24/160f04d2589bcb15b8661dcd1763437b22e01643626899a4139bf98f02af/greenlet-3.2.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:e8ac9a2c20fbff3d0b853e9ef705cdedb70d9276af977d1ec1cde86a87a4c821", size = 1117968 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/ff/c6e3f3a5168fef5209cfd9498b2b5dd77a0bf29dfc686a03dcc614cf4432/greenlet-3.2.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:cd37273dc7ca1d5da149b58c8b3ce0711181672ba1b09969663905a765affe21", size = 1145510 },
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/62/5215e374819052e542b5bde06bd7d4a171454b6938c96a2384f21cb94279/greenlet-3.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:8a8940a8d301828acd8b9f3f85db23069a692ff2933358861b19936e29946b95", size = 296004 },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/6d/dc9c909cba5cbf4b0833fce69912927a8ca74791c23c47b9fd4f28092108/greenlet-3.2.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ee59db626760f1ca8da697a086454210d36a19f7abecc9922a2374c04b47735b", size = 629900 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5e/a9/f3f304fbbbd604858ff3df303d7fa1d8f7f9e45a6ef74481aaf03aaac021/greenlet-3.2.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7154b13ef87a8b62fc05419f12d75532d7783586ad016c57b5de8a1c6feeb517", size = 635270 },
|
||||
{ url = "https://files.pythonhosted.org/packages/34/92/4b7b4e2e23ecc723cceef9fe3898e78c8e14e106cc7ba2f276a66161da3e/greenlet-3.2.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:199453d64b02d0c9d139e36d29681efd0e407ed8e2c0bf89d88878d6a787c28f", size = 632534 },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/7f/91f0ecbe72c9d789fb7f400b39da9d1e87fcc2cf8746a9636479ba79ab01/greenlet-3.2.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0010e928e1901d36625f21d008618273f9dda26b516dbdecf873937d39c9dff0", size = 628826 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/59/e449a44ce52b13751f55376d85adc155dd311608f6d2aa5b6bd2c8d15486/greenlet-3.2.0-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6005f7a86de836a1dc4b8d824a2339cdd5a1ca7cb1af55ea92575401f9952f4c", size = 593697 },
|
||||
{ url = "https://files.pythonhosted.org/packages/bb/09/cca3392927c5c990b7a8ede64ccd0712808438d6490d63ce6b8704d6df5f/greenlet-3.2.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:17fd241c0d50bacb7ce8ff77a30f94a2d0ca69434ba2e0187cf95a5414aeb7e1", size = 1105762 },
|
||||
{ url = "https://files.pythonhosted.org/packages/4d/b9/3d201f819afc3b7a8cd7ebe645f1a17799603e2d62c968154518f79f4881/greenlet-3.2.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:7b17a26abc6a1890bf77d5d6b71c0999705386b00060d15c10b8182679ff2790", size = 1125173 },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/7b/773a30602234597fc2882091f8e1d1a38ea0b4419d99ca7ed82c827e2c3a/greenlet-3.2.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:397b6bbda06f8fe895893d96218cd6f6d855a6701dc45012ebe12262423cec8b", size = 269908 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/43/c0b655d4d7eae19282b028bcec449e5c80626ad0d8d0ca3703f9b1c29258/greenlet-3.2.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:b86a3ccc865ae601f446af042707b749eebc297928ea7bd0c5f60c56525850be", size = 269131, upload_time = "2025-04-15T16:19:19.469Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/7d/c8f51c373c7f7ac0f73d04a6fd77ab34f6f643cb41a0d186d05ba96708e7/greenlet-3.2.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:144283ad88ed77f3ebd74710dd419b55dd15d18704b0ae05935766a93f5671c5", size = 637323, upload_time = "2025-04-15T16:49:02.677Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/65/c3ee41b2e56586737d6e124b250583695628ffa6b324855b3a1267a8d1d9/greenlet-3.2.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5be69cd50994b8465c3ad1467f9e63001f76e53a89440ad4440d1b6d52591280", size = 651430, upload_time = "2025-04-15T16:50:43.445Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/07/33bd7a3dcde1db7259371d026ce76be1eb653d2d892334fc79a500b3c5ee/greenlet-3.2.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:47aeadd1e8fbdef8fdceb8fb4edc0cbb398a57568d56fd68f2bc00d0d809e6b6", size = 645798, upload_time = "2025-04-15T16:55:03.795Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/35/5b/33c221a6a867030b0b770513a1b78f6c30e04294131dafdc8da78906bbe6/greenlet-3.2.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18adc14ab154ca6e53eecc9dc50ff17aeb7ba70b7e14779b26e16d71efa90038", size = 648271, upload_time = "2025-04-15T16:22:42.458Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4d/dd/d6452248fa6093504e3b7525dc2bdc4e55a4296ec6ee74ba241a51d852e2/greenlet-3.2.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8622b33d8694ec373ad55050c3d4e49818132b44852158442e1931bb02af336", size = 606779, upload_time = "2025-04-15T16:22:41.417Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9d/24/160f04d2589bcb15b8661dcd1763437b22e01643626899a4139bf98f02af/greenlet-3.2.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:e8ac9a2c20fbff3d0b853e9ef705cdedb70d9276af977d1ec1cde86a87a4c821", size = 1117968, upload_time = "2025-04-15T16:52:53.627Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/ff/c6e3f3a5168fef5209cfd9498b2b5dd77a0bf29dfc686a03dcc614cf4432/greenlet-3.2.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:cd37273dc7ca1d5da149b58c8b3ce0711181672ba1b09969663905a765affe21", size = 1145510, upload_time = "2025-04-15T16:23:01.873Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/62/5215e374819052e542b5bde06bd7d4a171454b6938c96a2384f21cb94279/greenlet-3.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:8a8940a8d301828acd8b9f3f85db23069a692ff2933358861b19936e29946b95", size = 296004, upload_time = "2025-04-15T16:55:46.007Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/6d/dc9c909cba5cbf4b0833fce69912927a8ca74791c23c47b9fd4f28092108/greenlet-3.2.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ee59db626760f1ca8da697a086454210d36a19f7abecc9922a2374c04b47735b", size = 629900, upload_time = "2025-04-15T16:49:04.099Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5e/a9/f3f304fbbbd604858ff3df303d7fa1d8f7f9e45a6ef74481aaf03aaac021/greenlet-3.2.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7154b13ef87a8b62fc05419f12d75532d7783586ad016c57b5de8a1c6feeb517", size = 635270, upload_time = "2025-04-15T16:50:44.769Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/34/92/4b7b4e2e23ecc723cceef9fe3898e78c8e14e106cc7ba2f276a66161da3e/greenlet-3.2.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:199453d64b02d0c9d139e36d29681efd0e407ed8e2c0bf89d88878d6a787c28f", size = 632534, upload_time = "2025-04-15T16:55:05.203Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/7f/91f0ecbe72c9d789fb7f400b39da9d1e87fcc2cf8746a9636479ba79ab01/greenlet-3.2.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0010e928e1901d36625f21d008618273f9dda26b516dbdecf873937d39c9dff0", size = 628826, upload_time = "2025-04-15T16:22:44.545Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/59/e449a44ce52b13751f55376d85adc155dd311608f6d2aa5b6bd2c8d15486/greenlet-3.2.0-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6005f7a86de836a1dc4b8d824a2339cdd5a1ca7cb1af55ea92575401f9952f4c", size = 593697, upload_time = "2025-04-15T16:22:43.796Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bb/09/cca3392927c5c990b7a8ede64ccd0712808438d6490d63ce6b8704d6df5f/greenlet-3.2.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:17fd241c0d50bacb7ce8ff77a30f94a2d0ca69434ba2e0187cf95a5414aeb7e1", size = 1105762, upload_time = "2025-04-15T16:52:55.245Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4d/b9/3d201f819afc3b7a8cd7ebe645f1a17799603e2d62c968154518f79f4881/greenlet-3.2.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:7b17a26abc6a1890bf77d5d6b71c0999705386b00060d15c10b8182679ff2790", size = 1125173, upload_time = "2025-04-15T16:23:03.009Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/7b/773a30602234597fc2882091f8e1d1a38ea0b4419d99ca7ed82c827e2c3a/greenlet-3.2.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:397b6bbda06f8fe895893d96218cd6f6d855a6701dc45012ebe12262423cec8b", size = 269908, upload_time = "2025-04-15T16:20:33.58Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "3.10"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload_time = "2024-09-15T18:07:39.745Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 },
|
||||
{ url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload_time = "2024-09-15T18:07:37.964Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kontor-schema"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "beautifulsoup4" },
|
||||
@@ -107,18 +107,18 @@ dependencies = [
|
||||
{ name = "idna" },
|
||||
{ name = "urllib3" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218, upload_time = "2024-05-29T15:37:49.536Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928, upload_time = "2024-05-29T15:37:47.027Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "soupsieve"
|
||||
version = "2.7"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/3f/f4/4a80cd6ef364b2e8b65b15816a843c0980f7a5a2b4dc701fc574952aa19f/soupsieve-2.7.tar.gz", hash = "sha256:ad282f9b6926286d2ead4750552c8a6142bc4c783fd66b0293547c8fe6ae126a", size = 103418 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/3f/f4/4a80cd6ef364b2e8b65b15816a843c0980f7a5a2b4dc701fc574952aa19f/soupsieve-2.7.tar.gz", hash = "sha256:ad282f9b6926286d2ead4750552c8a6142bc4c783fd66b0293547c8fe6ae126a", size = 103418, upload_time = "2025-04-20T18:50:08.518Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/9c/0e6afc12c269578be5c0c1c9f4b49a8d32770a080260c333ac04cc1c832d/soupsieve-2.7-py3-none-any.whl", hash = "sha256:6e60cc5c1ffaf1cebcc12e8188320b72071e922c2e897f737cadce79ad5d30c4", size = 36677 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/9c/0e6afc12c269578be5c0c1c9f4b49a8d32770a080260c333ac04cc1c832d/soupsieve-2.7-py3-none-any.whl", hash = "sha256:6e60cc5c1ffaf1cebcc12e8188320b72071e922c2e897f737cadce79ad5d30c4", size = 36677, upload_time = "2025-04-20T18:50:07.196Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -129,33 +129,33 @@ dependencies = [
|
||||
{ name = "greenlet", marker = "(python_full_version < '3.14' and platform_machine == 'AMD64') or (python_full_version < '3.14' and platform_machine == 'WIN32') or (python_full_version < '3.14' and platform_machine == 'aarch64') or (python_full_version < '3.14' and platform_machine == 'amd64') or (python_full_version < '3.14' and platform_machine == 'ppc64le') or (python_full_version < '3.14' and platform_machine == 'win32') or (python_full_version < '3.14' and platform_machine == 'x86_64')" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/68/c3/3f2bfa5e4dcd9938405fe2fab5b6ab94a9248a4f9536ea2fd497da20525f/sqlalchemy-2.0.40.tar.gz", hash = "sha256:d827099289c64589418ebbcaead0145cd19f4e3e8a93919a0100247af245fa00", size = 9664299 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/68/c3/3f2bfa5e4dcd9938405fe2fab5b6ab94a9248a4f9536ea2fd497da20525f/sqlalchemy-2.0.40.tar.gz", hash = "sha256:d827099289c64589418ebbcaead0145cd19f4e3e8a93919a0100247af245fa00", size = 9664299, upload_time = "2025-03-27T17:52:31.876Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/8c/18/4e3a86cc0232377bc48c373a9ba6a1b3fb79ba32dbb4eda0b357f5a2c59d/sqlalchemy-2.0.40-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:915866fd50dd868fdcc18d61d8258db1bf9ed7fbd6dfec960ba43365952f3b01", size = 2107887 },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/60/9fa692b1d2ffc4cbd5f47753731fd332afed30137115d862d6e9a1e962c7/sqlalchemy-2.0.40-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4a4c5a2905a9ccdc67a8963e24abd2f7afcd4348829412483695c59e0af9a705", size = 2098367 },
|
||||
{ url = "https://files.pythonhosted.org/packages/4c/9f/84b78357ca641714a439eb3fbbddb17297dacfa05d951dbf24f28d7b5c08/sqlalchemy-2.0.40-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55028d7a3ebdf7ace492fab9895cbc5270153f75442a0472d8516e03159ab364", size = 3184806 },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/7d/e06164161b6bfce04c01bfa01518a20cccbd4100d5c951e5a7422189191a/sqlalchemy-2.0.40-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6cfedff6878b0e0d1d0a50666a817ecd85051d12d56b43d9d425455e608b5ba0", size = 3198131 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/51/354af20da42d7ec7b5c9de99edafbb7663a1d75686d1999ceb2c15811302/sqlalchemy-2.0.40-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bb19e30fdae77d357ce92192a3504579abe48a66877f476880238a962e5b96db", size = 3131364 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7a/2f/48a41ff4e6e10549d83fcc551ab85c268bde7c03cf77afb36303c6594d11/sqlalchemy-2.0.40-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:16d325ea898f74b26ffcd1cf8c593b0beed8714f0317df2bed0d8d1de05a8f26", size = 3159482 },
|
||||
{ url = "https://files.pythonhosted.org/packages/33/ac/e5e0a807163652a35be878c0ad5cfd8b1d29605edcadfb5df3c512cdf9f3/sqlalchemy-2.0.40-cp313-cp313-win32.whl", hash = "sha256:a669cbe5be3c63f75bcbee0b266779706f1a54bcb1000f302685b87d1b8c1500", size = 2080704 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1c/cb/f38c61f7f2fd4d10494c1c135ff6a6ddb63508d0b47bccccd93670637309/sqlalchemy-2.0.40-cp313-cp313-win_amd64.whl", hash = "sha256:641ee2e0834812d657862f3a7de95e0048bdcb6c55496f39c6fa3d435f6ac6ad", size = 2104564 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/7c/5fc8e802e7506fe8b55a03a2e1dab156eae205c91bee46305755e086d2e2/sqlalchemy-2.0.40-py3-none-any.whl", hash = "sha256:32587e2e1e359276957e6fe5dad089758bc042a971a8a09ae8ecf7a8fe23d07a", size = 1903894 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8c/18/4e3a86cc0232377bc48c373a9ba6a1b3fb79ba32dbb4eda0b357f5a2c59d/sqlalchemy-2.0.40-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:915866fd50dd868fdcc18d61d8258db1bf9ed7fbd6dfec960ba43365952f3b01", size = 2107887, upload_time = "2025-03-27T18:40:05.461Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/60/9fa692b1d2ffc4cbd5f47753731fd332afed30137115d862d6e9a1e962c7/sqlalchemy-2.0.40-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4a4c5a2905a9ccdc67a8963e24abd2f7afcd4348829412483695c59e0af9a705", size = 2098367, upload_time = "2025-03-27T18:40:07.182Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4c/9f/84b78357ca641714a439eb3fbbddb17297dacfa05d951dbf24f28d7b5c08/sqlalchemy-2.0.40-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55028d7a3ebdf7ace492fab9895cbc5270153f75442a0472d8516e03159ab364", size = 3184806, upload_time = "2025-03-27T18:51:29.356Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/7d/e06164161b6bfce04c01bfa01518a20cccbd4100d5c951e5a7422189191a/sqlalchemy-2.0.40-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6cfedff6878b0e0d1d0a50666a817ecd85051d12d56b43d9d425455e608b5ba0", size = 3198131, upload_time = "2025-03-27T18:50:31.616Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/51/354af20da42d7ec7b5c9de99edafbb7663a1d75686d1999ceb2c15811302/sqlalchemy-2.0.40-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bb19e30fdae77d357ce92192a3504579abe48a66877f476880238a962e5b96db", size = 3131364, upload_time = "2025-03-27T18:51:31.336Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7a/2f/48a41ff4e6e10549d83fcc551ab85c268bde7c03cf77afb36303c6594d11/sqlalchemy-2.0.40-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:16d325ea898f74b26ffcd1cf8c593b0beed8714f0317df2bed0d8d1de05a8f26", size = 3159482, upload_time = "2025-03-27T18:50:33.201Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/33/ac/e5e0a807163652a35be878c0ad5cfd8b1d29605edcadfb5df3c512cdf9f3/sqlalchemy-2.0.40-cp313-cp313-win32.whl", hash = "sha256:a669cbe5be3c63f75bcbee0b266779706f1a54bcb1000f302685b87d1b8c1500", size = 2080704, upload_time = "2025-03-27T18:46:00.193Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1c/cb/f38c61f7f2fd4d10494c1c135ff6a6ddb63508d0b47bccccd93670637309/sqlalchemy-2.0.40-cp313-cp313-win_amd64.whl", hash = "sha256:641ee2e0834812d657862f3a7de95e0048bdcb6c55496f39c6fa3d435f6ac6ad", size = 2104564, upload_time = "2025-03-27T18:46:01.442Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/7c/5fc8e802e7506fe8b55a03a2e1dab156eae205c91bee46305755e086d2e2/sqlalchemy-2.0.40-py3-none-any.whl", hash = "sha256:32587e2e1e359276957e6fe5dad089758bc042a971a8a09ae8ecf7a8fe23d07a", size = 1903894, upload_time = "2025-03-27T18:40:43.796Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "4.13.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967, upload_time = "2025-04-10T14:19:05.416Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806, upload_time = "2025-04-10T14:19:03.967Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "urllib3"
|
||||
version = "2.4.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/8a/78/16493d9c386d8e60e442a35feac5e00f0913c0f4b7c217c11e8ec2ff53e0/urllib3-2.4.0.tar.gz", hash = "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466", size = 390672 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/8a/78/16493d9c386d8e60e442a35feac5e00f0913c0f4b7c217c11e8ec2ff53e0/urllib3-2.4.0.tar.gz", hash = "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466", size = 390672, upload_time = "2025-04-10T15:23:39.232Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/6b/11/cc635220681e93a0183390e26485430ca2c7b5f9d33b15c74c2861cb8091/urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813", size = 128680 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6b/11/cc635220681e93a0183390e26485430ca2c7b5f9d33b15c74c2861cb8091/urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813", size = 128680, upload_time = "2025-04-10T15:23:37.377Z" },
|
||||
]
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user