166 Commits

Author SHA1 Message Date
Thomas Peetz 2cfaaa4d87 remove obsolete kontor.py
fixes #15
2025-04-30 17:28:09 +02:00
Thomas Peetz 5850b1cfb0 rename User to Profile and Role to Permission. refs #15 2025-04-30 17:22:40 +02:00
Thomas Peetz 8833d2ca6a add db-password.txt in preparation of switching to PostgreSQL 2025-04-29 14:48:31 +02:00
Thomas Peetz 1290a45fb5 kontor-api: Add details pages for Comics, Artists and MediaFiles 2025-04-28 15:47:07 +02:00
Thomas Peetz 3afbbf900d add life of MediaFile 2025-04-27 22:11:56 +02:00
tpeetz f71419d37b Merge branch 'feature/12-import-templates-from-bottle' into 'develop/0.1.0'
Resolve "import templates from  bottle"

Closes #12

See merge request tpeetz/kontor!16
2025-04-27 14:53:34 +02:00
Thomas Peetz f5f673fd4e add comic artist details page 2025-04-27 14:52:07 +02:00
Thomas Peetz 8ec0784490 import templates from bottle and django. Use bootstrap for HTML content 2025-04-27 14:14:56 +02:00
Thomas Peetz 13faff8e01 add dependency to mariadb for kontor and kontor-api 2025-04-26 22:57:52 +02:00
Thomas Peetz 0a4f6bb0f9 remove kontor-cli 2025-04-26 22:17:09 +02:00
Thomas Peetz 3b4e5634b1 remove scriptsa.bak 2025-04-26 00:40:00 +02:00
Thomas Peetz 8c98e6de26 kontor-spring: change IssueView to use StatusIcon 2025-04-25 16:32:31 +02:00
Thomas Peetz 525d82cbda remove conflict marker 2025-04-25 08:13:33 +02:00
Thomas Peetz e26aaa1420 fix deprecatino warning in build.gradle 2025-04-25 07:47:29 +02:00
tpeetz ec7f05d8fc Merge branch 'feature/10-improve-docker-image' into 'develop/0.1.0'
Feature/10 improve docker image

See merge request tpeetz/kontor!14
2025-04-25 01:21:16 +02:00
Thomas Peetz 727e95e732 setup kontor-schema 2025-04-25 01:19:59 +02:00
tpeetz 75d3bf6ffb Resolve "evaluate uv" 2025-04-25 01:19:59 +02:00
Thomas Peetz 94782fc468 relocate sources 2025-04-25 01:19:59 +02:00
tpeetz 101ece44d8 Resolve "evaluate uv" 2025-04-25 01:19:59 +02:00
Thomas Peetz e880594a5b change schema back to use BIT(1) for boolean 2025-04-25 01:19:59 +02:00
Thomas Peetz c222d4cd7a copy sources from branch develop/0.1.0 2025-04-25 01:19:59 +02:00
Thomas Peetz cb0fa3f728 change schema 2025-04-25 01:19:59 +02:00
Thomas Peetz 4709e431b7 add schema from kontor-schema 2025-04-25 01:19:59 +02:00
Thomas Peetz bfccca72a1 copy schema to kontor-api 2025-04-25 01:19:59 +02:00
Thomas Peetz 8a3eebaab5 move schema to separate uv project 2025-04-25 01:19:47 +02:00
Thomas Peetz 6716103d0c setup kontor-schema 2025-04-25 01:19:37 +02:00
Thomas Peetz ee78af1abe relocate sources 2025-04-25 01:19:28 +02:00
tpeetz 1fc726ee4b Resolve "evaluate uv" 2025-04-25 01:19:28 +02:00
Thomas Peetz 882f48de0b relocate sources 2025-04-25 01:19:14 +02:00
tpeetz 097a3efd4a Merge branch 'feature/11-evaluate-uv' into 'develop/0.1.0'
Resolve "evaluate uv"

Closes #11

See merge request tpeetz/kontor!15
2025-04-21 17:39:21 +02:00
tpeetz 934ef826c9 Resolve "evaluate uv" 2025-04-21 17:39:20 +02:00
Thomas Peetz bb049441b4 extend check_kontor.py for comparing json file with db 2025-04-21 00:55:50 +02:00
tpeetz 2bdbef8f8a change boolean fields in schema from Bit(1) to Boolean 2025-04-19 01:40:13 +02:00
tpeetz 20f29d88fa add script to copy from json to mariadb 2025-04-18 22:55:40 +02:00
Thomas Peetz 1997957069 remove .itlab-ci.yml and call of pytest to Makefile 2025-04-18 00:52:00 +02:00
Thomas Peetz 116bb77e4c update check_kontor.py to call Kontor API 2025-04-18 00:09:30 +02:00
Thomas Peetz a6eeea6c1f add testing to fastapi 2025-04-17 02:21:14 +02:00
Thomas Peetz 4a61d6a727 refactor scripts to work wit api 2025-04-16 05:08:59 +02:00
Thomas Peetz 98e3d91edd reorganize python projects 2025-04-15 01:29:08 +02:00
tpeetz a169f6a6c1 Merge branch 'feature/9-evaluate-sqlmodel' into 'develop/0.1.0'
evaluate sqlmodel

See merge request tpeetz/kontor!13
2025-04-13 16:16:10 +02:00
tpeetz b14a267b5b evaluate sqlmodel 2025-04-13 16:16:10 +02:00
Thomas Peetz a43e2c806c add virtualenv to scripts 2025-04-08 12:14:20 +02:00
Thomas Peetz 10834df92b add script for import data 2025-04-08 11:49:52 +02:00
Thomas Peetz 3d97c3360a add MetaDataColumn import 2025-04-07 22:26:33 +02:00
Thomas Peetz 76d91dd506 add import view 2025-04-07 19:34:08 +02:00
Thomas Peetz 84aff2b7d7 add data for cards 2025-04-06 09:41:53 +02:00
Thomas Peetz 2f8c10a692 add package repository and refactor classes 2025-04-03 22:31:11 +02:00
Thomas Peetz c5f74e007a organize imports 2025-04-01 23:13:08 +02:00
Thomas Peetz 243f693f9d ignore project files 2025-04-01 23:08:54 +02:00
Thomas Peetz 976b4f3f47 extract ColumnToggleContextMenu and createStatusIcon to separate classes 2025-04-01 22:09:41 +02:00
Thomas Peetz 1b18dae311 add scripts from repository python-scripts 2025-04-01 08:12:53 +02:00
Thomas Peetz 38e77b25b1 change display of boolean in ComicView and add docker tasks to build.gradle 2025-03-31 22:41:08 +02:00
tpeetz bbc08863f0 Merge branch 'feature/8-create-docker-build' into 'develop/0.1.0'
Resolve "Create docker build"

Closes #8

See merge request tpeetz/kontor!6
2025-03-31 18:40:40 +02:00
tpeetz d58f18d206 Merge branch 'develop/0.1.0' into 'feature/8-create-docker-build'
# Conflicts:
#   springboot/Dockerfile
2025-03-31 18:30:11 +02:00
Thomas Peetz fd5bc54eee import repositories kontor-java and kontor-ee 2025-03-30 15:57:09 +02:00
Thomas Peetz cbc57d22f8 add Docker build and domcker-compose file 2025-03-30 04:17:58 +02:00
Thomas Peetz a5cdf8867a move Git repository kontor-docker to directory bottle-docker 2025-03-29 19:05:31 +01:00
tpeetz 45971518ee change ModuleData view 2025-03-17 18:43:55 +01:00
Thomas Peetz 7db244f599 add length vor varchar for _id fields 2025-02-12 18:07:48 +01:00
Thomas Peetz 41e2b11da2 add details view 2025-02-11 16:16:38 +01:00
Thomas Peetz 0d1b2e416e add tab witg table and details 2025-02-11 10:39:07 +01:00
Thomas Peetz d8eecb4dab fix NPE in MediaActorView 2025-02-10 15:56:32 +01:00
Thomas Peetz 3bb3b22cea add MediaActor and MediaActorFile to kontor-schema 2025-02-10 12:54:59 +01:00
Thomas Peetz 8ffd421c1b add views for MediaActor and adapt MediaFile 2025-02-10 12:25:39 +01:00
Thomas Peetz 6d54c7f315 add MediaActor 2025-02-10 00:27:23 +01:00
Thomas Peetz f5fb743503 refactor kontor-schema 2025-02-08 20:22:21 +01:00
tpeetz ca9022d289 Merge branch 'table_sortable' into 'develop/0.1.0'
Table sortable

See merge request tpeetz/kontor!11
2025-02-06 15:59:29 +00:00
Thomas Peetz ed0e108599 refactor project by using enums for recurring strings 2025-02-06 16:48:41 +01:00
Thomas Peetz 7dc18b10cb refactor kontor-schema by moving classes to seprate modules 2025-02-06 16:44:14 +01:00
Thomas Peetz 171bc1676a refactor project by using enums for recurring strings 2025-02-06 16:33:15 +01:00
Thomas Peetz 1a5cd6ffe8 refactor kontor-schema by moving classes to seprate modules 2025-02-06 11:09:09 +01:00
Thomas Peetz 71ecfaff1f make tables sortable 2025-02-03 23:28:35 +01:00
Thomas Peetz f33aaadce7 fix problem with closing subwindows and define field length for id in schema 2025-02-03 17:37:19 +01:00
Thomas Peetz 591171b223 add meta data subwindow 2025-02-02 21:38:16 +01:00
Thomas Peetz da453d642d rename kontor.py to main.py for pyside6-deploy 2025-01-28 23:19:23 +01:00
Thomas Peetz e733fa21e6 moved update and download functionality to kontor-schema 2025-01-28 15:10:10 +01:00
Thomas Peetz c61e49720e update title 2025-01-27 22:42:32 +01:00
Thomas Peetz d01489b1fa reformat python file 2025-01-27 15:21:51 +01:00
Thomas Peetz 93c7498a83 setting return value of adding link to db 2025-01-27 15:20:35 +01:00
Thomas Peetz fe89cc6e0f add Mixin for videos 2025-01-27 13:34:15 +01:00
Thomas Peetz 1e9ca7c1a4 add Enum for Videotype 2025-01-25 03:53:47 +01:00
Thomas Peetz 400aff6524 Add MediaWindow 2025-01-24 08:58:59 +01:00
Thomas Peetz 845f085a76 remove data.json 2025-01-24 05:31:20 +01:00
Thomas Peetz 88c623edb7 subclass VideoFile 2025-01-24 05:02:08 +01:00
Thomas Peetz 0cc73c09aa remove pyproject.toml 2025-01-21 20:14:27 +01:00
Thomas Peetz e8660faa06 Refactor GUI to use Multi Document Interface 2025-01-21 20:12:50 +01:00
Thomas Peetz 4e884fdbe5 fix exporting and importing from file 2025-01-21 16:46:12 +01:00
Thomas Peetz 4c330a1138 remove kontor-cli.backup 2025-01-21 09:42:33 +01:00
Thomas Peetz 65288a53a1 complete MetaDataTable and MetaDataColumn 2025-01-20 15:40:45 +01:00
Thomas Peetz 60fba0d3e9 add dependencies to kontor-schema and kontor-video in rquirements.txt 2025-01-19 23:39:53 +01:00
Thomas Peetz ada723dc48 separate cli and gui application in own python packages. provide database schema as python package. 2025-01-19 23:36:52 +01:00
Thomas Peetz f07c7b74ee prepare progress bar for download 2025-01-19 17:49:13 +01:00
Thomas Peetz f3c59c11ba add progress in statusbar 2025-01-18 01:28:27 +01:00
Thomas Peetz 3c4d3ad326 add check files command 2025-01-17 17:19:31 +01:00
Thomas Peetz 917e287784 fix missing argument for KontorDB in import_cmd and export 2025-01-17 13:38:16 +01:00
Thomas Peetz 611d6b9e61 add update and download for MediaFile 2025-01-17 13:31:49 +01:00
Thomas Peetz 58263cb854 use sqlalchemy session only as short as possible 2025-01-16 23:03:30 +01:00
Thomas Peetz 33dcbc4413 add command to add link 2025-01-16 16:50:14 +01:00
Thomas Peetz f3cf1a17f3 add import functionality 2025-01-15 14:16:35 +01:00
Thomas Peetz 3466c10a88 add commands and subcommands 2025-01-14 22:58:15 +01:00
Thomas Peetz 4cea554116 add missing schema classes 2025-01-14 17:53:05 +01:00
tpeetz 332e09b283 Merge branch 'feature/5-add-token-management' into 'develop/0.1.0'
Resolve "Add token management"

Closes #5

See merge request tpeetz/kontor!3
2025-01-14 14:55:54 +00:00
tpeetz d3c720f995 Merge branch 'develop/0.1.0' into 'feature/5-add-token-management'
# Conflicts:
#   qt/gui/main_window.py
2025-01-14 14:44:46 +00:00
Thomas Peetz 453ad2af7b merged command line and gui app in one command 2025-01-14 15:39:07 +01:00
Thomas Peetz 54bc17ee7d add cli app and fix relationship typos 2025-01-14 15:39:06 +01:00
Thomas Peetz 89b7b87b8c add export to json 2025-01-14 15:39:03 +01:00
Thomas Peetz 3f0a37ff19 create files with abstract model class 2025-01-14 15:38:30 +01:00
Thomas Peetz 954dab289a extend MetaDataColumn by adding column for name of column for referenced tables 2025-01-14 15:38:30 +01:00
Thomas Peetz 5affa16505 add tysc schema 2025-01-14 15:38:30 +01:00
Thomas Peetz d2b1cb999a reorganize files for Qt application 2025-01-14 15:38:27 +01:00
Thomas Peetz f1d36acff7 add sqlalchemy as orm tool 2025-01-14 15:35:53 +01:00
Thomas Peetz 7e8a6b3c91 add repo kontor-quarkus 2025-01-14 15:35:53 +01:00
Thomas Peetz b424e95e05 import other kontor repos into directories 2025-01-14 15:35:53 +01:00
Thomas Peetz 28746adfbb import from kontor-flask branch develop/0.1.0 2025-01-14 15:35:53 +01:00
Thomas Peetz 0d2f27f771 import from kontor-flask 2025-01-14 15:35:53 +01:00
Thomas Peetz d6410e2584 implement generic table model
implement generic table model which reads table info from db and
constructs table view
2025-01-14 15:21:03 +01:00
Thomas Peetz ce1514f20a fix errors when displaying empty table 2025-01-14 15:18:49 +01:00
Thomas Peetz c8ea7c4188 refresh table when updated 2025-01-14 15:18:46 +01:00
Thomas Peetz 245ee2378e display tick and cross for boolean values 2025-01-14 15:17:41 +01:00
Thomas Peetz afb3ac88c8 first implementation to show Comics and MediaFiles 2025-01-14 15:17:37 +01:00
Thomas Peetz 3a0a0055a6 Add Taskjuggler project file 2025-01-14 15:13:59 +01:00
tpeetz 283577df9b Merge branch 'add-cli-app' into 'develop/0.1.0'
Add cli app

See merge request tpeetz/kontor!9
2025-01-14 12:22:53 +00:00
Thomas Peetz 276302570f merged command line and gui app in one command 2025-01-14 13:10:24 +01:00
Thomas Peetz f74c07af9a add cli app and fix relationship typos 2025-01-13 22:54:25 +01:00
Thomas Peetz d0eae1980a add export to json 2025-01-13 16:18:13 +01:00
Thomas Peetz 820ae3d374 create files with abstract model class 2025-01-13 00:26:42 +01:00
Thomas Peetz bf14c9a020 extend MetaDataColumn by adding column for name of column for referenced tables 2025-01-12 02:41:40 +01:00
Thomas Peetz a3652cc9b8 add tysc schema 2025-01-11 19:00:58 +01:00
Thomas Peetz 8f2e99195a reorganize files for Qt application 2025-01-11 01:54:05 +01:00
Thomas Peetz 2ae11e24ef add sqlalchemy as orm tool 2025-01-10 17:39:54 +01:00
Thomas Peetz a1e8474149 add repo kontor-quarkus 2025-01-09 22:28:46 +01:00
Thomas Peetz 9b329c0ac4 import other kontor repos into directories 2025-01-09 19:28:50 +01:00
tpeetz 8120c92b32 Merge branch 'feature/4-import-kontor-flask-into-directory-flask' into 'develop/0.1.0'
Import kontor-flask into directory flask

Closes #4

See merge request tpeetz/kontor!2
2025-01-08 21:44:47 +00:00
Thomas Peetz 5c261529fc import from kontor-flask branch develop/0.1.0 2025-01-08 22:34:21 +01:00
Thomas Peetz 6923b3e5ee import from kontor-flask 2025-01-08 22:31:20 +01:00
Thomas Peetz fc4110b11d fix problem in string formatting 2025-01-08 22:25:44 +01:00
tpeetz e078f43cc6 set correct project id in build.gradle 2025-01-08 22:25:44 +01:00
Thomas Peetz 24e4c0b58e import export dialog by adding selection of export type 2025-01-08 22:25:44 +01:00
Thomas Peetz 8d31a92692 improve view of meta data by using icons for boolean and add search 2025-01-08 22:25:44 +01:00
Thomas Peetz c619745a4a prevent NPE 2025-01-08 22:25:44 +01:00
Thomas Peetz c769115331 implement general data tab view 2025-01-08 22:25:44 +01:00
Thomas Peetz 1a7da0ab9f add column header info 2025-01-08 22:25:44 +01:00
tpeetz 78632e0e12 Import kontor-spring into directory springboot 2025-01-08 22:25:44 +01:00
Thomas Peetz 3aed8af868 implement generic table model
implement generic table model which reads table info from db and
constructs table view
2025-01-08 22:25:44 +01:00
Thomas Peetz c6d1e4d7e7 fix errors when displaying empty table 2025-01-08 22:25:44 +01:00
Thomas Peetz d98dc79cf8 refresh table when updated 2025-01-08 22:25:44 +01:00
Thomas Peetz 9dcc09c586 display tick and cross for boolean values 2025-01-08 22:25:44 +01:00
Thomas Peetz 57e7b9e999 first implementation to show Comics and MediaFiles 2025-01-08 22:25:44 +01:00
Thomas Peetz d97145b629 add first implementation of Dockerfile 2025-01-08 21:57:13 +01:00
Thomas Peetz 8ba23b6365 add first example of a Dockerfile for a Spring Boot Application 2025-01-08 21:41:20 +01:00
Thomas Peetz 66d61e2c1f add entity token 2025-01-08 21:08:15 +01:00
Thomas Peetz 83f645d4c6 fix problem in string formatting 2025-01-08 15:34:11 +01:00
tpeetz 7d84e341d6 set correct project id in build.gradle 2025-01-08 14:28:18 +00:00
Thomas Peetz c3ec0977c0 import export dialog by adding selection of export type 2025-01-08 14:15:29 +01:00
Thomas Peetz d51bc47ffe improve view of meta data by using icons for boolean and add search 2025-01-08 10:39:52 +01:00
Thomas Peetz faa1d73eea prevent NPE 2025-01-08 08:05:09 +01:00
Thomas Peetz fe407d6d89 implement general data tab view 2025-01-07 21:18:14 +01:00
Thomas Peetz 0759c99c1d add column header info 2025-01-07 01:48:03 +01:00
tpeetz 77bb86a9fa Merge branch 'feature/2-import-kontor-spring-into-directory-springboot' into 'develop/0.1.0'
Import kontor-spring into directory springboot

Closes #2

See merge request tpeetz/kontor!1
2025-01-06 17:09:38 +00:00
tpeetz 4d7d7391c3 Import kontor-spring into directory springboot 2025-01-06 17:09:38 +00:00
Thomas Peetz c5aa3b32ef implement generic table model
implement generic table model which reads table info from db and
constructs table view
2025-01-06 17:07:20 +01:00
Thomas Peetz 4ff999a74e fix errors when displaying empty table 2025-01-05 23:44:29 +01:00
Thomas Peetz bdf2b2ba43 refresh table when updated 2025-01-05 21:47:27 +01:00
Thomas Peetz 04dfe483e4 display tick and cross for boolean values 2025-01-05 14:13:15 +01:00
Thomas Peetz 4a279738e3 first implementation to show Comics and MediaFiles 2025-01-05 14:10:15 +01:00
Thomas Peetz 3f65ec55fc Add Taskjuggler project file 2024-12-05 20:40:13 +01:00
236 changed files with 4661 additions and 6083 deletions
-5
View File
@@ -13,9 +13,4 @@ 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
+2 -3
View File
@@ -1,10 +1,9 @@
kontor_api := kontor-api
kontor_spring := kontor-spring
kontor_servicemix := kontor-servicemix
.PHONY: all $(kontor_spring) $(kontor_api)
all: $(kontor_spring) $(kontor_api) $(kontor_servicemix)
all: $(kontor_spring) $(kontor_api)
$(kontor_spring) $(kontor_api) $(kontor_servicemix):
$(kontor_spring) $(kontor_api):
$(MAKE) --directory=$@ $(TARGET)
+16 -73
View File
@@ -1,101 +1,44 @@
services:
postgres:
image: postgres
mariadb:
image: mariadb
restart: unless-stopped
environment:
- 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
MYSQL_ROOT_PASSWORD: kontor
MYSQL_USER: kontor
MYSQL_PASSWORD: kontor
MYSQL_DATABASE: kontor
ports:
- 5432:5432
- 3316:3306
networks:
- database
volumes:
- 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
- mariadb-storage:/var/lib/mysql:rw
kontor:
image: kontor:0.2.0-SNAPSHOT
image: kontor
restart: unless-stopped
networks:
- database
- integration
- frontend
ports:
- 8000:8000
volumes:
- images-data:/data/images
depends_on:
postgres:
condition: service_healthy
- mariadb
kontor-api:
image: kontor-api:0.2.0-SNAPSHOT
image: kontor-api
restart: unless-stopped
networks:
- database
- integration
- frontend
ports:
- 8800:8800
volumes:
- images-data:/data/images
depends_on:
postgres:
condition: service_healthy
- mariadb
networks:
database:
integration:
name: integration
frontend:
volumes:
postgres-data:
couchdb-data:
activemq-data:
images-data:
secrets:
db-password:
file: db-password.txt
couchdb-password:
file: couchdb-password.txt
volumes:
mariadb-storage:
Binary file not shown.
-3
View File
@@ -1,4 +1 @@
.env
.coverage
app.log
+3 -1
View File
@@ -1,7 +1,8 @@
## ------------------------------- Builder Stage ------------------------------ ##
FROM python:3.13-bookworm AS builder
RUN apt-get update && apt-get install --no-install-recommends -y build-essential && \
RUN apt-get update && apt-get install --no-install-recommends -y \
build-essential libmariadb-dev && \
apt-get clean && rm -rf /var/lib/apt/lists/*
# Download the latest installer, install it and then remove it
@@ -41,6 +42,7 @@ 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
View File
@@ -4,11 +4,11 @@ clean:
find . -name '*.py[co]' -delete
test:
DB_SERVER=localhost uv run pytest -v --cov --cov-report=term --cov-report=html:coverage-report
DB_HOST=localhost uv run pytest -v --cov --cov-report=term --cov-report=html:coverage-report
docker: clean
docker build --target=production -t kontor-api:0.2.0-SNAPSHOT .
docker build --target=production -t kontor-api -t kontor-api:0.1.0-SNAPSHOT .
dev:
DB_SERVER=127.0.0.1 uv run fastapi dev src/main.py --port 8008
MARIADB_SERVER=localhost uv run fastapi dev src/main.py --port 8008
+1 -7
View File
@@ -8,6 +8,7 @@ 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",
@@ -21,11 +22,4 @@ 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 -2
View File
@@ -1,9 +1,8 @@
from fastapi import APIRouter
from src.apis.version1 import comic, media, tysc, admin
from src.apis.version1 import comic, media, tysc
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"])
-40
View File
@@ -1,13 +1,4 @@
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
@@ -15,34 +6,3 @@ from sqlalchemy.orm import Session
from src.db.session import get_db
SessionDep = Annotated[Session, Depends(get_db)]
class OAuth2PasswordBearerWithCookie(OAuth2):
def __init__(
self,
tokenUrl: str,
scheme_name: Optional[str] = None,
scopes: Optional[Dict[str, str]] = None,
auto_error: bool = True,
):
if not scopes:
scopes = {}
flows = OAuthFlowsModel(password={"tokenUrl": tokenUrl, "scopes": scopes})
super().__init__(flows=flows, scheme_name=scheme_name, auto_error=auto_error)
async def __call__(self, request: Request) -> Optional[str]:
authorization: str = request.cookies.get(
"access_token"
) # changed to accept access token from httpOnly Cookie
scheme, param = get_authorization_scheme_param(authorization)
if not authorization or scheme.lower() != "bearer":
if self.auto_error:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Not authenticated",
headers={"WWW-Authenticate": "Bearer"},
)
else:
return None
return param
-69
View File
@@ -1,69 +0,0 @@
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
+13 -18
View File
@@ -1,28 +1,31 @@
from typing import List, AnyStr
from uuid import UUID
from typing import List
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
from src.db.models.comic import Comic, Artist, Issue
from src.schema.comics.issue import IssueDetailsResponse
from src.schema.comics.artist import ArtistCreation, ArtistDetailResponse, ArtistResponse, get_artist_details
from src.db.models.comic import Comic, Artist
router = APIRouter()
router = APIRouter(
prefix="/comic",
tags=["comics"],
responses={404: {"description": "Not found"}},
)
@router.get("/comics")
def get_all_comics(db: SessionDep) -> List[ComicResponse]:
results: List[ComicResponse] = []
comics = list_comics(db)
comics = db.scalars(select(Comic)).all()
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: AnyStr, db: SessionDep) -> ComicDetailsResponse:
def get_comic(comic_id: UUID, db: SessionDep) -> ComicDetailsResponse:
comic = db.get(Comic, comic_id)
if comic is None:
raise HTTPException(status_code=404, detail="Comic could not be found")
@@ -38,7 +41,7 @@ def get_all_artists(db: SessionDep) -> List[ArtistResponse]:
return results
@router.get("/artists/{artist_id}", response_model=ArtistDetailResponse)
def get_artist(artist_id: AnyStr, db: SessionDep) -> ArtistDetailResponse:
def get_artist(artist_id: UUID, db: SessionDep) -> ArtistDetailResponse:
artist = db.get(Artist, artist_id)
if artist is None:
raise HTTPException(status_code=404, detail="Artist could not be found")
@@ -57,11 +60,3 @@ 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
+20 -13
View File
@@ -1,19 +1,22 @@
from typing import List, AnyStr
from typing import List
from uuid import UUID
from fastapi import APIRouter, status, HTTPException, Depends
from fastapi import APIRouter, status, HTTPException
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()
router = APIRouter(
prefix="/media",
tags=["media"]
)
@router.get("/update-titles")
def update_titles(db: SessionDep) -> list[MediaFileResponse]:
results: list[MediaFileResponse] = []
files = db.query(MediaFile).filter(MediaFile.review == True).all()
files = db.query(MediaFile).filter(MediaFile.review == 1).all()
for mediafile in files:
mediafile.update_title()
db.add(mediafile)
@@ -24,14 +27,13 @@ def update_titles(db: SessionDep) -> list[MediaFileResponse]:
@router.get("/files", response_model=List[MediaFileResponse])
#def get_all_files(db: SessionDep, review: bool = False, download: bool = False, 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 == True).all()
files = db.query(MediaFile).filter(MediaFile.review == 1).all()
elif download:
files = db.query(MediaFile).filter(MediaFile.should_download == True).all()
files = db.query(MediaFile).filter(MediaFile.should_download == 1).all()
else:
files = db.scalars(select(MediaFile)).all()
for mediafile in files:
@@ -40,7 +42,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: AnyStr, db: SessionDep) -> MediaFileResponse:
def get_file(file_id: UUID, db: SessionDep) -> MediaFileResponse:
mediafile = db.get(MediaFile, file_id)
if not mediafile:
raise HTTPException(status_code=404, detail="MediaFile could not be found")
@@ -48,7 +50,7 @@ def get_file(file_id: AnyStr, db: SessionDep) -> MediaFileResponse:
return response
@router.put("/files/{file_id}", response_model=MediaFileResponse)
def update_file(file_id: AnyStr, db: SessionDep, info: MediaFileResponse) -> MediaFileResponse:
def update_file(file_id: UUID, 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")
@@ -60,9 +62,14 @@ def update_file(file_id: AnyStr, db: SessionDep, info: MediaFileResponse) -> Med
@router.post("/files", status_code=status.HTTP_201_CREATED)
def add_file(new_link: Link, db: SessionDep) -> MediaFileResponse:
logger.info(f"add url {new_link.url}")
print(new_link.url)
try:
mediaFile: MediaFile = create_new_mediafile(new_link.url, db)
mediaFile: MediaFile = MediaFile()
setattr(mediaFile, "url", new_link.url)
setattr(mediaFile, "review", 1)
setattr(mediaFile, "should_download", 1)
db.add(mediaFile)
db.commit()
except:
raise HTTPException(status_code=409, detail="Link duplicate")
response = get_file_details(mediaFile)
+5 -1
View File
@@ -5,7 +5,11 @@ from src.apis.utils import SessionDep
from src.schema.tysc.sport import SportResponse
from src.db.models.tysc import Sport
router = APIRouter()
router = APIRouter(
prefix="/tysc",
tags=["tysc"],
responses={404: {"description": "Not found"}},
)
@router.get("/sports")
def get_all_sports(db: SessionDep) -> List[SportResponse]:
+8 -10
View File
@@ -9,17 +9,15 @@ load_dotenv(dotenv_path=env_path)
class Settings:
PROJECT_NAME: str = "Kontor"
PROJECT_VERSION: str = "0.2.0"
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}"
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()
-44
View File
@@ -1,44 +0,0 @@
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__)
-21
View File
@@ -1,21 +0,0 @@
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
+24 -20
View File
@@ -1,6 +1,7 @@
from datetime import datetime
from sqlalchemy import Column, ForeignKey, Integer, String, Boolean
from sqlalchemy import Column, ForeignKey, Integer, String
from sqlalchemy.dialects.mysql import BIT
from sqlalchemy.orm import relationship, mapped_column, Mapped
from src.db.models.base import Base, BaseMixin
@@ -8,12 +9,12 @@ from src.db.models.base import Base, BaseMixin
class Profile(Base, BaseMixin):
__tablename__ = 'profile'
first_name = Column(String)
last_name = Column(String)
user_name = Column(String, nullable=False)
email = Column(String)
password = Column(String)
enabled = Column(Boolean)
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))
assignments = relationship("Assignment")
tokens = relationship("Token")
@@ -27,23 +28,20 @@ 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, nullable=False, unique=True)
name = Column(String)
token = Column(String(255), nullable=False, unique=True)
name = Column(String(255))
last_used_date: Mapped[datetime] = mapped_column()
enabled = Column(Boolean)
profile_id = Column(String, ForeignKey("profile.id"), nullable=False)
enabled = Column(BIT(1))
profile_id = Column(String(255), ForeignKey("profile.id"), nullable=False)
profile = relationship("Profile", back_populates="tokens")
class Permission(Base, BaseMixin):
__tablename__ = "permission"
name = Column(String, nullable=False)
name = Column(String(255), nullable=False)
assignments = relationship("Assignment")
@@ -55,14 +53,20 @@ 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)
host = Column(String(255))
port = Column(Integer)
protocol = Column(String)
user_name = Column(String)
password = Column(String)
start_tls = Column(Boolean)
protocol = Column(String(255))
user_name = Column(String(255))
password = Column(String(255))
start_tls = Column(BIT(1))
class Mail(Base, BaseMixin):
+11 -10
View File
@@ -1,7 +1,8 @@
import uuid
from datetime import datetime
from sqlalchemy import func, Column, String, Boolean
from sqlalchemy import func, Column, String
from sqlalchemy.dialects.mysql import BIT
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
@@ -10,8 +11,8 @@ class Base(DeclarativeBase):
class BaseMixin:
#id = Column(String, primary_key=True, default=uuid.uuid4)
id: Mapped[str] = mapped_column(primary_key=True, default=str(uuid.uuid4()))
id = Column(String(255), primary_key=True, default=uuid.uuid4())
# id: Mapped[str] = mapped_column(primary_key=True, default=uuid.uuid4())
# created_date = Column(DateTime)
created_date: Mapped[datetime] = mapped_column(default=func.now())
# last_modified_date = Column(DateTime)
@@ -21,10 +22,10 @@ class BaseMixin:
class BaseVideoMixin:
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)
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))
+6 -6
View File
@@ -6,28 +6,28 @@ from src.db.models.base import Base, BaseMixin
class Article(Base, BaseMixin):
__tablename__ = 'article'
title = Column(String, unique=True)
title = Column(String(length=255), unique=True)
article_authors = relationship("ArticleAuthor")
class Author(Base, BaseMixin):
__tablename__ = 'author'
first_name = Column(String)
last_name = Column(String)
first_name = Column(String(255))
last_name = Column(String(255))
article_authors = relationship("ArticleAuthor")
book_authors = relationship("BookAuthor")
class BookshelfPublisher(Base, BaseMixin):
__tablename__ = 'bookshelf_publisher'
name = Column(String, unique=True)
name = Column(String(length=255), unique=True)
books = relationship("Book")
class Book(Base, BaseMixin):
__tablename__ = 'book'
isbn = Column(String, unique=True)
title = Column(String)
isbn = Column(String(255), unique=True)
title = Column(String(255))
year = Column(Integer, nullable=False)
publisher_id = Column(String, ForeignKey('bookshelf_publisher.id'), nullable=False)
publisher = relationship('BookshelfPublisher', back_populates="books")
+23 -84
View File
@@ -1,24 +1,15 @@
import uuid
from datetime import datetime
from typing import AnyStr, Dict, List, Optional, Any
from typing import Dict, List
from natsort import natsorted
from sqlalchemy import Column, ForeignKey, Integer, String, Boolean, func
from sqlalchemy.orm import relationship, Mapped, mapped_column
from sqlalchemy import Column, ForeignKey, Integer, String
from sqlalchemy.dialects.mysql import BIT
from sqlalchemy.orm import relationship
from src.db.models.base import Base, BaseMixin
class Publisher(Base):
class Publisher(Base, BaseMixin):
__tablename__ = "publisher"
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")
name = Column(String(length=255), unique=True)
comics = relationship("Comic")
def __repr__(self):
@@ -30,12 +21,11 @@ class Publisher(Base):
class Comic(Base, BaseMixin):
__tablename__ = 'comic'
title = Column(String, unique=True)
title = Column(String(length=255), unique=True)
publisher_id = Column(String, ForeignKey('publisher.id'), nullable=False)
publisher = relationship("Publisher", back_populates="comics")
current_order = Column(Boolean)
completed = Column(Boolean)
weblink = Column(String, nullable=True)
current_order = Column(BIT(1))
completed = Column(BIT(1))
issues = relationship("Issue", order_by="Issue.issue_number")
story_arcs = relationship("StoryArc")
trade_paperbacks = relationship("TradePaperback")
@@ -48,10 +38,10 @@ class Comic(Base, BaseMixin):
def __str__(self):
return f'{self.title}({self.id})'
def get_artists(self) -> Dict[Any, List[Any]]:
works: Dict[Any, List[Any]] = {}
def get_artists(self) -> Dict[str, List[str]]:
works: Dict[str, List[str]] = {}
for work in self.comic_works:
work_type = work.work_type
work_type = work.work_type.name
artist = work.artist
if work_type in works:
works[work_type].append(artist)
@@ -66,16 +56,15 @@ class Comic(Base, BaseMixin):
class Volume(Base, BaseMixin):
__tablename__ = "volume"
name = Column(String, nullable=False)
name = Column(String(length=255), 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, nullable=False)
name = Column(String(length=255), nullable=False)
issue_start = Column(Integer)
issue_end = Column(Integer)
comic_id = Column(String, ForeignKey("comic.id"), nullable=False)
@@ -84,58 +73,31 @@ class TradePaperback(Base, BaseMixin):
class StoryArc(Base, BaseMixin):
__tablename__ = "story_arc"
name = Column(String, nullable=False)
name = Column(String(length=255), 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)
title = Column(String, nullable=True)
published_on: Mapped[datetime] = mapped_column(nullable=True)
in_stock = Column(Boolean)
is_read = Column(Boolean)
issue_number = Column(String(255))
in_stock = Column(BIT(1))
is_read = Column(BIT(1))
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, nullable=False)
weblink = Column(String, nullable=True)
name = Column(String(length=255), nullable=False)
comic_works = relationship("ComicWork")
issue_works = relationship("IssueWork")
def get_comics(self) -> Dict[Any, List[Comic]]:
works: Dict[Any, List[Comic]] = {}
def get_comics(self) -> Dict[str, List[str]]:
works: Dict[str, List[str]] = {}
for work in self.comic_works:
work_type = work.work_type
work_type = work.work_type.name
comic = work.comic
if work_type in works:
works[work_type].append(comic)
@@ -146,20 +108,8 @@ class Artist(Base, BaseMixin):
class WorkType(Base, BaseMixin):
__tablename__ = "worktype"
name = Column(String, nullable=False, unique=True)
name = Column(String(length=255), 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)})'
@@ -176,14 +126,3 @@ 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")
+7 -6
View File
@@ -1,9 +1,10 @@
import json
import logging
import uuid
from datetime import datetime
from enum import Enum, auto
from pathlib import Path
from typing import Any, List
from typing import Any
from sqlalchemy import select
from sqlalchemy.exc import IntegrityError
@@ -12,7 +13,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, Token, Assignment, Permission, Profile
from src.db.models.admin import Mail, MailAccount, ModuleData, Role, User, Token, AuthorizationMatrix
from src.db.models.metadata import MetaDataTable, MetaDataColumn
from src.db.models.media import MediaVideo, MediaArticle, MediaFile, MediaActor, MediaActorFile
@@ -78,10 +79,10 @@ class KontorDB:
self.registry[MediaVideo.__tablename__] = MediaVideo
self.registry[MetaDataColumn.__tablename__] = MetaDataColumn
self.registry[MetaDataTable.__tablename__] = MetaDataTable
self.registry[Assignment.__tablename__] = Assignment
self.registry[AuthorizationMatrix.__tablename__] = AuthorizationMatrix
self.registry[Token.__tablename__] = Token
self.registry[Profile.__tablename__] = Profile
self.registry[Permission.__tablename__] = Permission
self.registry[User.__tablename__] = User
self.registry[Role.__tablename__] = Role
self.registry[ModuleData.__tablename__] = ModuleData
self.registry[MailAccount.__tablename__] = MailAccount
self.registry[Mail.__tablename__] = Mail
@@ -359,7 +360,7 @@ class KontorDB:
update_list[link.id] = link.title
return update_list
def get_download_list(self) -> List[str]:
def get_download_list(self) -> list[uuid.UUID]:
download_list = []
__session__ = sessionmaker(self.engine)
_filter = { 'should_download': True}
+20 -30
View File
@@ -6,7 +6,8 @@ from pathlib import Path
import requests
from bs4 import BeautifulSoup
from sqlalchemy import Column, String, ForeignKey, Boolean
from sqlalchemy import Column, String, ForeignKey
from sqlalchemy.dialects.mysql import BIT
from sqlalchemy.orm import relationship
from src.db.models.base import Base, BaseMixin, BaseVideoMixin
@@ -29,10 +30,10 @@ class MediaFile(Base, BaseMixin, BaseVideoMixin):
soup = BeautifulSoup(r.content, "html.parser")
title = soup.title.string
self.title = title
self.review = False
self.review = 0
except:
self.title = None
self.review = True
self.review = 1
self.last_modified_date = datetime.now()
def download_file(self, download_dir: str, dl_tool: str):
@@ -44,12 +45,12 @@ class MediaFile(Base, BaseMixin, BaseVideoMixin):
lines_list = output.splitlines()
file_name = self.__parse_output__(lines_list)
if file_name is None:
self.review = True
self.should_download = True
self.review = 1
self.should_download = 1
self.file_name = None
else:
download_file = Path(file_name)
self.should_download = False
self.should_download = 0
self.file_name = download_file.name
self.cloud_link = str(download_file.absolute())
self.last_modified_date = datetime.now()
@@ -70,42 +71,31 @@ class MediaFile(Base, BaseMixin, BaseVideoMixin):
class MediaActor(Base, BaseMixin):
__tablename__ = 'media_actor'
name = Column(String)
name = Column(String(255))
media_actor_files = relationship("MediaActorFile")
class MediaActorFile(Base, BaseMixin):
__tablename__ = 'media_actor_file'
media_actor_id = Column(String, ForeignKey("media_actor.id"), nullable=False)
media_actor_id = Column(String(255), ForeignKey("media_actor.id"), nullable=False)
media_actor = relationship("MediaActor", back_populates="media_actor_files")
media_file_id = Column(String, ForeignKey("media_file.id"), nullable=True)
media_file_id = Column(String(255), ForeignKey("media_file.id"), nullable=True)
media_file = relationship("MediaFile", back_populates="media_actor_files")
class MediaArticle(Base, BaseMixin):
__tablename__ = 'media_article'
review = Column(Boolean)
title = Column(String)
url = Column(String, unique=True)
review = Column(BIT(1))
title = Column(String(255))
url = Column(String(255), unique=True)
class MediaVideo(Base, BaseMixin):
__tablename__ = 'media_video'
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})'
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))
+42
View File
@@ -0,0 +1,42 @@
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})'
+13 -12
View File
@@ -1,4 +1,5 @@
from sqlalchemy import Column, Integer, String, ForeignKey, UniqueConstraint, Boolean
from sqlalchemy import Column, Integer, String, ForeignKey, UniqueConstraint
from sqlalchemy.dialects.mysql import BIT
from sqlalchemy.orm import relationship
from src.db.models.base import Base, BaseMixin
@@ -9,15 +10,15 @@ class Sport(Base, BaseMixin):
__table_args__ = (
UniqueConstraint("name"),
)
name = Column(String, nullable=False, index=True, unique=True)
name = Column(String(255), nullable=False, index=True, unique=True)
teams = relationship("Team")
positions = relationship("FieldPosition")
class Team(Base, BaseMixin):
__tablename__ = "team"
name = Column(String, nullable=False, index=True, unique=True)
short_name = Column(String, nullable=False, )
name = Column(String(255), nullable=False, index=True, unique=True)
short_name = Column(String(255), nullable=False, )
sport_id = Column(String, ForeignKey("sport.id"), nullable=False)
sport = relationship("Sport", back_populates="teams")
roosters = relationship("Rooster")
@@ -29,8 +30,8 @@ class FieldPosition(Base, BaseMixin):
UniqueConstraint("name", "sport_id"),
UniqueConstraint("short_name", "sport_id"),
)
name = Column(String, nullable=False, index=True)
short_name = Column(String, nullable=False)
name = Column(String(255), nullable=False, index=True)
short_name = Column(String(255), nullable=False)
sport_id = Column(String, ForeignKey("sport.id"), nullable=False, index=True)
sport = relationship("Sport", back_populates="positions")
roosters = relationship("Rooster")
@@ -41,8 +42,8 @@ class Player(Base, BaseMixin):
__table_args__ = (
UniqueConstraint("first_name", "last_name"),
)
first_name = Column(String, nullable=False, index=True)
last_name = Column(String, nullable=False, index=True)
first_name = Column(String(255), nullable=False, index=True)
last_name = Column(String(255), nullable=False, index=True)
roosters = relationship("Rooster")
def get_full_name(self) -> str:
@@ -66,7 +67,7 @@ class Rooster(Base, BaseMixin):
class Vendor(Base, BaseMixin):
__tablename__ = "vendor"
name = Column(String, nullable=False, unique=True, index=True)
name = Column(String(255), nullable=False, unique=True, index=True)
card_sets = relationship("CardSet")
cards = relationship("Card")
@@ -76,9 +77,9 @@ class CardSet(Base, BaseMixin):
__table_args__ = (
UniqueConstraint("name", "vendor_id"),
)
name = Column(String, index=True)
parallel_set = Column(Boolean)
insert_set = Column(Boolean)
name = Column(String(255), index=True)
parallel_set = Column(BIT(1))
insert_set = Column(BIT(1))
vendor_id = Column(String, ForeignKey("vendor.id"), nullable=False, index=True)
vendor = relationship("Vendor", back_populates="card_sets")
cards = relationship("Card")
-10
View File
@@ -1,10 +0,0 @@
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
@@ -1,19 +0,0 @@
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
@@ -1,31 +0,0 @@
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
@@ -1,32 +0,0 @@
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
-40
View File
@@ -1,40 +0,0 @@
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
+2 -2
View File
@@ -1,7 +1,7 @@
from typing import Generator
from typing import Generator, Annotated
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from sqlalchemy.orm import sessionmaker, Session
from src.core.config import settings
-28
View File
@@ -1,28 +0,0 @@
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
+5 -17
View File
@@ -1,25 +1,12 @@
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)
@@ -30,12 +17,13 @@ def configure_static(app: FastAPI):
def create_tables():
Base.metadata.create_all(bind=engine)
def start_application(log):
log.info(f"using database: {settings.DATABASE_URL}")
app = FastAPI(title=settings.PROJECT_NAME, version=settings.PROJECT_VERSION, lifespan=lifespan)
def start_application():
app = FastAPI(title=settings.PROJECT_NAME, version=settings.PROJECT_VERSION)
include_router(app)
configure_static(app)
create_tables()
return app
kontor = start_application(logger)
kontor = start_application()
-8
View File
@@ -1,8 +0,0 @@
from typing import Optional
from pydantic import BaseModel
class Token(BaseModel):
access_token: str
token_type: str
+21 -4
View File
@@ -1,19 +1,36 @@
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: str
id: UUID
name: str
class ArtistDetailResponse(BaseModel):
id: str
id: UUID
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
+8 -17
View File
@@ -1,35 +1,26 @@
from typing import List, Dict, Optional
from typing import List, Dict
from uuid import UUID
from pydantic import BaseModel, AnyUrl
from pydantic import BaseModel
from src.db.models.comic import Comic
class ComicResponse(BaseModel):
id: str
id: UUID
title: str
completed: bool
class ComicDetailsResponse(BaseModel):
id: str
id: UUID
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,
@@ -55,11 +46,11 @@ def get_comic_details(comic: Comic) -> ComicDetailsResponse | None:
id=comic.id,
created=str(comic.created_date),
title=comic.title,
completed=comic.completed,
current_order=comic.current_order,
weblink=comic.weblink,
completed=(comic.completed == 1),
current_order=(comic.current_order == 1),
publisher=comic.publisher.name,
volumes=volumes,
works=works
)
return response
-10
View File
@@ -1,10 +0,0 @@
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
-4
View File
@@ -1,4 +0,0 @@
from pydantic import BaseModel
class AddWorkType(BaseModel):
worktype: str
+12 -5
View File
@@ -1,11 +1,12 @@
from datetime import datetime
from uuid import UUID
from src.db.models.media import MediaFile
from pydantic import BaseModel
class MediaFileResponse(BaseModel):
id: str
id: UUID
title: str | None = None
file_name: str | None = None
cloud_link: str | None = None
@@ -22,8 +23,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,
should_download=mediafile.should_download)
review=(mediafile.review == 1),
should_download=(mediafile.should_download == 1))
#print(f"id: {mediafile.id}: review: {response.review} <- {mediafile.review}")
#print(f"id: {mediafile.id}: download: {response.should_download} <- {mediafile.should_download}")
return response
@@ -34,5 +35,11 @@ def set_file(model: MediaFileResponse, mediafile: MediaFile) -> None:
mediafile.url = model.url
mediafile.title = model.title
mediafile.last_modified_date = datetime.now()
mediafile.review = model.review
mediafile.should_download = model.should_download
if model.review:
mediafile.review = 1
else:
mediafile.review = 0
if model.should_download:
mediafile.should_download = 1
else:
mediafile.should_download = 0
-5
View File
@@ -1,5 +0,0 @@
from pydantic import BaseModel
class AddLink(BaseModel):
url: str
+2 -2
View File
@@ -1,8 +1,8 @@
from uuid import UUID
from typing import AnyStr
from pydantic import BaseModel
class SportResponse(BaseModel):
id: AnyStr
id: UUID
name: str
@@ -1,38 +0,0 @@
{% 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 %}
@@ -1,43 +0,0 @@
{% 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 %}
-40
View File
@@ -1,40 +0,0 @@
{% 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 %}
@@ -20,16 +20,12 @@
<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>
<a href="/comic/worktypes/{{work.id}}">{{work.name}}</a>
{{work}}:
<ul>
{% for comic in artist.get_comics()[work] %}
<li><a href="/comic/comics/{{comic.id}}">{{comic.title}}</a></li>
@@ -47,20 +43,8 @@
<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 %}
@@ -1,32 +0,0 @@
{% 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 %}
+1 -1
View File
@@ -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 "comic/artist_cards.html" %}
{% include "components/artist_cards.html" %}
{% endwith %}
{% if loop.index %3 %}
@@ -27,17 +27,12 @@
{% 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>
<a href="/comic/worktypes/{{work.id}}">{{work.name}}</a>
{{work}}:
<ul>
{% for artist in comic.get_artists()[work] %}
<li><a href="/comic/artists/{{artist.id}}">{{artist.name}}</a></li>
@@ -47,29 +42,6 @@
{% 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>
@@ -82,15 +54,18 @@
<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 %}
@@ -1,56 +0,0 @@
{% 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 %}
+17 -34
View File
@@ -9,40 +9,23 @@
{% include "components/alerts.html" %}
{% endwith %}
<div class="container">
<div class="row">
<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">
<h1 class="display-5">Comics..</h1>
</div>
<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 class="row">
<div class="col">
<h1 class="display-5">Find Jobs..</h1>
</div>
</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 %}
</div>
{% else %}
</div></div><br><div class="row">
{% endif %}
{% endfor %}
</div>
</div>
{% endblock %}
@@ -1,92 +0,0 @@
{% 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,24 +20,6 @@
<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">
@@ -59,12 +41,5 @@
</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 "comic/publisher_cards.html" %}
{% include "components/publisher_cards.html" %}
{% endwith %}
{% if loop.index %3 %}
</div>
@@ -1,60 +0,0 @@
{% 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 %}
@@ -1,28 +0,0 @@
{% 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 %}
@@ -1,28 +0,0 @@
{% 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,7 +1,6 @@
<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>
@@ -1,4 +1,4 @@
{% if check %}
{% if check == 1 %}
<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,6 +1,8 @@
<nav class="navbar navbar-expand-lg navbar-light bg-light px-5">
<div class="container-fluid">
<a class="navbar-brand" href="#">Kontor</a>
<a class="navbar-brand" href="#">
<img src="{{ url_for('static', path='images/logo.png') }}" alt="" width="30" height="24">
</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>
@@ -16,7 +18,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="/comic/worktypes">WorkTypes</a></li>
<li><a class="dropdown-item" href="#">Something else here</a></li>
</ul>
</li>
<li class="nav-item dropdown">
@@ -24,7 +26,6 @@
<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">
@@ -41,19 +42,18 @@
<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="/admin/profiles">Profiles</a></li>
<li><a class="dropdown-item" href="/admin/permissions">Permissions</a></li>
<li><a class="dropdown-item" href="#">Something else here</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">
Media
Jobs
</a>
<ul class="dropdown-menu" aria-labelledby="navbarDropdown">
<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>
<li><a class="dropdown-item" href="/comics/">Comics</a></li>
<li><a class="dropdown-item" href="/comics/">Media</a></li>
</ul>
</li>
</ul>
@@ -41,12 +41,5 @@
</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 %}
+1 -1
View File
@@ -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 "media/actor_cards.html" %}
{% include "components/actor_cards.html" %}
{% endwith %}
{% if loop.index %3 %}
@@ -1,29 +0,0 @@
{% 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,12 +61,5 @@
</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 %}
+15 -25
View File
@@ -9,31 +9,21 @@
{% 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">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>{% with check=mediafile.review %}{% include "components/check.html" %}{% endwith %}</td>
<td>{% with check=mediafile.should_download %}{% include "components/check.html" %}{% endwith %}</td>
</tr>
<table class="table table-hover">
<thead><tr>
<th scope="col">Titel</th>
<th scope="col">URL</th>
<th scope="col">Cloudlink</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>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</tbody>
</table>
</div>
{% endblock %}
@@ -1,55 +0,0 @@
{% 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 %}
@@ -1,29 +0,0 @@
{% 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 %}
+2 -1
View File
@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="de-DE">
<html lang="en-us">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
@@ -21,5 +21,6 @@
{% block scripts %}
{% endblock %}
</body>
</html>
@@ -1,31 +0,0 @@
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})
-27
View File
@@ -1,27 +0,0 @@
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
@@ -1,35 +0,0 @@
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__)
+3 -10
View File
@@ -1,22 +1,15 @@
from fastapi import APIRouter, Request
from fastapi.templating import Jinja2Templates
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
from src.webapps.comic import route_comics
from src.webapps.media import route_media
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 = None):
def home(request: Request, msg: str = None):
return templates.TemplateResponse("index.html", {"request": request, "msg": msg})
@@ -1,27 +0,0 @@
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=}"
@@ -1,20 +0,0 @@
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
@@ -1,53 +0,0 @@
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)
+17 -65
View File
@@ -1,90 +1,42 @@
from fastapi import APIRouter, Form, Request, status
from uuid import UUID
from fastapi import APIRouter, Request
from fastapi.templating import Jinja2Templates
from fastapi.responses import RedirectResponse
from src.apis.utils import SessionDep
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
from src.db.models.comic import Comic, Artist, Publisher
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 = 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()
def get_comics(db: SessionDep, request: Request, msg: str = None):
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: AnyStr, request: Request, db: SessionDep):
def comic_details(comic_id: UUID, request: Request, db: SessionDep):
comic = db.get(Comic, comic_id)
return templates.TemplateResponse("comic/comic_detail.html", {"request": request, "comic":comic})
@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")
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.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("/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.get("/publishers")
def get_publishers(db: SessionDep, request: Request, msg: str | None = None):
def get_publishers(db: SessionDep, request: Request, msg: str = 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: AnyStr, request: Request, db: SessionDep, msg: str = None):
def publisher_details(publisher_id: UUID, 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})
@@ -1,73 +0,0 @@
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)
-20
View File
@@ -1,20 +0,0 @@
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
+6 -34
View File
@@ -1,50 +1,22 @@
from typing import AnyStr
from uuid import UUID
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": []})
mediafiles = db.query(MediaFile).all()
return templates.TemplateResponse("media/files.html", {"request": request, "msg": msg, "mediafiles": mediafiles})
@router.get("/files/{file_id}")
def file_details(file_id: AnyStr, request: Request, db: SessionDep):
def file_details(file_id: UUID, request: Request, db: SessionDep):
mediafile = db.get(MediaFile, file_id)
return templates.TemplateResponse("media/file_detail.html", {"request": request, "mediafile":mediafile})
@@ -54,7 +26,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: AnyStr, request: Request, db: SessionDep):
def artist_detail(actor_id: UUID, request: Request, db: SessionDep):
actor = db.get(MediaActor, actor_id)
return templates.TemplateResponse("media/actor_detail.html", {"request": request, "actor": actor})
@@ -1,53 +0,0 @@
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__)
-75
View File
@@ -1,75 +0,0 @@
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
+10 -2
View File
@@ -1,7 +1,15 @@
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("/api/comic/artists")
response = client.get("/comic/artists")
assert response.status_code == 200
assert len(response.json()) == 0
assert len(response.json()) == 5
+240 -400
View File
File diff suppressed because it is too large Load Diff
-81
View File
@@ -1,81 +0,0 @@
"""
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')
@@ -1 +0,0 @@
3.13
@@ -1,50 +0,0 @@
## ------------------------------- 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"]
@@ -1,16 +0,0 @@
[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",
]
@@ -1,6 +0,0 @@
def main():
print("Hello from kontor-domain!")
if __name__ == "__main__":
main()
-57
View File
@@ -1,57 +0,0 @@
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 -1
View File
@@ -1,6 +1,6 @@
[project]
name = "kontor-schema"
version = "0.2.0"
version = "0.1.0"
description = "Kontor Schema Library"
readme = "README.md"
authors = [
+59 -59
View File
@@ -1,5 +1,5 @@
version = 1
revision = 2
revision = 1
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, upload_time = "2025-04-15T17:05:13.836Z" }
sdist = { url = "https://files.pythonhosted.org/packages/d8/e4/0c4c39e18fd76d6a628d4dd8da40543d136ce2d1752bd6eeeab0791f4d6b/beautifulsoup4-4.13.4.tar.gz", hash = "sha256:dbb3c4e1ceae6aefebdaf2423247260cd062430a410e38c66f2baa50a8437195", size = 621067 }
wheels = [
{ 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" },
{ url = "https://files.pythonhosted.org/packages/50/cd/30110dc0ffcf3b131156077b90e9f60ed75711223f306da4db08eff8403b/beautifulsoup4-4.13.4-py3-none-any.whl", hash = "sha256:9bbbb14bfde9d79f38b8cd5f8c7c85f4b8f2523190ebed90e950a8dea4cb1c4b", size = 187285 },
]
[[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, upload_time = "2025-01-31T02:16:47.166Z" }
sdist = { url = "https://files.pythonhosted.org/packages/1c/ab/c9f1e32b7b1bf505bf26f0ef697775960db7932abeb7b516de930ba2705f/certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", size = 167577 }
wheels = [
{ 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" },
{ url = "https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe", size = 166393 },
]
[[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, upload_time = "2024-12-24T18:12:35.43Z" }
sdist = { url = "https://files.pythonhosted.org/packages/16/b0/572805e227f01586461c80e0fd25d65a2115599cc9dad142fee4b747c357/charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3", size = 123188 }
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, 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" },
{ 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 },
]
[[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, upload_time = "2025-04-15T16:21:26.141Z" }
sdist = { url = "https://files.pythonhosted.org/packages/b0/9c/666d8c71b18d0189cf801c0e0b31c4bfc609ac823883286045b1f3ae8994/greenlet-3.2.0.tar.gz", hash = "sha256:1d2d43bd711a43db8d9b9187500e6432ddb4fafe112d082ffabca8660a9e01a7", size = 183685 }
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, 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" },
{ 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 },
]
[[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, upload_time = "2024-09-15T18:07:39.745Z" }
sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 }
wheels = [
{ 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" },
{ url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 },
]
[[package]]
name = "kontor-schema"
version = "0.2.0"
version = "0.1.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, upload_time = "2024-05-29T15:37:49.536Z" }
sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218 }
wheels = [
{ 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" },
{ url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 },
]
[[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, upload_time = "2025-04-20T18:50:08.518Z" }
sdist = { url = "https://files.pythonhosted.org/packages/3f/f4/4a80cd6ef364b2e8b65b15816a843c0980f7a5a2b4dc701fc574952aa19f/soupsieve-2.7.tar.gz", hash = "sha256:ad282f9b6926286d2ead4750552c8a6142bc4c783fd66b0293547c8fe6ae126a", size = 103418 }
wheels = [
{ 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" },
{ url = "https://files.pythonhosted.org/packages/e7/9c/0e6afc12c269578be5c0c1c9f4b49a8d32770a080260c333ac04cc1c832d/soupsieve-2.7-py3-none-any.whl", hash = "sha256:6e60cc5c1ffaf1cebcc12e8188320b72071e922c2e897f737cadce79ad5d30c4", size = 36677 },
]
[[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, upload_time = "2025-03-27T17:52:31.876Z" }
sdist = { url = "https://files.pythonhosted.org/packages/68/c3/3f2bfa5e4dcd9938405fe2fab5b6ab94a9248a4f9536ea2fd497da20525f/sqlalchemy-2.0.40.tar.gz", hash = "sha256:d827099289c64589418ebbcaead0145cd19f4e3e8a93919a0100247af245fa00", size = 9664299 }
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, 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" },
{ 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 },
]
[[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, upload_time = "2025-04-10T14:19:05.416Z" }
sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967 }
wheels = [
{ 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" },
{ url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806 },
]
[[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, upload_time = "2025-04-10T15:23:39.232Z" }
sdist = { url = "https://files.pythonhosted.org/packages/8a/78/16493d9c386d8e60e442a35feac5e00f0913c0f4b7c217c11e8ec2ff53e0/urllib3-2.4.0.tar.gz", hash = "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466", size = 390672 }
wheels = [
{ 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" },
{ url = "https://files.pythonhosted.org/packages/6b/11/cc635220681e93a0183390e26485430ca2c7b5f9d33b15c74c2861cb8091/urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813", size = 128680 },
]
-81
View File
@@ -1,81 +0,0 @@
"""
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')

Some files were not shown because too many files have changed in this diff Show More