99 Commits

Author SHA1 Message Date
tpeetz ae66fe3c61 add Hibernate to kontor-javalin 2025-12-22 00:22:42 +01:00
tpeetz 66cbd4b2d1 Merge branch 'feature/Dockerfile-multistage' into 'develop/0.2.0'
add kontor-javalin and change to multistage Dockerfile for Gradle projects

See merge request tpeetz/kontor!37
2025-12-21 21:44:23 +01:00
tpeetz 11b13e0bf4 add healthcheck for kontor-javalin 2025-12-21 21:15:08 +01:00
tpeetz 3c4e182832 added running kontor-javalin with example API 2025-12-21 20:14:31 +01:00
tpeetz 7ef616a2a9 add task fatJar 2025-12-21 19:42:22 +01:00
tpeetz d3ccc2a46b create logs directory in Dockerfile for kontor-spring 2025-12-21 19:27:49 +01:00
tpeetz e0a235fcc5 change Dockerfile for kontor-spring to multistage and add kontor-javalin 2025-12-20 04:13:41 +01:00
tpeetz 25bc4e45ff add kontor-javalin 2025-12-16 23:10:15 +01:00
tpeetz 2a51b4a2f3 add links from file 2025-12-14 16:12:22 +01:00
tpeetz 729d019de9 format download.py 2025-12-09 17:05:20 +01:00
tpeetz 430a69a95c fix missing line feed 2025-12-09 15:35:50 +01:00
Thomas Peetz 0a53e68b54 add limit for downloads 2025-12-09 13:48:22 +01:00
tpeetz e061e0aadb fix declaration of network database as external 2025-12-07 23:30:32 +01:00
tpeetz 6d78d60ecf fix declaration of network database as external 2025-12-07 22:35:59 +01:00
Thomas Peetz 3a0c062e78 change port for kontor-spring to enable port 800 for paperless-ngx 2025-12-07 21:24:32 +01:00
Thomas Peetz a5393f471f fix login functionality 2025-12-04 17:23:59 +01:00
Thomas Peetz 46bca919d7 add authentication for kontor-api-go 2025-12-01 06:53:03 +01:00
Thomas Peetz 741032ec02 add schema for MediaFile 2025-12-01 05:25:54 +01:00
Thomas Peetz d63120b092 add kontor-api-go REST API with Go, Fiber, Bun 2025-11-30 01:50:00 +01:00
Thomas Peetz 836a10e3ef store token 2025-11-13 19:10:46 +01:00
Thomas Peetz 8ca73b94aa fix call for login endpoint 2025-11-12 23:21:39 +01:00
Thomas Peetz 447030533f setup Auth component 2025-11-12 21:06:28 +01:00
Thomas Peetz 66a93b2b97 setup AuthComponent 2025-11-12 17:29:40 +01:00
Thomas Peetz 6c71086104 add handling of missing urls 2025-11-07 08:00:52 +01:00
Thomas Peetz 4a2048c378 add healthcheck to kontor-api and docker-compose.yml 2025-11-06 16:37:50 +01:00
Thomas Peetz f3e47126b3 add login functions for cookie and non-cookie authentication 2025-11-05 21:48:25 +01:00
Thomas Peetz 09c2a350e4 add JWT dependencies 2025-10-17 22:14:32 +02:00
Thomas Peetz 92cd9be8ec add first draft of search bar to comics list 2025-10-06 19:40:28 +02:00
Thomas Peetz e6ead4937d display imprints and parent publisher of comics publisher 2025-10-03 20:35:06 +02:00
tpeetz 5e17182686 Merge branch 'feature/34-refactor-kontor-angular' into 'develop/0.2.0'
Resolve "refactor kontor-angular"

Closes #34

See merge request tpeetz/kontor!36
2025-10-03 16:06:57 +02:00
Thomas Peetz c52a5b8715 Extend display of comics publishers 2025-10-03 16:04:52 +02:00
Thomas Peetz a9829bf118 extend ComicDetailsResponse 2025-10-02 14:18:48 +02:00
Thomas Peetz d8e4cbfb9a display Comics as Artists 2025-10-01 21:06:12 +02:00
Thomas Peetz 0db55e9ba7 added issue works on artist details 2025-09-23 17:08:46 +02:00
Thomas Peetz 2534c67a5e moved interface to common place 2025-09-22 14:55:38 +02:00
Thomas Peetz 6d88b87f93 add comic artists 2025-09-21 20:09:46 +02:00
Thomas Peetz 64ed4876a5 change response for comic details 2025-09-20 13:05:07 +02:00
Thomas Peetz fd7a6bd1a1 load comic details 2025-09-19 22:34:44 +02:00
Thomas Peetz b250bfe76c WIP: setup styling 2025-09-19 14:50:45 +02:00
Thomas Peetz 63ac0231dc add route for specific comic 2025-09-19 00:55:04 +02:00
Thomas Peetz 21533ee9f9 add links for tysc types 2025-09-19 00:17:04 +02:00
Thomas Peetz 30f9829768 add links for media types 2025-09-18 23:56:53 +02:00
Thomas Peetz a5b1d771a0 add links for comics and publishers 2025-09-18 23:49:39 +02:00
Thomas Peetz b5425c62de add links for artists 2025-09-18 23:40:09 +02:00
Thomas Peetz 0cc2561327 add routes for Media section 2025-09-18 23:07:41 +02:00
Thomas Peetz f933312283 add routes for TYSC section 2025-09-18 22:58:19 +02:00
Thomas Peetz a0d2bef4ef add routes for Comic section 2025-09-18 22:46:23 +02:00
Thomas Peetz eb8b283769 setup header, navigation and footer of app 2025-09-18 22:33:58 +02:00
Thomas Peetz fcfe9a2c08 Revert "move Kontor related components to subfolder kontor"
This reverts commit 9ee1a04a5c.
2025-09-18 22:13:39 +02:00
Thomas Peetz 9ee1a04a5c move Kontor related components to subfolder kontor 2025-09-18 21:59:14 +02:00
Thomas Peetz 933974c958 add Artist Details 2025-09-15 17:08:42 +02:00
Thomas Peetz 5ff9d5a11b fetch list of artists 2025-09-14 10:47:17 +02:00
Thomas Peetz 8b9313da93 add navigation and fetching of sports 2025-09-12 23:51:38 +02:00
Thomas Peetz 9acdff19e1 add routing for comics and media 2025-09-10 17:25:22 +02:00
Thomas Peetz ad1118803d setup kontor-angiöar with comics navigation 2025-09-08 22:10:23 +02:00
Thomas Peetz 55a09d32b3 improve download.py 2025-09-08 13:38:47 +02:00
Thomas Peetz 8b1b84b195 refactor find_links.py by adding methods for specific tasks 2025-09-08 12:42:01 +02:00
Thomas Peetz acbf9c51a3 refactor find_links.py by adding methods for specific tasks 2025-09-07 22:47:06 +02:00
Thomas Peetz 5bfea51b27 improve find_links.py 2025-09-06 19:55:33 +02:00
Thomas Peetz a398866f04 improve find_links.py 2025-09-05 13:01:11 +02:00
Thomas Peetz ee737aaf61 add Actors page to navigation 2025-09-04 18:07:07 +02:00
Thomas Peetz c9460ac198 add actors 2025-09-03 22:00:30 +02:00
Thomas Peetz 3c5f5e50e0 add adding of actors 2025-09-03 17:45:20 +02:00
Thomas Peetz b40b9538ab adapt logging levels for messages 2025-09-02 20:39:21 +02:00
Thomas Peetz 2fa77d3af6 adapt logging levels for messages 2025-08-31 13:50:31 +02:00
Thomas Peetz a6a03e3f04 fix problem in download.py when title has not been set 2025-08-19 08:55:45 +02:00
Thomas Peetz ddc5797549 fix problem in download.py when title has not been set 2025-08-18 13:42:58 +02:00
Thomas Peetz 65f5e4bb2e add styles from kontor-api to kontor-vue 2025-08-18 00:06:12 +02:00
tpeetz 976a116799 Merge branch 'feature/33-use-css-for-header' into 'develop/0.2.0'
Resolve "use CSS for header"

Closes #33

See merge request tpeetz/kontor!35
2025-08-16 00:40:32 +02:00
tpeetz c8736577e5 Resolve "use CSS for header" 2025-08-16 00:40:32 +02:00
Thomas Peetz 60d314abaf split docker-compose.yml to extract database services 2025-08-10 02:55:56 +02:00
Thomas Peetz 0810ca8d50 update versions 2025-08-07 22:03:37 +02:00
tpeetz 68b4bbb979 Merge branch 'feature/31-setup-vue-app' into 'develop/0.2.0'
Resolve "Setup Vue app"

Closes #31

See merge request tpeetz/kontor!33
2025-08-07 21:08:28 +02:00
Thomas Peetz 427850bd64 setup Example Vue app
refs #31
2025-08-07 21:07:40 +02:00
Thomas Peetz 6201e433ae add VUE.js app 2025-08-07 18:33:34 +02:00
Thomas Peetz 450bfdb394 add column to MediaActor to store weblink 2025-08-03 17:37:57 +02:00
tpeetz aed2413b88 Merge branch 'feature/29-setup-angular-app' into 'develop/0.2.0'
Setup Angular app

Closes #29

See merge request tpeetz/kontor!32
2025-07-29 20:57:25 +02:00
Thomas Peetz 01788286e2 setup Angular app and integrate Docker image build in docker-compose.yml 2025-07-20 11:19:05 +02:00
Thomas Peetz 169b349277 add handling of error condition before renaming file 2025-07-18 23:37:00 +02:00
Thomas Peetz 9c0de2a0be set version for kontor-angular to 0.2.0 2025-07-18 21:29:24 +02:00
Thomas Peetz 3b4098cb1a setup Angular app 2025-07-18 17:17:00 +02:00
Thomas Peetz 0d99f383fb add MediaFile from queue 2025-07-11 16:49:42 +02:00
Thomas Peetz c6fd80408b read queue 2025-07-10 22:04:31 +02:00
Thomas Peetz d2d4deb350 set messages durable 2025-07-10 15:58:11 +02:00
tpeetz ee280e0b8e Merge branch 'feature/27-use-amqp-protocol-for-messages' into 'develop/0.2.0'
use AMQP protocol for messages

Closes #27

See merge request tpeetz/kontor!30
2025-07-07 15:52:04 +02:00
Thomas Peetz 1c2c2f38a4 use message for adding links 2025-07-07 15:49:33 +02:00
Thomas Peetz b5cca50960 add_link.py sends message 2025-07-06 22:46:47 +02:00
tpeetz 0a505fabcf Merge branch 'feature/25-integrate-apache-camel' into 'develop/0.2.0'
integrate apache camel

Closes #25 and #26

See merge request tpeetz/kontor!29
2025-07-04 00:29:48 +02:00
Thomas Peetz 4b8053c6e2 fix configuration to connect to Artemis from Sprint Boot for all environments 2025-07-04 00:29:15 +02:00
Thomas Peetz 5df3c1c47e integrate Apache Camel into Spring Boot 2025-07-03 17:42:30 +02:00
Thomas Peetz adad4f88da configure actuator endpoints 2025-07-03 11:31:40 +02:00
Thomas Peetz 957c7a702e Extend docker-compose.yml network configuration for camel-karavan 2025-07-03 07:33:24 +02:00
tpeetz b5344a9ed1 Merge branch 'feature/24-add-docker-image-for-apache-camel-caravan' into 'develop/0.2.0'
Add Docker image for Apache Camel Caravan

Closes #24

See merge request tpeetz/kontor!28
2025-07-01 22:28:37 +02:00
Thomas Peetz 16ad701eed Extend docker-compose.yml for camel-karavan, registry and gitea 2025-07-01 22:23:24 +02:00
Thomas Peetz 2ba7465675 change port and credentials for messages 2025-07-01 07:01:34 +02:00
tpeetz c703dfc6e3 Merge branch 'feature/23-add-docker-image-for-apache-activemq' into 'develop/0.2.0'
Add Docker image for Apache ActiveMQ

Closes #23

See merge request tpeetz/kontor!26
2025-06-30 17:39:10 +02:00
Thomas Peetz b62cc89f71 add Apache ActiveMQ Artemis as Docker image to docker-compose.yml 2025-06-30 17:33:39 +02:00
tpeetz e741a46f69 Merge branch 'feature/22-add-docker-image-for-couchdb' into 'develop/0.2.0'
Add Docker image for CouchDB

Closes #22

See merge request tpeetz/kontor!25
2025-06-30 16:42:08 +02:00
Thomas Peetz 4f87ff83ce add CouchDB as Docker image to docker-compose.yml 2025-06-30 16:38:15 +02:00
482 changed files with 15409 additions and 23373 deletions
-19
View File
@@ -1,19 +0,0 @@
name: Gitea Actions Demo
run-name: ${{ gitea.actor }} ist testing out Gitea Actions
on: [push]
jobs:
Explore-Gitea-Actions:
runs-on: inky
steps:
- run: echo "The job was automatically triggered by a ${{ gitea.event_name }} event."
- run: echo "This job is now running on a ${{ runner.os }} server hosted by Gitea!"
- run: echo "The name of your branch is ${{ gitea.ref }} and your repository is ${{ gitea.repository }}."
- name: Check out repository code
uses: actions/checkout@v4
- run: echo "The ${{ gitea.repository }} repository has been cloned to the runner."
- run: echo "The workflow is now ready to test your code on the runner."
- name: List files in the directory
run: |
ls ${{ gitea.workspace }}
- run: echo "The job's status is ${{ job.status }}."
+3 -15
View File
@@ -2,19 +2,7 @@
.idea/
.angular/
__pycache__/
bonus/
icons/
icons-shadowless/
springboot/.factorypath
java-ee/.settings
java-ee/.project
kontor-schema/env
kontor-schema/kontor_schema.egg-info
kontor-gui/.pdm-python
kontor-gui/dist
fastapi/.coverage
kontor-api/.coverage
node_modules/
.editorconfig
db-password.txt
kontor-api/tests/test_main.py
kontor-api/tests/test_db.db
kontor-api/test_db.db
couchdb-password.txt
+6 -4
View File
@@ -1,10 +1,12 @@
kontor_api := kontor-api
kontor_spring := kontor-spring
kontor_servicemix := kontor-servicemix
kontor_angular := kontor-angular
TARGET=docker
.PHONY: all $(kontor_spring) $(kontor_api)
all: $(kontor_spring) $(kontor_api) $(kontor_servicemix)
.PHONY: all $(kontor_spring) $(kontor_api) $(kontor_angular)
$(kontor_spring) $(kontor_api) $(kontor_servicemix):
all: $(kontor_spring) $(kontor_api) $(kontor_angular)
$(kontor_spring) $(kontor_api) $(kontor_angular):
$(MAKE) --directory=$@ $(TARGET)
+39
View File
@@ -0,0 +1,39 @@
services:
postgres:
image: postgres
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
ports:
- 5432:5432
networks:
- database
volumes:
- postgres-data:/var/lib/postgresql/data:rw
secrets:
- db-password
adminer:
image: adminer
ports:
- 8090:8080
networks:
- database
- frontend
volumes:
postgres-data:
secrets:
db-password:
file: db-password.txt
networks:
database:
name: database
+100 -98
View File
@@ -1,35 +1,25 @@
include:
- ./compose-postgres.yaml
services:
postgres:
image: postgres
activemq:
image: apache/activemq-artemis:latest-alpine
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
ports:
- 5432:5432
- 61616:61616
- 8161:8161
- 5672:5672
networks:
- integration
- frontend
volumes:
- postgres-data:/var/lib/postgresql/data:rw
secrets:
- db-password
adminer:
image: adminer
ports:
- 8090:8080
networks:
- database
- frontend
- activemq-data:/var/lib/artemis-instance
kontor:
build:
context: ./kontor-spring
dockerfile: Dockerfile
tags:
- kontor:0.2.0-SNAPSHOT
image: kontor:0.2.0-SNAPSHOT
restart: unless-stopped
networks:
@@ -37,85 +27,22 @@ services:
- integration
- frontend
ports:
- 8000:8000
- 8100:8100
volumes:
- images-data:/data/images
depends_on:
postgres:
condition: service_healthy
kontor-api:
build:
context: ./kontor-api
dockerfile: Dockerfile
tags:
- kontor-api:0.2.0-SNAPSHOT
image: kontor-api:0.2.0-SNAPSHOT
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://kontor-api:8500/health"]
interval: 10s
timeout: 5s
retries: 3
networks:
- database
- integration
- frontend
ports:
- 8500:8500
volumes:
- images-data:/data/images
depends_on:
postgres:
condition: service_healthy
kontor-fiber:
build:
context: ./kontor-fiber
dockerfile: Dockerfile
tags:
- kontor-fiber:0.2.0-SNAPSHOT
image: kontor-fiber:0.2.0-SNAPSHOT
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://kontor-fiber:8600/health"]
interval: 10s
timeout: 5s
retries: 3
networks:
- database
- integration
- frontend
ports:
- 8600:8600
depends_on:
postgres:
condition: service_healthy
kontor-echo:
build:
context: ./kontor-echo
dockerfile: Dockerfile
tags:
- kontor-echo:0.2.0-SNAPSHOT
image: kontor-echo:0.2.0-SNAPSHOT
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://kontor-echo:8700/health"]
interval: 10s
timeout: 5s
retries: 3
networks:
- database
- integration
- frontend
ports:
- 8700:8700
depends_on:
postgres:
condition: service_healthy
kontor-quarkus:
build:
context: ./kontor-quarkus
dockerfile: Dockerfile
tags:
- kontor-quarkus:0.2.0-SNAPSHOT
image: kontor-quarkus:0.2.0-SNAPSHOT
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://kontor-quarkus:8800/q/health"]
test: ["CMD", "curl", "-f", "http://kontor-api:8800/health"]
interval: 10s
timeout: 5s
retries: 3
@@ -130,14 +57,89 @@ services:
depends_on:
postgres:
condition: service_healthy
kontor-api-go:
build:
context: ./kontor-api-go
dockerfile: Dockerfile
tags:
- kontor-api-go:0.2.0-SNAPSHOT
image: kontor-api-go:0.2.0-SNAPSHOT
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://kontor-api-go:8900/health"]
interval: 10s
timeout: 5s
retries: 3
networks:
- database
- integration
- frontend
ports:
- 8900:8900
depends_on:
postgres:
condition: service_healthy
kontor-angular:
build:
context: ./kontor-angular
dockerfile: Dockerfile
tags:
- kontor-angular:0.2.0-SNAPSHOT
image: kontor-angular:0.2.0-SNAPSHOT
restart: unless-stopped
networks:
- database
- integration
- frontend
ports:
- 8200:80
depends_on:
kontor-api:
condition: service_healthy
kontor-vue:
build:
context: ./kontor-vue
dockerfile: Dockerfile
tags:
- kontor-vue:0.2.0-SNAPSHOT
image: kontor-vue:0.2.0-SNAPSHOT
restart: unless-stopped
networks:
- database
- integration
- frontend
ports:
- 8300:80
depends_on:
postgres:
condition: service_healthy
kontor-javalin:
build:
context: ./kontor-javalin
dockerfile: Dockerfile
tags:
- kontor-javalin:0.2.0-SNAPSHOT
image: kontor-javalin:0.2.0-SNAPSHOT
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://kontor-javalin:8400/health"]
interval: 10s
timeout: 5s
retries: 3
networks:
- database
- integration
- frontend
ports:
- 8400:8400
depends_on:
postgres:
condition: service_healthy
networks:
integration:
name: integration
frontend:
volumes:
postgres-data:
activemq-data:
images-data:
secrets:
db-password:
file: db-password.txt
+9
View File
@@ -0,0 +1,9 @@
build/
.settings/
.gradle/
.classpath
.project
.vscode/
bin/
.idea/
/.eclipse-pmd
+38
View File
@@ -0,0 +1,38 @@
variables:
GRADLE_OPTS: "-Dorg.gradle.daemon=false"
before_script:
- source "/home/gitlab-runner/.sdkman/bin/sdkman-init.sh"
- sdk u java 11.0.12-open
- chmod +x ./gradlew
stages:
- build
- test
- analysis
- publish
Build Application:
stage: build
script: ./gradlew --build-cache compileJava
Create Documentation:
stage: build
script: ./gradlew asciidoctorPdf
Test Application:
stage: test
script: ./gradlew check
sonarqube-check:
stage: analysis
variables:
SONAR_USER_HOME: "${CI_PROJECT_DIR}/.sonar" # Defines the location of the analysis task cache
GIT_DEPTH: "0" # Tells git to fetch all the branches of the project, required by the analysis task
script: ./gradlew sonarqube
allow_failure: true
Publish Artifacts:
stage: publish
script: ./gradlew publish
@@ -1,68 +1,60 @@
# kontor-quarkus
# kontor-quarkus Project
This project uses Quarkus, the Supersonic Subatomic Java Framework.
If you want to learn more about Quarkus, please visit its website: <https://quarkus.io/>.
If you want to learn more about Quarkus, please visit its website: https://quarkus.io/ .
## Running the application in dev mode
You can run your application in dev mode that enables live coding using:
```shell script
./gradlew quarkusDev
```
> **_NOTE:_** Quarkus now ships with a Dev UI, which is available in dev mode only at <http://localhost:8080/q/dev/>.
> **_NOTE:_** Quarkus now ships with a Dev UI, which is available in dev mode only at http://localhost:8080/q/dev/.
## Packaging and running the application
The application can be packaged using:
```shell script
./gradlew build
```
It produces the `quarkus-run.jar` file in the `build/quarkus-app/` directory.
Be aware that its not an _über-jar_ as the dependencies are copied into the `build/quarkus-app/lib/` directory.
The application is now runnable using `java -jar build/quarkus-app/quarkus-run.jar`.
If you want to build an _über-jar_, execute the following command:
```shell script
./gradlew build -Dquarkus.package.jar.type=uber-jar
./gradlew build -Dquarkus.package.type=uber-jar
```
The application, packaged as an _über-jar_, is now runnable using `java -jar build/*-runner.jar`.
## Creating a native executable
You can create a native executable using:
You can create a native executable using:
```shell script
./gradlew build -Dquarkus.native.enabled=true
./gradlew build -Dquarkus.package.type=native
```
Or, if you don't have GraalVM installed, you can run the native executable build in a container using:
Or, if you don't have GraalVM installed, you can run the native executable build in a container using:
```shell script
./gradlew build -Dquarkus.native.enabled=true -Dquarkus.native.container-build=true
./gradlew build -Dquarkus.package.type=native -Dquarkus.native.container-build=true
```
You can then execute your native executable with: `./build/kontor-quarkus-1.0.0-SNAPSHOT-runner`
If you want to learn more about building native executables, please consult <https://quarkus.io/guides/gradle-tooling>.
If you want to learn more about building native executables, please consult https://quarkus.io/guides/gradle-tooling.
## Related Guides
- REST ([guide](https://quarkus.io/guides/rest)): A Jakarta REST implementation utilizing build time processing and Vert.x. This extension is not compatible with the quarkus-resteasy extension, or any of the extensions that depend on it.
- REST Jackson ([guide](https://quarkus.io/guides/rest#json-serialisation)): Jackson serialization support for Quarkus REST. This extension is not compatible with the quarkus-resteasy extension, or any of the extensions that depend on it
- JDBC Driver - PostgreSQL ([guide](https://quarkus.io/guides/datasource)): Connect to the PostgreSQL database via JDBC
- RESTEasy Reactive ([guide](https://quarkus.io/guides/resteasy-reactive)): A JAX-RS implementation utilizing build time processing and Vert.x. This extension is not compatible with the quarkus-resteasy extension, or any of the extensions that depend on it.
## Provided Code
### REST
### RESTEasy Reactive
Easily start your REST Web Services
Easily start your Reactive RESTful Web Services
[Related guide section...](https://quarkus.io/guides/getting-started-reactive#reactive-jax-rs-resources)
+158
View File
@@ -0,0 +1,158 @@
plugins {
id 'java'
id 'maven-publish'
id 'jacoco'
id 'jacoco-report-aggregation'
id 'pmd'
id 'checkstyle'
id 'jvm-test-suite'
id 'io.quarkus'
id "org.sonarqube" version "3.3"
alias(versionsLibs.plugins.asciidoctorConvert)
alias(versionsLibs.plugins.asciidoctorPdf)
alias(versionsLibs.plugins.asciidoctorGems)
}
repositories {
ruby.gems()
maven {
url "https://nexus.thpeetz.de/nexus/content/groups/public/"
}
mavenCentral()
mavenLocal()
}
dependencies {
implementation enforcedPlatform("${quarkusPlatformGroupId}:${quarkusPlatformArtifactId}:${quarkusPlatformVersion}")
implementation 'io.quarkus:quarkus-resteasy-reactive'
implementation 'io.quarkus:quarkus-resteasy-reactive-jackson'
implementation 'io.quarkus:quarkus-arc'
testImplementation 'io.quarkus:quarkus-junit5'
testImplementation 'io.rest-assured:rest-assured'
testImplementation("io.quarkus:quarkus-jacoco")
}
final BUILD_DATE = new Date().format('dd.MM.yyyy').toString()
def pdfFile = layout.buildDirectory.file("docs/asciidocPdf/" + project.name + ".pdf")
def pdfArtifact = artifacts.add('archives', pdfFile.get().asFile) {
type 'pdf'
builtBy asciidoctorPdf
}
tasks.withType(GenerateModuleMetadata).configureEach {
suppressedValidationErrors.add('enforced-platform')
}
publishing {
publications {
quarkusApp(MavenPublication){
from components.java
}
docs(MavenPublication) {
groupId = group + '.docs'
artifactId = project.name
artifact pdfArtifact
}
}
repositories {
maven {
name = 'nexusPeetz'
def releasesRepoUrl = "https://nexus.thpeetz.de/nexus/content/repositories/releases/"
def snapshotsRepoUrl = "https://nexus.thpeetz.de/nexus/content/repositories/snapshots/"
url = version.endsWith('SNAPSHOT') ? snapshotsRepoUrl : releasesRepoUrl
credentials(PasswordCredentials)
}
}
}
java {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
tasks.withType(GenerateModuleMetadata).configureEach {
suppressedValidationErrors.add('enforced-platform')
}
compileJava {
options.encoding = 'UTF-8'
options.compilerArgs << '-parameters'
}
compileTestJava {
options.encoding = 'UTF-8'
}
jacocoTestReport {
reports {
xml.required = true
}
}
test.finalizedBy jacocoTestReport
pmd {
//ruleSetFiles = files("custom-pmd-ruleset.xml")
ruleSets = ["category/java/errorprone.xml", "category/java/bestpractices.xml"]
ignoreFailures = true
}
tasks.withType(Checkstyle) {
reports {
xml.required = true
html.required = true
}
}
testing {
suites {
test {
useJUnitJupiter()
}
}
}
pmdTest.dependsOn("compileQuarkusTestGeneratedSourcesJava")
checkstyleTest.dependsOn("compileQuarkusTestGeneratedSourcesJava")
jacocoTestReport.dependsOn("compileQuarkusGeneratedSourcesJava")
pmdMain.dependsOn("compileQuarkusGeneratedSourcesJava")
sonarqube {
properties {
property "sonar.host.url", "https://sonar.thpeetz.de"
property "sonar.qualitygate.wait", true
property "sonar.sourceEncoding", "UTF-8"
property "sonar.projectKey", "kontor_kontor-quarkus_AYQek8Mxz0hBjLSV8I8O"
property "sonar.login", "5ecd90dee57806857e07443a9b0efd3cd7774a81"
}
}
tasks.named('sonarqube').configure {
dependsOn 'test', 'pmdMain', 'checkstyleMain', 'jacocoTestReport'
}
asciidoctorPdf {
baseDirFollowsSourceFile()
asciidoctorj {
attributes 'build-gradle': file('build.gradle'),
'endpoint-url': 'https://www.thpeetz.de',
'source-highlighter': 'rouge',
'imagesdir': './images',
'toc': 'left',
'toc-title': 'Inhaltsverzeichnis',
'revdate': BUILD_DATE,
'revnumber': { project.version.toString() },
'revremark': 'Entwurf',
'chapter-label': '',
'icons': 'font',
'idprefix': 'id_',
'idseparator': '-',
'docinfo1': ''
}
}
wrapper {
gradleVersion = "7.5"
}
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE module PUBLIC "-//Checkstyle//DTD Checkstyle Configuration 1.3//EN" "https://checkstyle.org/dtds/configuration_1_3.dtd">
<module name="Checker">
<module name="TreeWalker">
<module name="UnusedImports" />
<module name="AvoidStarImport" />
<module name="ConstantName" />
<module name="NeedBraces" />
<module name="RightCurly" />
<module name="ModifierOrder">
<property name="severity" value="warning"/>
</module>
</module>
</module>
+9
View File
@@ -0,0 +1,9 @@
#Gradle properties
group=de.thpeetz.kontor
version=1.0.0-SNAPSHOT
quarkusPluginId=io.quarkus
quarkusPluginVersion=2.13.2.Final
quarkusPlatformGroupId=io.quarkus.platform
quarkusPlatformArtifactId=quarkus-bom
quarkusPlatformVersion=2.13.2.Final
Binary file not shown.
@@ -1,7 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-bin.zip
networkTimeout=10000
validateDistributionUrl=true
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
+185
View File
@@ -0,0 +1,185 @@
#!/usr/bin/env sh
#
# Copyright 2015 the original author or authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=`expr $i + 1`
done
case $i in
0) set -- ;;
1) set -- "$args0" ;;
2) set -- "$args0" "$args1" ;;
3) set -- "$args0" "$args1" "$args2" ;;
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=`save "$@"`
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
exec "$JAVACMD" "$@"
+104
View File
@@ -0,0 +1,104 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:init
@rem Get command-line arguments, handling Windows variants
if not "%OS%" == "Windows_NT" goto win9xME_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
@@ -0,0 +1,84 @@
= Projektbeschreibung kontor-quarkus: Pflichtenheft und Projekthandbuch
:author: Thomas Peetz
:email: <thomas.peetz@thpeetz.de>
:doctype: article
:sectnums:
:sectnumlevels: 4
:toc:
:toclevels: 4
:table-caption!:
:counter: table-number: 0
//[title="Dokumenthistorie", caption="Tabelle {counter:table-number} ", id="Tabelle-{counter:table-number}", options="header"]
//[title="Dokumenthistorie", id="Table-{counter:table-number}", options="header", cols="4"]
[title="Dokumenthistorie", id="Table-{counter:table-number}", options="header"]
|===
| Version | Datum | Autor | Änderungsgrund / Bemerkungen
| 0.0.1 | 16.05.2022 | Thomas Peetz | Ersterstellung
|===
== Einführung
=== Zweck
=== Stakeholder des Systems
=== Systemumfang
==== Zielsetzung des Systems
=== Systemübersicht
==== Systemkontext
==== Systemarchitektur
==== Systemschnittstellen
===== Realisierte Schnittstellen
===== Verwendete Schnittstellen
==== Logisches Datenmodell
==== Einschränkungen
== Anforderungen der Domäne
=== Systemfunktionen
==== Anwendungsfälle
==== Akteure
==== Zielgruppen
=== Anforderungen
==== Anforderungen an externe Schnittstellen
==== Funktionale Anforderungen
==== Qualitätsanforderungen
==== Randbedingungen
==== Weitere Anforderungen
==== Wartungs- und Supportinformationen
=== Verifikation
[bibliography]
== Referenzen
[glossary]
== Glossar
== Verzeichnisse
=== Abbildungsverzeichnis
=== Tabellenverzeichnis
<<Table-1, Tabelle 1>> <<Table-1>>
@@ -14,9 +14,7 @@
# docker run -i --rm -p 8080:8080 quarkus/kontor-quarkus-jvm
#
# If you want to include the debug port into your docker image
# you will have to expose the debug port (default 5005 being the default) like this : EXPOSE 8080 5005.
# Additionally you will have to set -e JAVA_DEBUG=true and -e JAVA_DEBUG_PORT=*:5005
# when running the container
# you will have to expose the debug port (default 5005) like this : EXPOSE 8080 5005
#
# Then run the container using :
#
@@ -26,8 +24,7 @@
# This scripts computes the command line to execute your Java application, and
# includes memory/GC tuning.
# You can configure the behavior using the following environment properties:
# - JAVA_OPTS: JVM options passed to the `java` command (example: "-verbose:class") - Be aware that this will override
# the default JVM options, use `JAVA_OPTS_APPEND` to append options
# - JAVA_OPTS: JVM options passed to the `java` command (example: "-verbose:class")
# - JAVA_OPTS_APPEND: User specified Java options to be appended to generated options
# in JAVA_OPTS (example: "-Dsome.property=foo")
# - JAVA_MAX_MEM_RATIO: Is used when no `-Xmx` option is given in JAVA_OPTS. This is
@@ -78,7 +75,7 @@
# accessed directly. (example: "foo.example.com,bar.example.com")
#
###
FROM registry.access.redhat.com/ubi9/openjdk-21:1.21
FROM registry.access.redhat.com/ubi8/openjdk-11:1.14
ENV LANGUAGE='en_US:en'
@@ -91,8 +88,7 @@ COPY --chown=185 build/quarkus-app/quarkus/ /deployments/quarkus/
EXPOSE 8080
USER 185
ENV JAVA_OPTS_APPEND="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager"
ENV AB_JOLOKIA_OFF=""
ENV JAVA_OPTS="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager"
ENV JAVA_APP_JAR="/deployments/quarkus-run.jar"
ENTRYPOINT [ "/opt/jboss/container/java/run/run-java.sh" ]
@@ -3,7 +3,7 @@
#
# Before building the container image run:
#
# ./gradlew build -Dquarkus.package.jar.type=legacy-jar
# ./gradlew build -Dquarkus.package.type=legacy-jar
#
# Then, build the image with:
#
@@ -14,9 +14,7 @@
# docker run -i --rm -p 8080:8080 quarkus/kontor-quarkus-legacy-jar
#
# If you want to include the debug port into your docker image
# you will have to expose the debug port (default 5005 being the default) like this : EXPOSE 8080 5005.
# Additionally you will have to set -e JAVA_DEBUG=true and -e JAVA_DEBUG_PORT=*:5005
# when running the container
# you will have to expose the debug port (default 5005) like this : EXPOSE 8080 5005
#
# Then run the container using :
#
@@ -26,8 +24,7 @@
# This scripts computes the command line to execute your Java application, and
# includes memory/GC tuning.
# You can configure the behavior using the following environment properties:
# - JAVA_OPTS: JVM options passed to the `java` command (example: "-verbose:class") - Be aware that this will override
# the default JVM options, use `JAVA_OPTS_APPEND` to append options
# - JAVA_OPTS: JVM options passed to the `java` command (example: "-verbose:class")
# - JAVA_OPTS_APPEND: User specified Java options to be appended to generated options
# in JAVA_OPTS (example: "-Dsome.property=foo")
# - JAVA_MAX_MEM_RATIO: Is used when no `-Xmx` option is given in JAVA_OPTS. This is
@@ -78,7 +75,7 @@
# accessed directly. (example: "foo.example.com,bar.example.com")
#
###
FROM registry.access.redhat.com/ubi9/openjdk-21:1.21
FROM registry.access.redhat.com/ubi8/openjdk-11:1.14
ENV LANGUAGE='en_US:en'
@@ -88,7 +85,6 @@ COPY build/*-runner.jar /deployments/quarkus-run.jar
EXPOSE 8080
USER 185
ENV JAVA_OPTS_APPEND="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager"
ENV AB_JOLOKIA_OFF=""
ENV JAVA_OPTS="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager"
ENV JAVA_APP_JAR="/deployments/quarkus-run.jar"
ENTRYPOINT [ "/opt/jboss/container/java/run/run-java.sh" ]
@@ -3,7 +3,7 @@
#
# Before building the container image run:
#
# ./gradlew build -Dquarkus.native.enabled=true
# ./gradlew build -Dquarkus.package.type=native
#
# Then, build the image with:
#
@@ -13,17 +13,15 @@
#
# docker run -i --rm -p 8080:8080 quarkus/kontor-quarkus
#
# The ` registry.access.redhat.com/ubi9/ubi-minimal:9.5` base image is based on UBI 9.
# To use UBI 8, switch to `quay.io/ubi8/ubi-minimal:8.10`.
###
FROM registry.access.redhat.com/ubi9/ubi-minimal:9.5
FROM registry.access.redhat.com/ubi8/ubi-minimal:8.6
WORKDIR /work/
RUN chown 1001 /work \
&& chmod "g+rwX" /work \
&& chown 1001:root /work
COPY --chown=1001:root --chmod=0755 build/*-runner /work/application
COPY --chown=1001:root build/*-runner /work/application
EXPOSE 8080
USER 1001
ENTRYPOINT ["./application", "-Dquarkus.http.host=0.0.0.0"]
CMD ["./application", "-Dquarkus.http.host=0.0.0.0"]
@@ -6,7 +6,7 @@
#
# Before building the container image run:
#
# ./gradlew build -Dquarkus.native.enabled=true
# ./gradlew build -Dquarkus.package.type=native
#
# Then, build the image with:
#
@@ -16,17 +16,15 @@
#
# docker run -i --rm -p 8080:8080 quarkus/kontor-quarkus
#
# The `quay.io/quarkus/ubi9-quarkus-micro-image:2.0` base image is based on UBI 9.
# To use UBI 8, switch to `quay.io/quarkus/quarkus-micro-image:2.0`.
###
FROM quay.io/quarkus/ubi9-quarkus-micro-image:2.0
FROM quay.io/quarkus/quarkus-micro-image:1.0
WORKDIR /work/
RUN chown 1001 /work \
&& chmod "g+rwX" /work \
&& chown 1001:root /work
COPY --chown=1001:root --chmod=0755 build/*-runner /work/application
COPY --chown=1001:root build/*-runner /work/application
EXPOSE 8080
USER 1001
ENTRYPOINT ["./application", "-Dquarkus.http.host=0.0.0.0"]
CMD ["./application", "-Dquarkus.http.host=0.0.0.0"]
@@ -0,0 +1,95 @@
package de.thpeetz.kontor.comics;
import javax.inject.Inject;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import de.thpeetz.kontor.comics.service.ArtistService;
import de.thpeetz.kontor.comics.service.ComicsService;
import de.thpeetz.kontor.comics.service.PublisherService;
@Path("/kontor/comics")
public class ComicsResource {
@Inject
transient ComicsService comicsService;
@Inject
transient ArtistService artistService;
@Inject
transient PublisherService publisherService;
@GET
@Produces(MediaType.TEXT_HTML)
@Path("/")
public String showComicList() {
return comicsService.showComicList();
}
@GET
@Produces(MediaType.TEXT_HTML)
@Path("/artist")
public String showArtistList() {
return artistService.showArtistList();
}
@GET
@Produces(MediaType.APPLICATION_JSON)
@Path("/artist")
public String showArtistListJson() {
return artistService.showArtistList();
}
@GET
@Produces(MediaType.TEXT_HTML)
@Path("/artist/view/{artist_id}")
public String showArtist(String artist_id) {
return artistService.showArtist(artist_id);
}
@GET
@Produces(MediaType.TEXT_HTML)
@Path("/artist/create")
public String showArtistCreation() {
return artistService.showArtistCreation();
}
@POST
@Produces(MediaType.TEXT_HTML)
@Path("/artist/create")
public String validateArtistDetails() {
return artistService.validateArtistDetails();
}
@GET
@Produces(MediaType.TEXT_HTML)
@Path("/publisher")
public String showPublisherList() {
return publisherService.showPublisherList();
}
@GET
@Produces(MediaType.TEXT_HTML)
@Path("/publisher/view/{publisher_id}")
public String showPublisher(String publisher_id) {
return publisherService.showPublisher(publisher_id);
}
@GET
@Produces(MediaType.TEXT_HTML)
@Path("/publisher/create")
public String showPublisherCreation() {
return publisherService.showPublisherCreation();
}
@POST
@Produces(MediaType.TEXT_HTML)
@Path("/publisher/create")
public String validatePublisherDetails() {
return publisherService.validatePublisherDetails();
}
}
@@ -0,0 +1,28 @@
package de.thpeetz.kontor.comics.service;
import javax.enterprise.context.ApplicationScoped;
@ApplicationScoped
public class ArtistService {
public String showArtistList() {
// TODO Auto-generated method stub
return null;
}
public String showArtist(String artist_id) {
// TODO Auto-generated method stub
return null;
}
public String showArtistCreation() {
// TODO Auto-generated method stub
return null;
}
public String validateArtistDetails() {
// TODO Auto-generated method stub
return null;
}
}
@@ -0,0 +1,12 @@
package de.thpeetz.kontor.comics.service;
import javax.enterprise.context.ApplicationScoped;
@ApplicationScoped
public class ComicsService {
public String showComicList() {
return "comics";
}
}
@@ -0,0 +1,28 @@
package de.thpeetz.kontor.comics.service;
import javax.enterprise.context.ApplicationScoped;
@ApplicationScoped
public class PublisherService {
public String showPublisherList() {
// TODO Auto-generated method stub
return null;
}
public String showPublisher(String publisher_id) {
// TODO Auto-generated method stub
return null;
}
public String showPublisherCreation() {
// TODO Auto-generated method stub
return null;
}
public String validatePublisherDetails() {
// TODO Auto-generated method stub
return null;
}
}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,3 @@
# Your configuration properties
quarkus.http.test-port=8083
quarkus.http.test-ssl-port=8446
@@ -0,0 +1,8 @@
package de.thpeetz.kontor.comics;
import io.quarkus.test.junit.QuarkusIntegrationTest;
@QuarkusIntegrationTest
public class ComicsResourceIT extends ComicsResourceTest {
}
@@ -0,0 +1,19 @@
package de.thpeetz.kontor.comics;
import org.junit.jupiter.api.Test;
import static io.restassured.RestAssured.given;
import static org.hamcrest.CoreMatchers.is;
import io.quarkus.test.junit.QuarkusTest;
@QuarkusTest
class ComicsResourceTest {
@Test
void testComicsEndpoint() {
given().when().get("/kontor/comics")
.then()
.statusCode(200)
.body(is("comics"));
}
}
@@ -1,5 +1,5 @@
### STAGE 1: Build ###
FROM docker.io/node:22-alpine AS build
FROM node:22.15-alpine AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm install
@@ -7,9 +7,8 @@ COPY . .
RUN npm run build
### STAGE 2: Run ###
FROM docker.io/library/nginx:stable-alpine
FROM nginx:1.17.1-alpine
COPY nginx.conf /etc/nginx/conf.d/default.conf
COPY --from=build /app/dist/kontor-angular/browser /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
-1
View File
@@ -1,5 +1,4 @@
server {
listen 8800;
# Root-Verzeichnis für den Server setzen (wir kopieren unsere Anwendung hierher)
root /usr/share/nginx/html;
@@ -1,5 +1,5 @@
# ---------- Stage 1: Build ----------
FROM docker.io/library/golang:1.25-alpine AS builder
FROM golang:1.25-alpine AS builder
# Set the working directory
WORKDIR /app
# Ensure a portable, static-ish binary
@@ -18,7 +18,11 @@ FROM alpine:latest
WORKDIR /app
# Install runtime dependencies you actually need
RUN apk add --no-cache ca-certificates tzdata curl
# Create non-root user for security
RUN addgroup -S kontor && adduser -S -G kontor -H -s /sbin/nologin kontor
# Copy the binary and set ownership
COPY --from=builder /app/kontor /app/kontor
COPY --from=builder --chown=kontor:kontor /app/kontor /app/kontor
# Run as non-root user
USER kontor
# Set the entrypoint command
ENTRYPOINT ["/app/kontor"]
@@ -1,8 +1,8 @@
package main
import (
"kontor-api-fiber/pkg/handler"
"kontor-api-fiber/pkg/schema"
"kontor-api-go/pkg/handler"
"kontor-api-go/pkg/schema"
"log"
jwtware "github.com/gofiber/contrib/jwt"
@@ -23,19 +23,17 @@ func main() {
// SigningKey: jwtware.SigningKey{Key: []byte("secret")},
// }))
//app.Use(logger.New())
// app.Use(logger.New())
app.Get("/health", handler.GetHealth)
app.Post("/login", handler.Login)
api := app.Group("/api", logger.New(), jwtware.New(jwtware.Config{
SigningKey: jwtware.SigningKey{Key: []byte("secret")},
}))
api.Use(logger.New())
handler.SetupComicRoutes(api)
handler.SetupMediaRoutes(api)
// Listen on port 8900
app.Listen(":8500")
app.Listen(":8900")
log.Println("Kontor finished")
}
+2 -8
View File
@@ -1,4 +1,4 @@
module kontor-api-fiber
module kontor-api-go
go 1.24.2
@@ -7,8 +7,6 @@ require (
github.com/andybalholm/brotli v1.2.0 // indirect
github.com/clipperhouse/stringish v0.1.1 // indirect
github.com/clipperhouse/uax29/v2 v2.3.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/fatih/color v1.18.0 // indirect
github.com/gofiber/contrib/jwt v1.1.2 // indirect
github.com/gofiber/fiber/v2 v2.52.10 // indirect
github.com/golang-jwt/jwt/v5 v5.3.0 // indirect
@@ -19,15 +17,12 @@ require (
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.19 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/puzpuzpuz/xsync/v3 v3.5.1 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/stretchr/testify v1.11.1 // indirect
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // indirect
github.com/uptrace/bun v1.2.16 // indirect
github.com/uptrace/bun/dialect/pgdialect v1.2.16 // indirect
github.com/uptrace/bun/driver/pgdriver v1.2.16 // indirect
github.com/uptrace/bun/extra/bundebug v1.2.16 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.68.0 // indirect
github.com/valyala/tcplisten v1.0.0 // indirect
@@ -36,7 +31,6 @@ require (
go.opentelemetry.io/otel v1.38.0 // indirect
go.opentelemetry.io/otel/trace v1.38.0 // indirect
golang.org/x/crypto v0.45.0 // indirect
golang.org/x/sys v0.39.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
golang.org/x/sys v0.38.0 // indirect
mellium.im/sasl v0.3.2 // indirect
)
@@ -7,10 +7,6 @@ github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfa
github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
github.com/clipperhouse/uax29/v2 v2.3.0 h1:SNdx9DVUqMoBuBoW3iLOj4FQv3dN5mDtuqwuhIGpJy4=
github.com/clipperhouse/uax29/v2 v2.3.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/gofiber/contrib/jwt v1.1.2 h1:GmWnOqT4A15EkA8IPXwSpvNUXZR4u5SMj+geBmyLAjs=
github.com/gofiber/contrib/jwt v1.1.2/go.mod h1:CpIwrkUQ3Q6IP8y9n3f0wP9bOnSKx39EDp2fBVgMFVk=
github.com/gofiber/fiber v1.14.6 h1:QRUPvPmr8ijQuGo1MgupHBn8E+wW0IKqiOvIZPtV70o=
@@ -42,14 +38,10 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/puzpuzpuz/xsync/v3 v3.5.1 h1:GJYJZwO6IdxN/IKbneznS6yPkVC+c3zyY/j19c++5Fg=
github.com/puzpuzpuz/xsync/v3 v3.5.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc h1:9lRDQMhESg+zvGYmW5DyG0UqvY96Bu5QYsTLvCHdrgo=
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc/go.mod h1:bciPuU6GHm1iF1pBvUfxfsH0Wmnc2VbpgvbI9ZWuIRs=
github.com/uptrace/bun v1.2.16 h1:QlObi6ZIK5Ao7kAALnh91HWYNZUBbVwye52fmlQM9kc=
@@ -58,8 +50,6 @@ github.com/uptrace/bun/dialect/pgdialect v1.2.16 h1:KFNZ0LxAyczKNfK/IJWMyaleO6eI
github.com/uptrace/bun/dialect/pgdialect v1.2.16/go.mod h1:IJdMeV4sLfh0LDUZl7TIxLI0LipF1vwTK3hBC7p5qLo=
github.com/uptrace/bun/driver/pgdriver v1.2.16 h1:b1kpXKUxtTSGYow5Vlsb+dKV3z0R7aSAJNfMfKp61ZU=
github.com/uptrace/bun/driver/pgdriver v1.2.16/go.mod h1:H6lUZ9CBfp1X5Vq62YGSV7q96/v94ja9AYFjKvdoTk0=
github.com/uptrace/bun/extra/bundebug v1.2.16 h1:3OXAfHTU4ydu2+4j05oB1BxPx6+ypdWIVzTugl/7zl0=
github.com/uptrace/bun/extra/bundebug v1.2.16/go.mod h1:vk6R/1i67/S2RvUI5AH/m3P5e67mOkfDCmmCsAPUumo=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.16.0/go.mod h1:YOKImeEosDdBPnxc0gy7INqi3m1zK6A+xl6TwOBhHCA=
@@ -88,11 +78,6 @@ golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
mellium.im/sasl v0.3.2 h1:PT6Xp7ccn9XaXAnJ03FcEjmAn7kK1x7aoXV6F+Vmrl0=
mellium.im/sasl v0.3.2/go.mod h1:NKXDi1zkr+BlMHLQjY3ofYuU4KSPFxknb8mfEu6SveY=
@@ -2,8 +2,8 @@ package handler
import (
"context"
"kontor-api-fiber/pkg/schema"
"kontor-api-fiber/pkg/utils"
"kontor-api-go/pkg/schema"
"kontor-api-go/pkg/utils"
"github.com/gofiber/fiber/v2"
"github.com/golang-jwt/jwt/v5"
+35
View File
@@ -0,0 +1,35 @@
package handler
import (
"context"
"kontor-api-go/pkg/schema"
"log"
"github.com/gofiber/fiber/v2"
"github.com/uptrace/bun"
)
func SetupComicRoutes(api fiber.Router) {
comics := api.Group("/comics")
comics.Get("/comics", GetAllComics)
}
func GetAllComics(c *fiber.Ctx) error {
var comics []schema.Comic
var err error
var db *bun.DB
ctx := context.Background()
db, err = schema.GetDatabase()
if err != nil {
log.Fatal(err)
}
err = db.NewSelect().Model(&comics).Relation("Publisher").Scan(ctx)
if err != nil {
log.Fatal(err)
return fiber.NewError(fiber.StatusInternalServerError)
}
return c.JSON(comics)
}
@@ -4,7 +4,7 @@ import (
"context"
"encoding/json"
"fmt"
"kontor-api-fiber/pkg/schema"
"kontor-api-go/pkg/schema"
"log"
"time"
+45
View File
@@ -0,0 +1,45 @@
package schema
import (
"time"
"github.com/uptrace/bun"
)
type Publisher struct {
bun.BaseModel `bun:"table:publisher"`
ID string `bun:"id,pk"`
CreatedAt time.Time `bun:"created_date,nullzero,notnull,default:current_timestamp"`
UpdatedAt time.Time `bun:"last_modified_date,nullzero,notnull,default:current_timestamp"`
Version int `bun:"version,default:0"`
Name string `bun:"name,unique:name"`
WebLink string `bun:"weblink"`
ParentPublisherID *string `bun:"parent_publisher_id"`
ParentPublisher *Publisher `bun:"rel:belongs-to,join:parent_publisher_id=id"`
Imprints []Publisher `bun:"rel:has-many,join:id=parent_publisher_id"`
Comics []Comic `bun:"rel:has-many,join:id=publisher_id"`
}
type Comic struct {
bun.BaseModel `bun:"table:comic"`
ID string `bun:"id,pk"`
CreatedAt time.Time `bun:"created_date,nullzero,notnull,default:current_timestamp"`
UpdatedAt time.Time `bun:"last_modified_date,nullzero,notnull,default:current_timestamp"`
Version int `bun:"version,default:0"`
Title string `bun:"title,unique:title,notnull"`
CurrentOrder bool `bun:"current_order"`
Completed bool `bun:"completed"`
WebLink string `bun:"weblink"`
PublisherID *string `bun:"publisher_id"`
Publisher *Publisher `bun:"rel:belongs-to,join:publisher_id=id"`
// Issues []Issue `bun:"rel:has-many,join:id=comic_id"`
// StoryArcs []StoryArc `bun:"rel:has-many,join:id=comic_id"`
// TradePaperbacks []TradePaperback `bun:"rel:has-many,join:id=comic_id"`
// Volumes []Volume `bun:"rel:has-many,join:id=comic_id"`
// ComicWorks []ComicWork `bun:"rel:has-many,join:id=comic_id"`
}
@@ -31,7 +31,7 @@ type MediaActor struct {
CreatedAt time.Time `bun:"created_date,nullzero,notnull,default:current_timestamp"`
UpdatedAt time.Time `bun:"last_modified_date,nullzero,notnull,default:current_timestamp"`
Version int `bun:"version,default:0"`
Name string `bun:"name"`
Name string `bun:"name,unique:name"`
WebLink string `bun:"url,unique:url"`
MediaActorFiles []MediaActorFile `bun:"rel:has-many,join:id=media_actor_id"`
@@ -1,7 +1,7 @@
package utils
import (
"kontor-api-echo/pkg/schema"
"kontor-api-go/pkg/schema"
"time"
"github.com/golang-jwt/jwt/v5"
-38
View File
@@ -1,38 +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
RUN apt-get update && apt-get install --no-install-recommends -y curl
WORKDIR /app
COPY /src src
COPY --from=builder /app/.venv .venv
# Set up environment variables for production
ENV PATH="/app/.venv/bin:$PATH"
# Expose the specified port for FastAPI
EXPOSE 8200
# Start the application with Uvicorn in production mode, using environment variable references
CMD ["uvicorn", "src.main:kontor", "--log-level", "info", "--host", "0.0.0.0" , "--port", "8200"]
+3 -1
View File
@@ -1,7 +1,7 @@
## ------------------------------- 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 && \
apt-get clean && rm -rf /var/lib/apt/lists/*
# Download the latest installer, install it and then remove it
@@ -34,6 +34,8 @@ FROM python:3.13-slim-bookworm AS production
#RUN --mount=type=secret,id=secret-key,target=secrets.json
RUN apt-get update && apt-get install --no-install-recommends -y curl
RUN useradd --create-home appuser
USER appuser
+1
View File
@@ -28,4 +28,5 @@ dependencies = [
"jinja2>=3.1.6",
"asyncpg>=0.30.0",
"bcrypt>=4.3.0",
"fastapi-jwt-auth>=0.5.0",
]
+9 -235
View File
@@ -1,237 +1,11 @@
"""
add router for different parts (like comics, tysc, media)
"""
from fastapi import APIRouter
from fastapi import APIRouter, Depends
from src.apis.version1.admin import mailaccount
from src.apis.version1.comics import (
artist,
publisher,
comic,
issue,
worktype,
volume,
storyarc,
comicwork,
issuework,
)
from src.apis.version1.media import (
actor,
file,
mediaactorfile,
mediavideo,
mediaarticle,
)
from src.apis.version1.tysc import (
card,
cardset,
fieldposition,
player,
rooster,
sport,
team,
vendor,
)
from src.core.security import get_current_user_from_token
from src.apis.version1 import comic, mediaactor, mediafile, mediaactorfile, tysc, admin
from src.apis.version1 import comic, media, tysc, admin
api_router = APIRouter(prefix="/api")
api_router.include_router(
comic.router,
prefix="/comics",
tags=["comics"],
dependencies=[Depends(get_current_user_from_token)],
)
api_router.include_router(
publisher.router,
prefix="/comics",
tags=["comics"],
dependencies=[Depends(get_current_user_from_token)],
)
api_router.include_router(
artist.router,
prefix="/comics",
tags=["comics"],
dependencies=[Depends(get_current_user_from_token)],
)
api_router.include_router(
issue.router,
prefix="/comics",
tags=["comics"],
dependencies=[Depends(get_current_user_from_token)],
)
api_router.include_router(
worktype.router,
prefix="/comics",
tags=["comics"],
dependencies=[Depends(get_current_user_from_token)],
)
api_router.include_router(
volume.router,
prefix="/comics",
tags=["comics"],
dependencies=[Depends(get_current_user_from_token)],
)
api_router.include_router(
storyarc.router,
prefix="/comics",
tags=["comics"],
dependencies=[Depends(get_current_user_from_token)],
)
api_router.include_router(
comicwork.router,
prefix="/comics",
tags=["comics"],
dependencies=[Depends(get_current_user_from_token)],
)
api_router.include_router(
issuework.router,
prefix="/comics",
tags=["comics"],
dependencies=[Depends(get_current_user_from_token)],
)
api_router.include_router(
file.router,
prefix="/media",
tags=["media"],
dependencies=[Depends(get_current_user_from_token)],
)
api_router.include_router(
mediavideo.router,
prefix="/media",
tags=["media"],
dependencies=[Depends(get_current_user_from_token)],
)
api_router.include_router(
mediaarticle.router,
prefix="/media",
tags=["media"],
dependencies=[Depends(get_current_user_from_token)],
)
api_router.include_router(
actor.router,
prefix="/media",
tags=["media"],
dependencies=[Depends(get_current_user_from_token)],
)
api_router.include_router(
mediaactorfile.router,
prefix="/media",
tags=["media"],
dependencies=[Depends(get_current_user_from_token)],
)
api_router.include_router(
sport.router,
prefix="/tysc",
tags=["tysc"],
dependencies=[Depends(get_current_user_from_token)],
)
api_router.include_router(
player.router,
prefix="/tysc",
tags=["tysc"],
dependencies=[Depends(get_current_user_from_token)],
)
api_router.include_router(
team.router,
prefix="/tysc",
tags=["tysc"],
dependencies=[Depends(get_current_user_from_token)],
)
api_router.include_router(
fieldposition.router,
prefix="/tysc",
tags=["tysc"],
dependencies=[Depends(get_current_user_from_token)],
)
api_router.include_router(
rooster.router,
prefix="/tysc",
tags=["tysc"],
dependencies=[Depends(get_current_user_from_token)],
)
api_router.include_router(
vendor.router,
prefix="/tysc",
tags=["tysc"],
dependencies=[Depends(get_current_user_from_token)],
)
api_router.include_router(
cardset.router,
prefix="/tysc",
tags=["tysc"],
dependencies=[Depends(get_current_user_from_token)],
)
api_router.include_router(
card.router,
prefix="/tysc",
tags=["tysc"],
dependencies=[Depends(get_current_user_from_token)],
)
api_router.include_router(
article.router,
prefix="/bookshelf",
tags=["bookshelf"],
dependencies=[Depends(get_current_user_from_token)],
)
api_router.include_router(
bookshelf_publisher.router,
prefix="/bookshelf",
tags=["bookshelf"],
dependencies=[Depends(get_current_user_from_token)],
)
api_router.include_router(
book.router,
prefix="/bookshelf",
tags=["bookshelf"],
dependencies=[Depends(get_current_user_from_token)],
)
api_router.include_router(
author.router,
prefix="/bookshelf",
tags=["bookshelf"],
dependencies=[Depends(get_current_user_from_token)],
)
api_router.include_router(
articleauthor.router,
prefix="/bookshelf",
tags=["bookshelf"],
dependencies=[Depends(get_current_user_from_token)],
)
api_router.include_router(
bookauthor.router,
prefix="/bookshelf",
tags=["bookshelf"],
dependencies=[Depends(get_current_user_from_token)],
)
api_router.include_router(
profile.router,
prefix="/user",
tags=["user"],
dependencies=[Depends(get_current_user_from_token)],
)
api_router.include_router(
token.router,
prefix="/user",
tags=["user"],
dependencies=[Depends(get_current_user_from_token)],
)
api_router.include_router(
permission.router,
prefix="/user",
tags=["user"],
dependencies=[Depends(get_current_user_from_token)],
)
api_router.include_router(
assignment.router,
prefix="/user",
tags=["user"],
dependencies=[Depends(get_current_user_from_token)],
)
api_router.include_router(
mailaccount.router,
prefix="/admin",
tags=["admin"],
dependencies=[Depends(get_current_user_from_token)],
)
api_router = APIRouter(prefix="/api")
api_router.include_router(comic.router, prefix="/comics", tags=["comics"])
api_router.include_router(mediafile.router, prefix="/media", tags=["media"])
api_router.include_router(mediaactor.router, prefix="/media", tags=["media"])
api_router.include_router(mediaactorfile.router, prefix="/media", tags=["media"])
api_router.include_router(tysc.router, prefix="/tysc", tags=["tysc"])
api_router.include_router(admin.router, prefix="/login", tags=["login"])
-48
View File
@@ -1,48 +0,0 @@
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
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
+25 -42
View File
@@ -1,35 +1,37 @@
import logging
from datetime import timedelta
from typing import Annotated
import bcrypt
from fastapi import APIRouter, HTTPException, status, Response, Depends
from fastapi import APIRouter, HTTPException, status, Depends, Response
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.core.security import create_access_token, authenticate_user, get_current_active_user
from src.db.models.admin import Profile
from src.db.repository.admin import get_profile
from src.schema.admin import Token
from src.schema.admin import Token, ProfileModel
from src.webapps.auth.forms import LoginForm
router = APIRouter()
def authenticate_user(username: str, password: str, db: SessionDep) -> Profile | None:
user = get_profile(username=username, db=db)
print(user)
@router.post("/token")
def login_for_access_token(form_data: Annotated[OAuth2PasswordRequestForm, Depends()]) -> Token:
user = authenticate_user(form_data.username, form_data.password)
if not user:
return None
if bcrypt.checkpw(password.encode(), user.password.encode()):
print("User successful authenticated")
else:
logging.info("Authentication failed!")
return user
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
headers={"WWW-Authenticate": "Bearer"},
)
access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
access_token = create_access_token(
data={"sub": user.email, "scope": " ".join(form_data.scopes)}, expires_delta=access_token_expires
)
return Token(access_token=access_token, token_type="bearer")
@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)
# @router.post("/token-cookie", response_model=Token)
def login_for_token_cookie(response: Response, form_data: LoginForm = Depends()):
user = authenticate_user(form_data.username, form_data.password)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
@@ -45,25 +47,6 @@ def login_for_access_token(response: Response, db: SessionDep, form_data: OAuth2
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
@router.get("/profile", response_model=ProfileModel)
async def read_profile(current_user: Annotated[Profile, Depends(get_current_active_user)]):
return current_user
@@ -1,74 +0,0 @@
from datetime import timedelta
from fastapi import APIRouter, Depends, HTTPException, Response, status
from fastapi.security import OAuth2PasswordRequestForm
from src.core.config import settings
from src.core.log_conf import logger
from src.core.security import (
authenticate_user_by_email,
authenticate_user_by_username,
create_access_token,
)
from src.schema.admin.login import LoginRequest
from src.schema.admin.token import Token
from src.webapps.auth.forms import LoginForm
login_router = APIRouter()
@login_router.post(
"/login",
tags=["login"],
summary="Login and get token",
response_description="Return HTTP status code 200 (OK)",
status_code=status.HTTP_200_OK,
)
def login(request: LoginRequest) -> Token:
logger.info("login with %s", request.email)
user = authenticate_user_by_email(str(request.email), str(request.password))
scopes = ["admin", "read"]
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
headers={"WWW-Authenticate": "Bearer"},
)
access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
access_token = create_access_token(
data={"sub": user.email, "scope": " ".join(scopes)},
expires_delta=access_token_expires,
)
return Token(access_token=access_token, token_type="bearer")
@login_router.post("/token", tags=["login"], summary="Login for access token")
# async def login_for_access_token(form_data: Annotated[OAuth2PasswordRequestForm, Depends()]) -> Token:
async def login_for_access_token(
form_data: OAuth2PasswordRequestForm = Depends(),
) -> Token:
user = authenticate_user_by_username(form_data.username, form_data.password)
if not user:
raise HTTPException(status_code=400, detail="Incorrect username or password")
access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
access_token = create_access_token(
data={"sub": user.user_name, "scope": " ".join(form_data.scopes)},
expires_delta=access_token_expires,
)
return Token(access_token=access_token, token_type="bearer")
def login_for_token_cookie(response: Response, form_data: LoginForm = Depends()):
user = authenticate_user_by_email(str(form_data.username), str(form_data.password))
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"}
@@ -1,32 +0,0 @@
from typing import List
from fastapi import APIRouter, HTTPException
from src.db.models.admin import MailAccount
from src.db.session import SessionDep
from src.schema.admin.mailaccount import MailAccountResponse, to_response
router = APIRouter()
@router.get("/mailaccounts", response_model=List[MailAccountResponse])
def get_all_mailaccounts(db: SessionDep) -> List[MailAccountResponse]:
"""
return all MailAccounts as JSON.
"""
results: List[MailAccountResponse] = []
mailaccounts = db.query(MailAccount).all()
for mailaccount in mailaccounts:
response = to_response(mailaccount)
results.append(response)
return results
@router.get("/mailaccounts/{mailaccount_id}", response_model=MailAccountResponse)
def get_mailaccount(mailaccount_id: str, db: SessionDep) -> MailAccountResponse:
"""
return MailAccounts by id.
"""
mailaccount = db.get(MailAccount, mailaccount_id)
if mailaccount is None:
raise HTTPException(status_code=409, detail="Mailaccount could not be found")
response = to_response(mailaccount)
return response
@@ -1,20 +0,0 @@
from typing import List
from fastapi import APIRouter
from src.db.models.bookshelf import Article
from src.db.session import SessionDep
from src.schema.bookshelf.article import ArticleResponse, to_response
router = APIRouter()
@router.get("/articles", response_model=List[ArticleResponse])
def get_all_artists(db: SessionDep) -> List[ArticleResponse]:
results: List[ArticleResponse] = []
articles = db.query(Article).all()
for article in articles:
response = to_response(article)
results.append(response)
return results
@@ -1,19 +0,0 @@
from typing import List
from fastapi import APIRouter
from src.db.models.bookshelf import ArticleAuthor
from src.db.session import SessionDep
from src.schema.bookshelf.articleauthor import ArticleAuthorResponse, to_response
router = APIRouter()
@router.get("/articleauthors", response_model=List[ArticleAuthorResponse])
def get_all_artists(db: SessionDep) -> List[ArticleAuthorResponse]:
results: List[ArticleAuthorResponse] = []
articleauthors = db.query(ArticleAuthor).all()
for articleauthor in articleauthors:
response = to_response(articleauthor)
results.append(response)
return results
@@ -1,26 +0,0 @@
from typing import List
from fastapi import APIRouter, HTTPException
from src.db.models.bookshelf import Author
from src.db.session import SessionDep
from src.schema.bookshelf.author import AuthorResponse, to_response
router = APIRouter()
@router.get("/authors", response_model=List[AuthorResponse])
def get_all_authors(db: SessionDep) -> List[AuthorResponse]:
results: List[AuthorResponse] = []
authors = db.query(Author).all()
for author in authors:
response = to_response(author)
results.append(response)
return results
@router.get("/authors/{author_id}", response_model=AuthorResponse)
def get_author(author_id: str, db: SessionDep) -> AuthorResponse:
author = db.get(Author, author_id)
if author is None:
raise HTTPException(status_code=404, detail="Author could not be found")
response = to_response(author)
return response
@@ -1,19 +0,0 @@
from typing import List
from fastapi import APIRouter
from src.db.models.bookshelf import Book
from src.db.session import SessionDep
from src.schema.bookshelf.book import BookResponse, to_response
router = APIRouter()
@router.get("/books", response_model=List[BookResponse])
def get_all_artists(db: SessionDep) -> List[BookResponse]:
results: List[BookResponse] = []
books = db.query(Book).all()
for book in books:
response = to_response(book)
results.append(response)
return results
@@ -1,19 +0,0 @@
from typing import List
from fastapi import APIRouter
from src.db.models.bookshelf import BookAuthor
from src.db.session import SessionDep
from src.schema.bookshelf.bookauthor import BookAuthorResponse, to_response
router = APIRouter()
@router.get("/bookauthors", response_model=List[BookAuthorResponse])
def get_all_artists(db: SessionDep) -> List[BookAuthorResponse]:
results: List[BookAuthorResponse] = []
bookauthors = db.query(BookAuthor).all()
for bookauthor in bookauthors:
response = to_response(bookauthor)
results.append(response)
return results
@@ -1,19 +0,0 @@
from typing import List
from fastapi import APIRouter
from src.db.models.bookshelf import BookshelfPublisher
from src.db.session import SessionDep
from src.schema.bookshelf.publisher import PublisherResponse, to_response
router = APIRouter()
@router.get("/publishers", response_model=List[PublisherResponse])
def get_all_artists(db: SessionDep) -> List[PublisherResponse]:
results: List[PublisherResponse] = []
publishers = db.query(BookshelfPublisher).all()
for publisher in publishers:
response = to_response(publisher)
results.append(response)
return results
+48 -12
View File
@@ -1,13 +1,25 @@
from typing import List, AnyStr
from typing import List
from fastapi import APIRouter, HTTPException, status
from src.apis.utils import SessionDep
from src.core.log_conf import logger
from src.db.models.comic import Artist, Comic, Issue, Publisher
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.db.repository.comics.comic import (
get_comic_details,
get_issue_details,
get_short_info,
list_comics,
)
from src.db.repository.comics.publisher import get_publisher_details
from src.db.session import SessionDep
from src.schema.comics.artist import ArtistCreation, ArtistResponse
from src.schema.comics.artist_details import ArtistDetailResponse
from src.schema.comics.comic import ComicResponse
from src.schema.comics.comic_details import ComicDetailsResponse
from src.schema.comics.issue_details import IssueDetailsResponse
from src.schema.comics.publisher import PublisherResponse
from src.schema.comics.publisher_details import PublisherDetailsResponse
router = APIRouter()
@@ -21,30 +33,36 @@ def get_all_comics(db: SessionDep) -> List[ComicResponse]:
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: str, db: SessionDep) -> ComicDetailsResponse:
comic = db.get(Comic, comic_id)
if comic is None:
raise HTTPException(status_code=404, detail="Comic could not be found")
logger.info(f"create ComicDetailsResponse for {comic}")
response: ComicDetailsResponse = get_comic_details(comic)
logger.info(f"ComicDetailsResponse: {response}")
return response
@router.get("/artists", response_model=List[ArtistResponse])
def get_all_artists(db: SessionDep) -> List[ArtistResponse]:
results: List[ArtistResponse] = []
artists = db.query(Artist).all()
for artist in artists:
results.append(ArtistResponse(id=artist.id, name=artist.name))
results.append(ArtistResponse(id=artist.id, name=str(artist.name))) # type: ignore
return results
@router.get("/artists/{artist_id}", response_model=ArtistDetailResponse)
def get_artist(artist_id: AnyStr, db: SessionDep) -> ArtistDetailResponse:
def get_artist(artist_id: str, db: SessionDep) -> ArtistDetailResponse:
artist = db.get(Artist, artist_id)
if artist is None:
raise HTTPException(status_code=404, detail="Artist could not be found")
response: ArtistDetailResponse = get_artist_details(artist)
return response
@router.post("/artists", status_code=status.HTTP_201_CREATED)
def add_artist(db: SessionDep, artist_creation: ArtistCreation) -> ArtistResponse:
artist: Artist = Artist()
@@ -54,9 +72,28 @@ def add_artist(db: SessionDep, artist_creation: ArtistCreation) -> ArtistRespons
db.commit()
except:
raise HTTPException(status_code=409, detail="Artist already added")
response = ArtistResponse(id=artist.id, name=artist.name)
response = ArtistResponse(id=artist.id, name=str(artist.name))
return response
@router.get("/publishers", response_model=List[PublisherResponse])
def get_all_publishers(db: SessionDep) -> List[PublisherResponse]:
results: List[PublisherResponse] = []
publishers = db.query(Publisher).all()
for publisher in publishers:
results.append(PublisherResponse(id=publisher.id, name=str(publisher.name)))
return results
@router.get("/publishers/{publisher_id}", response_model=PublisherDetailsResponse)
def get_publisher(publisher_id: str, db: SessionDep) -> PublisherDetailsResponse:
publisher = db.get(Publisher, publisher_id)
if publisher is None:
raise HTTPException(status_code=404, detail="Publisher could not be found")
response: PublisherDetailsResponse = get_publisher_details(publisher)
return response
@router.get("/issues", response_model=List[IssueDetailsResponse])
def get_issues(db: SessionDep) -> List[IssueDetailsResponse]:
results: List[IssueDetailsResponse] = []
@@ -64,4 +101,3 @@ def get_issues(db: SessionDep) -> List[IssueDetailsResponse]:
for issue in issues:
results.append(get_issue_details(issue))
return results
@@ -1,53 +0,0 @@
from typing import List
from fastapi import APIRouter, HTTPException, status
from src.db.models.comic import Artist
from src.db.repository.comics.artist import get_artist_details
from src.db.session import SessionDep
from src.schema.comics.artist import ArtistCreation, ArtistResponse, artist_to_response
from src.schema.comics.artist_details import ArtistDetailResponse
router = APIRouter()
@router.get("/artists", response_model=List[ArtistResponse])
def get_all_artists(db: SessionDep) -> List[ArtistResponse]:
results: List[ArtistResponse] = []
artists = db.query(Artist).all()
for artist in artists:
response = artist_to_response(artist)
results.append(response)
return results
@router.get("/artists/{artist_id}", response_model=ArtistResponse)
def get_artist(artist_id: str, db: SessionDep) -> ArtistResponse:
artist = db.get(Artist, artist_id)
if artist is None:
raise HTTPException(status_code=404, detail="Artist could not be found")
response: ArtistResponse = artist_to_response(artist)
return response
@router.get("/artists/details/{artist_id}", response_model=ArtistDetailResponse)
def get_artist_details_by_id(artist_id: str, db: SessionDep) -> ArtistDetailResponse:
artist = db.get(Artist, artist_id)
if artist is None:
raise HTTPException(status_code=404, detail="Artist could not be found")
response: ArtistDetailResponse = get_artist_details(artist)
return response
@router.post("/artists", status_code=status.HTTP_201_CREATED)
def add_artist(db: SessionDep, artist_creation: ArtistCreation) -> ArtistResponse:
artist: Artist = Artist()
setattr(artist, "name", artist_creation.name)
try:
db.add(artist)
db.commit()
except:
raise HTTPException(status_code=409, detail="Artist already added")
response = artist_to_response(artist)
return response
@@ -1,73 +0,0 @@
from typing import List, AnyStr
from fastapi import APIRouter, HTTPException, status
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
router = APIRouter()
@router.get("/comics")
def get_all_comics(db: SessionDep) -> List[ComicResponse]:
results: List[ComicResponse] = []
comics = db.query(Comic).all()
for comic in comics:
response = comic_to_response(comic)
results.append(response)
return results
@router.get("/comics/{comic_id}", response_model=ComicDetailsResponse)
def get_comic(comic_id: AnyStr, db: SessionDep) -> ComicDetailsResponse:
comic = db.get(Comic, comic_id)
if comic is None:
raise HTTPException(status_code=404, detail="Comic could not be found")
logger.info(f"create ComicDetailsResponse for {comic}")
response: ComicDetailsResponse = get_comic_details(comic)
logger.info(f"ComicDetailsResponse: {response}")
return response
@router.get("/artists", response_model=List[ArtistResponse])
def get_all_artists(db: SessionDep) -> List[ArtistResponse]: # type: ignore
results: List[ArtistResponse] = []
artists = db.query(Artist).all()
for artist in artists:
results.append(ArtistResponse(id=artist.id, name=str(artist.name))) # type: ignore
return results
@router.get("/artists/{artist_id}", response_model=ArtistDetailResponse)
def get_artist(artist_id: AnyStr, db: SessionDep) -> ArtistDetailResponse:
artist = db.get(Artist, artist_id)
if artist is None:
raise HTTPException(status_code=404, detail="Artist could not be found")
response: ArtistDetailResponse = get_artist_details(artist)
return response
@router.post("/artists", status_code=status.HTTP_201_CREATED)
def add_artist(db: SessionDep, artist_creation: ArtistCreation) -> ArtistResponse: # type: ignore
artist: Artist = Artist()
setattr(artist, "name", artist_creation.name)
try:
db.add(artist)
db.commit()
except:
raise HTTPException(status_code=409, detail="Artist already added")
response = ArtistResponse(id=artist.id, name=str(artist.name))
return response
@router.get("/issues", response_model=List[IssueDetailsResponse])
def get_issues(db: SessionDep) -> List[IssueDetailsResponse]: # type: ignore
results: List[IssueDetailsResponse] = []
issues = db.query(Issue).all()
for issue in issues:
results.append(get_issue_details(issue))
return results
@@ -1,27 +0,0 @@
from typing import List
from fastapi import APIRouter, HTTPException
from src.db.models.comic import ComicWork
from src.db.session import SessionDep
from src.schema.comics.comicwork import ComicWorkResponse, comicwork_to_response
router = APIRouter()
@router.get("/comicworks", response_model=List[ComicWorkResponse])
def get_comicworks(db: SessionDep) -> List[ComicWorkResponse]:
results: List[ComicWorkResponse] = []
worktypes = db.query(ComicWork).all()
for worktype in worktypes:
response = comicwork_to_response(worktype)
results.append(response)
return results
@router.get("/comicworks/{comicwork_id}", response_model=ComicWorkResponse)
def get_comicwork(comicwork_id: str, db: SessionDep) -> ComicWorkResponse:
worktype = db.get(ComicWork, comicwork_id)
if worktype is None:
raise HTTPException(status_code=404, detail="Comicwork could not be found")
response = comicwork_to_response(worktype)
return response
@@ -1,27 +0,0 @@
from typing import List
from fastapi import APIRouter, HTTPException
from src.db.models.comic import Issue
from src.db.session import SessionDep
from src.schema.comics.issue import IssueResponse, issue_to_response
router = APIRouter()
@router.get("/issues", response_model=List[IssueResponse])
def get_issues(db: SessionDep) -> List[IssueResponse]:
results: List[IssueResponse] = []
issues = db.query(Issue).all()
for issue in issues:
response = issue_to_response(issue)
results.append(response)
return results
@router.get("/issues/{issue_id}", response_model=IssueResponse)
def get_issue(issue_id: str, db: SessionDep) -> IssueResponse:
issue = db.get(Issue, issue_id)
if issue is None:
raise HTTPException(status_code=404, detail="Issue could not be found")
response = issue_to_response(issue)
return response
@@ -1,27 +0,0 @@
from typing import List
from fastapi import APIRouter, HTTPException
from src.db.models.comic import IssueWork
from src.db.session import SessionDep
from src.schema.comics.issuework import IssueWorkResponse, issuework_to_response
router = APIRouter()
@router.get("/issueworks", response_model=List[IssueWorkResponse])
def get_issueworks(db: SessionDep) -> List[IssueWorkResponse]:
results: List[IssueWorkResponse] = []
worktypes = db.query(IssueWork).all()
for worktype in worktypes:
response = issuework_to_response(worktype)
results.append(response)
return results
@router.get("/issueworks/{issuework_id}", response_model=IssueWorkResponse)
def get_issuework(issuework_id: str, db: SessionDep) -> IssueWorkResponse:
worktype = db.get(IssueWork, issuework_id)
if worktype is None:
raise HTTPException(status_code=404, detail="Issuework could not be found")
response = issuework_to_response(worktype)
return response
@@ -1,37 +0,0 @@
from typing import List
from fastapi import APIRouter, HTTPException
from src.db.models.comic import Publisher
from src.db.repository.comics.publisher import get_publisher_details
from src.db.session import SessionDep
from src.schema.comics.publisher import PublisherResponse, publisher_to_response
from src.schema.comics.publisher_details import PublisherDetailsResponse
router = APIRouter()
@router.get("/publishers", response_model=List[PublisherResponse])
def get_all_publishers(db: SessionDep) -> List[PublisherResponse]:
results: List[PublisherResponse] = []
publishers = db.query(Publisher).all()
for publisher in publishers:
response: PublisherResponse = publisher_to_response(publisher)
results.append(response)
return results
@router.get("/publishers/{publisher_id}", response_model=PublisherResponse)
def get_publisher(publisher_id: str, db: SessionDep) -> PublisherResponse:
publisher = db.get(Publisher, publisher_id)
if publisher is None:
raise HTTPException(status_code=404, detail="Publisher could not be found")
response: PublisherResponse = publisher_to_response(publisher)
return response
@router.get("/publishers/details/{publisher_id}", response_model=PublisherDetailsResponse)
def get_publisher_details_by_id(publisher_id: str, db: SessionDep) -> PublisherDetailsResponse:
publisher = db.get(Publisher, publisher_id)
if publisher is None:
raise HTTPException(status_code=404, detail="Publisher could not be found")
response: PublisherDetailsResponse = get_publisher_details(publisher)
return response
@@ -1,26 +0,0 @@
from typing import List
from fastapi import APIRouter, HTTPException
from src.db.models.comic import StoryArc
from src.db.session import SessionDep
from src.schema.comics.storyarc import StoryArcResponse, storyarc_to_response
router = APIRouter()
@router.get("/storyarcs", response_model=List[StoryArcResponse])
def get_storyarcs(db: SessionDep) -> List[StoryArcResponse]:
results: List[StoryArcResponse] = []
storyarcs = db.query(StoryArc).all()
for storyarc in storyarcs:
response = storyarc_to_response(storyarc)
results.append(response)
return results
@router.get("/storyarcs/{storyarc_id}", response_model=StoryArcResponse)
def get_storyarc(story_arc_id: str, db: SessionDep) -> StoryArcResponse:
storyarc = db.get(StoryArc, story_arc_id)
if storyarc is None:
raise HTTPException(status_code=404, detail="Storyarc could not be found")
response = storyarc_to_response(storyarc)
return response
@@ -1,26 +0,0 @@
from typing import List
from fastapi import APIRouter, HTTPException
from src.db.models.comic import Volume
from src.db.session import SessionDep
from src.schema.comics.volume import VolumeResponse, volume_to_response
router = APIRouter()
@router.get("/volumes", response_model=List[VolumeResponse])
def volumes(db: SessionDep) -> List[VolumeResponse]:
results: List[VolumeResponse] = []
worktypes = db.query(Volume).all()
for worktype in worktypes:
response = volume_to_response(worktype)
results.append(response)
return results
@router.get("/volumes/{volume_id}", response_model=VolumeResponse)
def get_volume(volume_id: str, db: SessionDep) -> VolumeResponse:
worktype = db.get(Volume, volume_id)
if worktype is None:
raise HTTPException(status_code=404, detail="Volume could not be found")
response = volume_to_response(worktype)
return response
@@ -1,27 +0,0 @@
from typing import List
from fastapi import APIRouter, HTTPException
from src.db.models.comic import WorkType
from src.db.session import SessionDep
from src.schema.comics.worktype import WorktypeResponse, worktype_to_response
router = APIRouter()
@router.get("/worktypes", response_model=List[WorktypeResponse])
def get_issues(db: SessionDep) -> List[WorktypeResponse]:
results: List[WorktypeResponse] = []
worktypes = db.query(WorkType).all()
for worktype in worktypes:
response = worktype_to_response(worktype)
results.append(response)
return results
@router.get("/worktypes/{worktype_id}", response_model=WorktypeResponse)
def get_issue(worktype_id: str, db: SessionDep) -> WorktypeResponse:
worktype = db.get(WorkType, worktype_id)
if worktype is None:
raise HTTPException(status_code=404, detail="Worktype could not be found")
response = worktype_to_response(worktype)
return response
+1 -1
View File
@@ -1,6 +1,6 @@
from fastapi import APIRouter, status
from src.schema.admin.healthcheck import HealthCheck
from src.schema.admin import HealthCheck
health_router = APIRouter()
-69
View File
@@ -1,69 +0,0 @@
from typing import List, AnyStr
from fastapi import APIRouter, status, HTTPException, Depends
from sqlalchemy import select, Sequence
from src.core.log_conf import logger
from src.apis.utils import SessionDep
from src.db.repository.media import create_new_mediafile
from src.schema.media.file import MediaFileResponse, Link, get_file_details, set_file
from src.db.models.media import MediaFile
router = APIRouter()
@router.get("/update-titles")
def update_titles(db: SessionDep) -> list[MediaFileResponse]:
results: list[MediaFileResponse] = []
files = db.query(MediaFile).filter(MediaFile.review == True).all()
for mediafile in files:
mediafile.update_title()
db.add(mediafile)
response = get_file_details(mediafile)
results.append(response)
db.commit()
return results
@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()
elif download:
files = db.query(MediaFile).filter(MediaFile.should_download == True).all()
else:
files = db.scalars(select(MediaFile)).all()
for mediafile in files:
response = get_file_details(mediafile)
results.append(response)
return results
@router.get("/files/{file_id}", response_model=MediaFileResponse)
def get_file(file_id: AnyStr, db: SessionDep) -> MediaFileResponse:
mediafile = db.get(MediaFile, file_id)
if not mediafile:
raise HTTPException(status_code=404, detail="MediaFile could not be found")
response = get_file_details(mediafile)
return response
@router.put("/files/{file_id}", response_model=MediaFileResponse)
def update_file(file_id: AnyStr, db: SessionDep, info: MediaFileResponse) -> MediaFileResponse:
mediaFile = db.get(MediaFile, file_id)
if not mediaFile:
raise HTTPException(status_code=404, detail="MediaFile could not be found")
set_file(info, mediaFile)
db.add(mediaFile)
db.commit()
return info
@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}")
try:
mediaFile: MediaFile = create_new_mediafile(new_link.url, db)
except:
raise HTTPException(status_code=409, detail="Link duplicate")
response = get_file_details(mediaFile)
return response
@@ -1,45 +0,0 @@
from typing import List
from fastapi import APIRouter, status, HTTPException
from src.core.log_conf import logger
from src.db.repository.media.actor import delete_mediaactor, import_mediaactor
from src.db.session import SessionDep
from src.schema.media.actor import MediaActorModel, MediaActorResponse, actor_to_response
from src.db.models.media import MediaActor
router = APIRouter()
@router.get("/actors", response_model=List[MediaActorResponse])
def get_all_actors(db: SessionDep) -> List[MediaActorResponse]:
results: list[MediaActorResponse] = []
actors = db.query(MediaActor).all()
for mediaactor in actors:
response = actor_to_response(mediaactor)
results.append(response)
return results
@router.get("/actors/{actor_id}", response_model=MediaActorResponse)
def get_actor(actor_id: str, db: SessionDep) -> MediaActorResponse:
media_actor = db.get(MediaActor, actor_id)
if not media_actor:
raise HTTPException(status_code=404, detail="MediaActor could not be found")
response = actor_to_response(media_actor)
return response
@router.delete("/actors/{actor_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_actor(actor_id: str, db: SessionDep):
media_actor = db.get(MediaActor, actor_id)
if not media_actor:
raise HTTPException(status_code=404, detail="MediaActor could not be found")
logger.info(f"delete MediaActor: {actor_id}")
delete_mediaactor(db, media_actor.id)
@router.post("/actors", status_code=status.HTTP_201_CREATED)
def add_actor(new_actor: MediaActorModel, db: SessionDep) -> MediaActorResponse:
logger.info(f"add actor {new_actor.url}")
try:
mediaActor: MediaActor = import_mediaactor(db, new_actor)
except Exception as exception:
raise HTTPException(status_code=409, detail=f"Link duplicate: {exception}")
response = actor_to_response(mediaActor)
return response
-163
View File
@@ -1,163 +0,0 @@
from typing import List
from fastapi import APIRouter, status, HTTPException
from src.core.log_conf import logger
from src.db.repository.media.actorfile import (
create_new_mediaactorfile,
delete_mediaactorfile,
)
from src.db.repository.media.file import delete_mediafile, import_mediafile
from src.db.session import SessionDep
from src.schema.media.actor import MediaActorResponse, actor_to_response
from src.schema.media.actorfile import MediaActorFileResponse, actorfile_to_response
from src.schema.media.file import (
MediaFileModel,
MediaFileResponse,
file_to_response,
file_to_model,
)
from src.db.models.media import MediaFile
router = APIRouter()
@router.get("/update-titles")
def update_titles(db: SessionDep) -> List[MediaFileResponse]:
"""
Update title for given MediaFile.
"""
results: list[MediaFileResponse] = []
files = db.query(MediaFile).filter(MediaFile.review.is_(True)).all()
for mediafile in files:
mediafile.update_title()
db.add(mediafile)
response = file_to_response(mediafile)
results.append(response)
db.commit()
return results
@router.get("/files", response_model=List[MediaFileResponse])
def get_all_files(
db: SessionDep, review: bool = False, download: bool = False
) -> List[MediaFileResponse]:
"""
Get all MediaFiles.
"""
results: List[MediaFileResponse] = []
files: List[MediaFile]
if review:
files = db.query(MediaFile).filter(MediaFile.review.is_(True)).all()
elif download:
files = db.query(MediaFile).filter(MediaFile.should_download.is_(True)).all()
else:
files = db.query(MediaFile).all()
for mediafile in files:
response = file_to_response(mediafile)
results.append(response)
return results
@router.get("/files/{file_id}", response_model=MediaFileResponse)
def get_file(file_id: str, db: SessionDep) -> MediaFileResponse:
"""
Get MediaFile with given id or return HTTPException.
"""
mediafile = db.get(MediaFile, file_id)
if not mediafile:
raise HTTPException(status_code=404, detail="MediaFile could not be found")
response = file_to_response(mediafile)
return response
@router.delete("/files/{file_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_file(file_id: str, db: SessionDep):
"""
Delete MediaFile by given id.
"""
mediafile = db.get(MediaFile, file_id)
if not mediafile:
raise HTTPException(status_code=404, detail="MediaFile could not be found")
logger.info("delete MediaFile: %s", file_id)
actor_files = mediafile.media_actor_files
logger.info("MediaActorFiles links %s", len(actor_files))
if len(actor_files) > 0:
logger.info("delete MediaActor relations first")
for actor_file in actor_files:
delete_mediaactorfile(db, actor_file.id)
delete_mediafile(db, mediafile.id)
@router.get("/files/{file_id}/actors", response_model=list[MediaActorResponse])
def get_file_actors(file_id: str, db: SessionDep) -> list[MediaActorResponse]:
"""
Get list of Actors for given MediaFile.
"""
mediafile = db.get(MediaFile, file_id)
if not mediafile:
raise HTTPException(status_code=404, detail="MediaFile could not be found")
actor_files = mediafile.media_actor_files
logger.info("already known actors: %s", actor_files)
results: list[MediaActorResponse] = []
for actor_file in actor_files:
response = actor_to_response(actor_file.media_actor)
results.append(response)
return results
@router.put("/files/{file_id}/actors", response_model=List[MediaActorFileResponse])
def update_file_actors(
file_id: str, db: SessionDep, actors: List[MediaActorResponse]
) -> List[MediaActorFileResponse]:
mediafile = db.get(MediaFile, file_id)
if not mediafile:
raise HTTPException(status_code=404, detail="MediaFile could not be found")
actor_files = mediafile.media_actor_files
logger.info("already known actors: %s", actor_files)
for actor in actors:
already_associated = False
for actor_file in actor_files:
if actor.id == actor_file.media_actor_id:
logger.info("alreay associated - do nothing")
already_associated = True
break
if not already_associated:
create_new_mediaactorfile(db, actor.id, mediafile.id)
db.refresh(mediafile)
actor_files = mediafile.media_actor_files
results: List[MediaActorFileResponse] = []
for actor_file in actor_files:
response = actorfile_to_response(actor_file)
results.append(response)
return results
@router.put("/files/{file_id}", response_model=MediaFileResponse)
def update_file(
file_id: str, db: SessionDep, info: MediaFileResponse
) -> MediaFileResponse:
"""
Update MediaFile with given id and data.
"""
media_file = db.get(MediaFile, file_id)
if not media_file:
raise HTTPException(status_code=404, detail="MediaFile could not be found")
file_to_model(info, media_file)
db.add(media_file)
db.commit()
mediafile = db.get(MediaFile, file_id)
if not mediafile:
raise HTTPException(status_code=404, detail="MediaFile could not be updated")
response = file_to_response(mediafile)
return response
@router.post("/files", status_code=status.HTTP_201_CREATED)
def add_file(new_file: MediaFileModel, db: SessionDep) -> MediaFileResponse:
logger.info("add mediafile %s", new_file)
try:
mediaFile: MediaFile = import_mediafile(db, new_file)
except:
raise HTTPException(status_code=409, detail="MediaFile duplicate")
response = file_to_response(mediaFile)
return response
@@ -1,25 +0,0 @@
from typing import List
from fastapi import APIRouter
from src.db.models.media import MediaArticle
from src.db.session import SessionDep
from src.schema.media.article import MediaArticleResponse, to_response
router = APIRouter()
@router.get("/articles", response_model=List[MediaArticleResponse])
def get_all_files(
db: SessionDep, review: bool = False, download: bool = False
) -> List[MediaArticleResponse]:
"""
Get all MediaVideos.
"""
results: List[MediaArticleResponse] = []
articles: List[MediaArticle]
articles = db.query(MediaArticle).all()
for article in articles:
response = to_response(article)
results.append(response)
return results
@@ -1,42 +0,0 @@
from typing import List
from fastapi import APIRouter, HTTPException
from src.db.models.media import MediaVideo
from src.db.session import SessionDep
from src.schema.media.video import MediaVideoResponse, video_to_response
router = APIRouter()
@router.get("/videos", response_model=List[MediaVideoResponse])
def get_all_videos(
db: SessionDep, review: bool = False, download: bool = False
) -> List[MediaVideoResponse]:
"""
Get all MediaVideos.
"""
results: List[MediaVideoResponse] = []
files: List[MediaVideo]
if review:
files = db.query(MediaVideo).filter(MediaVideo.review.is_(True)).all()
elif download:
files = db.query(MediaVideo).filter(MediaVideo.should_download.is_(True)).all()
else:
files = db.query(MediaVideo).all()
for mediafile in files:
response = video_to_response(mediafile)
results.append(response)
return results
@router.get("/videos/{video_id}", response_model=MediaVideoResponse)
def get_video(video_id: str, db: SessionDep) -> MediaVideoResponse:
"""
Get MediaVideo by id.
"""
video = db.get(MediaVideo, video_id)
if video is None:
raise HTTPException(status_code=404, detail="MediaVideo could not be found")
response = video_to_response(video)
return response
@@ -0,0 +1,37 @@
from fastapi import APIRouter, status, HTTPException
from sqlalchemy import select
from src.core.log_conf import logger
from src.db.repository.media import create_new_mediaactor
from src.db.session import SessionDep
from src.schema.media.actor import Actor, MediaActorResponse, get_actor_details
from src.db.models.media import MediaActor
router = APIRouter()
@router.get("/actors", response_model=list[MediaActorResponse])
# 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_actors(db: SessionDep, review: bool = False, download: bool = False) -> list[MediaActorResponse]:
results: list[MediaActorResponse] = []
actors = db.scalars(select(MediaActor)).all()
for mediaactor in actors:
response = MediaActorResponse(id=mediaactor.id, name=str(mediaactor.name), url=str(mediaactor.url))
results.append(response)
return results
@router.get("/actors/{actor_id}", response_model=MediaActorResponse)
def get_actor(actor_id: str, db: SessionDep) -> MediaActorResponse:
media_actor = db.get(MediaActor, actor_id)
if not media_actor:
raise HTTPException(status_code=404, detail="MediaActor could not be found")
response = get_actor_details(media_actor)
return response
@router.post("/actors", status_code=status.HTTP_201_CREATED)
def add_actor(new_actor: Actor, db: SessionDep) -> MediaActorResponse:
logger.info(f"add actor {new_actor.url}")
try:
mediaActor: MediaActor = create_new_mediaactor(new_actor, db)
except:
raise HTTPException(status_code=409, detail="Link duplicate")
response = get_actor_details(mediaActor)
return response
@@ -1,18 +1,18 @@
from typing import List
from fastapi import APIRouter, status, HTTPException
from sqlalchemy import select
from src.db.models.media import MediaActorFile
from src.db.repository.media.actorfile import delete_mediaactorfile
from src.db.repository.media import delete_mediaactorfile
from src.db.session import SessionDep
from src.schema.media.actorfile import MediaActorFileResponse, actorfile_to_response
from src.schema.media.actorfile import MediaActorFileResponse, get_actorfile_details
router = APIRouter()
@router.get("/actorfiles", response_model=List[MediaActorFileResponse])
def get_all_actorfiles(db: SessionDep) -> List[MediaActorFileResponse]:
results: List[MediaActorFileResponse] = []
actorfiles = db.query(MediaActorFile).all()
for media_actorfile in actorfiles:
response = actorfile_to_response(media_actorfile)
@router.get("/actorfiles", response_model=list[MediaActorFileResponse])
def get_all_actorfiles(db: SessionDep) -> list[MediaActorFileResponse]:
results: list[MediaActorFileResponse] = []
actorfiles = db.scalars(select(MediaActorFile)).all()
for mediaactorfile in actorfiles:
response = MediaActorFileResponse(id=mediaactorfile.id, actor_id=str(mediaactorfile.media_actor_id), file_id=str(mediaactorfile.media_file_id))
results.append(response)
return results
@@ -21,7 +21,7 @@ def get_actorfile(actorfile_id: str, db: SessionDep) -> MediaActorFileResponse:
media_actorfile = db.get(MediaActorFile, actorfile_id)
if not media_actorfile:
raise HTTPException(status_code=404, detail="MediaActor could not be found")
response = actorfile_to_response(media_actorfile)
response = get_actorfile_details(media_actorfile)
return response
@router.delete("/actorfiles/{actorfile_id}", status_code=status.HTTP_204_NO_CONTENT)
@@ -30,3 +30,4 @@ def delete_actorfile(actorfile_id: str, db: SessionDep):
if not media_actorfile:
raise HTTPException(status_code=404, detail="MediaActor could not be found")
delete_mediaactorfile(db, media_actorfile.id)
+109
View File
@@ -0,0 +1,109 @@
from fastapi import APIRouter, status, HTTPException, Depends
from sqlalchemy import select, Sequence
from src.core.log_conf import logger
from src.db.repository.media import create_new_mediaactorfile, create_new_mediafile
from src.db.session import SessionDep
from src.schema.media.actor import MediaActorResponse
from src.schema.media.actorfile import MediaActorFileResponse
from src.schema.media.file import MediaFileResponse, Link, get_file_details, set_file
from src.db.models.media import MediaFile
router = APIRouter()
@router.get("/update-titles")
def update_titles(db: SessionDep) -> list[MediaFileResponse]:
results: list[MediaFileResponse] = []
files = db.query(MediaFile).filter(MediaFile.review == True).all()
for mediafile in files:
mediafile.update_title()
db.add(mediafile)
response = get_file_details(mediafile)
results.append(response)
db.commit()
return results
@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() # type: ignore
elif download:
files = db.query(MediaFile).filter(MediaFile.should_download == True).all() # type: ignore
else:
files = db.scalars(select(MediaFile)).all() # type: ignore
for mediafile in files: # type: ignore
response = get_file_details(mediafile)
results.append(response)
return results
@router.get("/files/{file_id}", response_model=MediaFileResponse)
def get_file(file_id: str, db: SessionDep) -> MediaFileResponse:
mediafile = db.get(MediaFile, file_id)
if not mediafile:
raise HTTPException(status_code=404, detail="MediaFile could not be found")
response = get_file_details(mediafile)
return response
@router.get("/files/{file_id}/actors", response_model=list[MediaActorResponse])
def get_file_actors(file_id: str, db: SessionDep) -> list[MediaActorResponse]:
mediafile = db.get(MediaFile, file_id)
if not mediafile:
raise HTTPException(status_code=404, detail="MediaFile could not be found")
actor_files = mediafile.media_actor_files
logger.info(f"already known actors: {actor_files}")
results: list[MediaActorResponse] = []
for actor_file in actor_files:
response = MediaActorResponse(id=actor_file.media_actor.id, name=actor_file.media_actor.name, url=actor_file.media_actor.url)
results.append(response)
return results
@router.put("/files/{file_id}/actors", response_model=list[MediaActorFileResponse])
def update_file_actors(file_id: str, db: SessionDep, actors: list[MediaActorResponse]) -> list[MediaActorFileResponse]:
mediafile = db.get(MediaFile, file_id)
if not mediafile:
raise HTTPException(status_code=404, detail="MediaFile could not be found")
actor_files = mediafile.media_actor_files
logger.info(f"already known actors: {actor_files}")
for actor in actors:
already_associated = False
for actor_file in actor_files:
if actor.id == actor_file.media_actor_id:
logger.info("alreay associated - do nothing")
already_associated = True
break
if not already_associated:
create_new_mediaactorfile(db, actor.id, mediafile.id)
db.refresh(mediafile)
actor_files = mediafile.media_actor_files
results: list[MediaActorFileResponse] = []
for actor_file in actor_files:
response = MediaActorFileResponse(id=actor_file.id, actor_id=actor_file.media_actor_id, file_id=actor_file.media_file_id)
results.append(response)
return results
@router.put("/files/{file_id}", response_model=MediaFileResponse)
def update_file(file_id: str, 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")
set_file(info, mediaFile)
db.add(mediaFile)
db.commit()
mediafile = db.get(MediaFile, file_id)
if not mediafile:
raise HTTPException(status_code=404, detail="MediaFile could not be updated")
response = get_file_details(mediafile)
return response
@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}")
try:
mediaFile: MediaFile = create_new_mediafile(new_link.url, db)
except:
raise HTTPException(status_code=409, detail="Link duplicate")
response = get_file_details(mediaFile)
return response
+1 -1
View File
@@ -1,7 +1,7 @@
from typing import List
from fastapi import APIRouter
from src.apis.utils import SessionDep
from src.db.session import SessionDep
from src.schema.tysc.sport import SportResponse
from src.db.models.tysc import Sport
-27
View File
@@ -1,27 +0,0 @@
from typing import List
from fastapi import APIRouter, HTTPException
from src.db.models.tysc import Card
from src.db.session import SessionDep
from src.schema.tysc.card import CardResponse, to_response
router = APIRouter()
@router.get("/cards")
def get_all_cards(db: SessionDep) -> List[CardResponse]:
results: List[CardResponse] = []
cards = db.query(Card).all()
for card in cards:
response = to_response(card)
results.append(response)
return results
@router.get("/cards/{card_id}", response_model=CardResponse)
def get_card(card_id: str, db: SessionDep) -> CardResponse:
card = db.get(Card, card_id)
if card is None:
raise HTTPException(status_code=404, detail="Card could not be found")
response = to_response(card)
return response
@@ -1,27 +0,0 @@
from typing import List
from fastapi import APIRouter, HTTPException
from src.db.models.tysc import CardSet
from src.db.session import SessionDep
from src.schema.tysc.cardset import CardSetResponse, to_response
router = APIRouter()
@router.get("/cardsets")
def get_all_cardsets(db: SessionDep) -> List[CardSetResponse]:
results: List[CardSetResponse] = []
cardsets = db.query(CardSet).all()
for cardset in cardsets:
response = to_response(cardset)
results.append(response)
return results
@router.get("/cardsets/{cardset_id}", response_model=CardSetResponse)
def get_cardset(cardset_id: str, db: SessionDep) -> CardSetResponse:
cardset = db.get(CardSet, cardset_id)
if cardset is None:
raise HTTPException(status_code=404, detail="Cardset could not be found")
response = to_response(cardset)
return response
@@ -1,27 +0,0 @@
from typing import List
from fastapi import APIRouter, HTTPException
from src.db.models.tysc import FieldPosition
from src.db.session import SessionDep
from src.schema.tysc.fieldposition import FieldPositionResponse, to_response
router = APIRouter()
@router.get("/positions")
def get_all_positions(db: SessionDep) -> List[FieldPositionResponse]:
results: list[FieldPositionResponse] = []
positions = db.query(FieldPosition).all()
for position in positions:
response = to_response(position)
results.append(response)
return results
@router.get("/positions/{position_id}", response_model=FieldPositionResponse)
def get_position(position_id: str, db: SessionDep) -> FieldPositionResponse:
position = db.get(FieldPosition, position_id)
if position is None:
raise HTTPException(status_code=404, detail="Fieldposition could not be found")
response = to_response(position)
return response
@@ -1,27 +0,0 @@
from typing import List
from fastapi import APIRouter, HTTPException
from src.db.models.tysc import Player
from src.db.session import SessionDep
from src.schema.tysc.player import PlayerResponse, to_response
router = APIRouter()
@router.get("/players")
def get_all_players(db: SessionDep) -> List[PlayerResponse]:
results: List[PlayerResponse] = []
players = db.query(Player).all()
for player in players:
response = to_response(player)
results.append(response)
return results
@router.get("/players/{player_id}", response_model=PlayerResponse)
def get_player(player_id: str, db: SessionDep) -> PlayerResponse:
player = db.get(Player, player_id)
if player is None:
raise HTTPException(status_code=404, detail="Player could not be found")
response = to_response(player)
return response
@@ -1,27 +0,0 @@
from typing import List
from fastapi import APIRouter, HTTPException
from src.db.models.tysc import Rooster
from src.db.session import SessionDep
from src.schema.tysc.rooster import RoosterResponse, to_response
router = APIRouter()
@router.get("/roosters")
def get_all_roosters(db: SessionDep) -> List[RoosterResponse]:
results: list[RoosterResponse] = []
roosters = db.query(Rooster).all()
for rooster in roosters:
response = to_response(rooster)
results.append(response)
return results
@router.get("/roosters/{rooster_id}", response_model=RoosterResponse)
def get_rooster(rooster_id: str, db: SessionDep) -> RoosterResponse:
rooster = db.get(Rooster, rooster_id)
if rooster is None:
raise HTTPException(status_code=404, detail="Rooster could not be found")
response = to_response(rooster)
return response
@@ -1,28 +0,0 @@
from typing import List
from fastapi import APIRouter, HTTPException
from src.core.log_conf import logger
from src.db.session import SessionDep
from src.schema.tysc.sport import SportResponse, to_response
from src.db.models.tysc import Sport
router = APIRouter()
@router.get("/sports")
def get_all_sports(db: SessionDep) -> List[SportResponse]:
results: list[SportResponse] = []
sports = db.query(Sport).all()
for sport in sports:
response = to_response(sport)
results.append(response)
return results
@router.get("/sports/{sport_id}", response_model=SportResponse)
def get_sport(sport_id: str, db: SessionDep) -> SportResponse:
sport = db.get(Sport, sport_id)
if sport is None:
raise HTTPException(status_code=404, detail="Sport could not be found")
logger.debug(f"create SportResponse for {sport}")
response: SportResponse = to_response(sport)
return response
-27
View File
@@ -1,27 +0,0 @@
from typing import List
from fastapi import APIRouter, HTTPException
from src.db.models.tysc import Team
from src.db.session import SessionDep
from src.schema.tysc.team import TeamResponse, to_response
router = APIRouter()
@router.get("/teams")
def get_all_teams(db: SessionDep) -> List[TeamResponse]:
results: list[TeamResponse] = []
teams = db.query(Team).all()
for team in teams:
response = to_response(team)
results.append(response)
return results
@router.get("/teams/{team_id}", response_model=TeamResponse)
def get_team(team_id: str, db: SessionDep) -> TeamResponse:
team = db.get(Team, team_id)
if team is None:
raise HTTPException(status_code=404, detail="Team could not be found")
response = to_response(team)
return response
@@ -1,33 +0,0 @@
from typing import List
from fastapi import APIRouter, HTTPException
from src.db.models.tysc import Vendor
from src.db.session import SessionDep
from src.schema.tysc.vendor import VendorResponse, to_response
router = APIRouter()
@router.get("/vendors")
def get_all_vendors(db: SessionDep) -> List[VendorResponse]:
"""
retrieve all vendors as json response.
"""
results: list[VendorResponse] = []
vendors = db.query(Vendor).all()
for vendor in vendors:
response = to_response(vendor)
results.append(response)
return results
@router.get("/vendors/{vendor_id}", response_model=VendorResponse)
def get_vendor(vendor_id: str, db: SessionDep) -> VendorResponse:
"""
retrieve vendor by id as json response.
"""
vendor = db.get(Vendor, vendor_id)
if vendor is None:
raise HTTPException(status_code=404, detail="Vendor could not be found")
response = to_response(vendor)
return response
@@ -1,26 +0,0 @@
from typing import List
from fastapi import APIRouter, HTTPException
from src.db.models.admin import Assignment
from src.db.session import SessionDep
from src.schema.user.assignment import AssignmentResponse, to_response
router = APIRouter()
@router.get("/assignments", response_model=List[AssignmentResponse])
def get_all_assignments(db: SessionDep) -> List[AssignmentResponse]:
results: List[AssignmentResponse] = []
assignments = db.query(Assignment).all()
for assignment in assignments:
response = to_response(assignment)
results.append(response)
return results
@router.get("/assignments/{assignment_id}", response_model=AssignmentResponse)
def get_assignment(assignment_id: str, db: SessionDep) -> AssignmentResponse:
assignment = db.get(Assignment, assignment_id)
if assignment is None:
raise HTTPException(status_code=404, detail="Assignment could not be found")
response = to_response(assignment)
return response
@@ -1,26 +0,0 @@
from typing import List
from fastapi import APIRouter, HTTPException
from src.db.models.admin import Permission
from src.db.session import SessionDep
from src.schema.user.permission import PermissionResponse, to_response
router = APIRouter()
@router.get("/permissions", response_model=List[PermissionResponse])
def get_all_permissions(db: SessionDep) -> List[PermissionResponse]:
results: List[PermissionResponse] = []
permissions = db.query(Permission).all()
for permission in permissions:
response = to_response(permission)
results.append(response)
return results
@router.get("/permissions/{permission_id}", response_model=PermissionResponse)
def get_permission(permission_id: str, db: SessionDep) -> PermissionResponse:
permission = db.get(Permission, permission_id)
if permission is None:
raise HTTPException(status_code=404, detail="Permission could not be found")
response = to_response(permission)
return response
@@ -1,53 +0,0 @@
from typing import List
from fastapi import APIRouter, HTTPException, status
from sqlalchemy import select
from src.core.log_conf import logger
from src.core.security import CurrentUser
from src.db.models.admin import Profile
from src.db.repository.user import create_new_profile
from src.db.session import SessionDep
from src.schema.user.profile import ProfileResponse, ProfileModel, to_response
router = APIRouter()
@router.get("/profile", response_model=ProfileModel)
async def read_profile(current_user: CurrentUser):
return current_user
@router.get("/profiles", response_model=List[ProfileResponse])
def get_all_profiles(db: SessionDep) -> List[ProfileResponse]:
results: List[ProfileResponse] = []
profiles = db.scalars(select(Profile)).all()
for profile in profiles:
response = to_response(profile)
results.append(response)
return results
@router.get("/profiles/{profile_id}", response_model=ProfileResponse)
def get_profile(profile_id: str, db: SessionDep) -> ProfileResponse:
profile = db.get(Profile, profile_id)
if not profile:
raise HTTPException(status_code=404, detail="Profile could not be found")
response = to_response(profile)
return response
@router.delete("/profiles/{profile_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_profile(profile_id: str, db: SessionDep): # type: ignore
profile = db.get(Profile, profile_id)
if not profile:
raise HTTPException(status_code=404, detail="Profile could not be found")
logger.info(f"delete Profile: {profile_id}")
delete_profile(profile_id=profile_id, db=db)
@router.post("/profiles", status_code=status.HTTP_201_CREATED)
def add_profile(new_profile: ProfileModel, db: SessionDep) -> ProfileResponse:
logger.info(f"add profile {new_profile.username}")
try:
profile: Profile = create_new_profile(new_profile, db)
except:
raise HTTPException(status_code=409, detail="Profile duplicate")
response = to_response(profile)
return response
@@ -1,19 +0,0 @@
from typing import List
from fastapi import APIRouter
from src.db.models.admin import Token
from src.db.session import SessionDep
from src.schema.user.token import TokenResponse, to_response
router = APIRouter()
@router.get("/tokens", response_model=List[TokenResponse])
def get_all_profiles(db: SessionDep) -> List[TokenResponse]:
results: List[TokenResponse] = []
tokens = db.query(Token).all()
for token in tokens:
response = to_response(token)
results.append(response)
return results
+2 -2
View File
@@ -9,7 +9,7 @@ load_dotenv(dotenv_path=env_path)
class Settings:
PROJECT_NAME: str = "Kontor"
PROJECT_VERSION: str = "0.3.0"
PROJECT_VERSION: str = "0.2.0"
DB_USER: str = os.getenv("DB_USER", "kontor")
DB_PASSWORD: str = os.getenv("DB_PASSWORD", "kontor")
@@ -19,7 +19,7 @@ class Settings:
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
ACCESS_TOKEN_EXPIRE_MINUTES = 60*24*7 # one week in mins
settings = Settings()
+3 -2
View File
@@ -34,11 +34,12 @@ LOGGING_CONFIG: dict[str, Any] = {
},
"loggers": {
"root": {"handlers": ["default"], "level": "INFO", "propagate": False},
"kontor": {"handlers": ["default"], "level": "INFO", "propagate": True},
"uvicorn": {"handlers": ["default"], "level": "INFO", "propagate": False},
"uvicorn.error": {"level": "INFO"},
"uvicorn.access": {"handlers": ["default"], "level": "INFO", "propagate": False},
"uvicorn.access": {"handlers": ["default"], "level": "WARNING", "propagate": False},
},
}
logging.config.dictConfig(LOGGING_CONFIG)
logger = logging.getLogger(__name__)
logger = logging.getLogger('kontor')
-35
View File
@@ -1,35 +0,0 @@
import json
import time
from fastapi import Request, Response
from starlette.middleware.base import BaseHTTPMiddleware
from src.core.log_conf import logger
class RequestLoggingMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next) -> Response:
start_time = time.time()
path = request.url.path
if path != "/health":
# Log request info
request_info ={
"method": request.method,
"path": path,
"client_ip": request.client.host if request.client else "unknown"
}
logger.info("Incoming: %s", json.dumps(request_info))
# Process request
response = await call_next(request)
if path != "/health":
# Log response info
duration_ms = (time.time()- start_time)*1000
response_info = {
"status_code": response.status_code,
"duration_ms": round(duration_ms, 2)
}
logger.info("completed: %s", json.dumps(response_info))
return response
+140 -4
View File
@@ -1,17 +1,84 @@
from datetime import datetime
import logging
from datetime import datetime, timezone
from datetime import timedelta
from typing import Optional, Annotated, List
from typing import Dict
from typing import Optional
from fastapi import HTTPException, Security
from fastapi import Request
from fastapi import status
from fastapi.openapi.models import OAuthFlows as OAuthFlowsModel
from fastapi.security.utils import get_authorization_scheme_param
import bcrypt
from fastapi import Depends
from fastapi.security import SecurityScopes, OAuth2PasswordBearer, OAuth2
from pydantic import ValidationError
from src.core.config import settings
from jose import jwt
from jose import jwt, JWTError
from src.core.log_conf import logger
from src.db.models.admin import Profile
from src.db.repository.admin import get_profile
from src.db.session import SessionLocal
from src.schema.admin import TokenData, ProfileModel
oauth2_scheme = OAuth2PasswordBearer(
tokenUrl="/api/login/token",
scopes={"me": "read", "admin": "read"},
)
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
def authenticate_user(username: str, password: str) -> Optional[Profile]:
with SessionLocal() as db:
user = get_profile(username=username, db=db)
logger.info(user)
if not user:
return None
if bcrypt.checkpw(password.encode(), user.password.encode()):
print("User successful authenticated")
else:
logger.info("Authentication failed!")
return user
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
to_encode = data.copy()
if expires_delta:
expire = datetime.utcnow() + expires_delta
expire = datetime.now(timezone.utc) + expires_delta
else:
expire = datetime.utcnow() + timedelta(
expire = datetime.now(timezone.utc) + timedelta(
minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES
)
to_encode.update({"exp": expire})
@@ -19,3 +86,72 @@ def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM
)
return encoded_jwt
async def get_current_user(security_scopes: SecurityScopes, token: Annotated[str, Depends(oauth2_scheme)]):
if security_scopes.scopes:
authenticate_value = f'Bearer scope="{security_scopes.scope_str}"'
else:
authenticate_value = "Bearer"
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": authenticate_value},
)
try:
payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM])
username: str = payload.get("sub")
logger.info("username/email extracted is ", username)
if username is None:
raise credentials_exception
scope: str = payload.get("scope", "")
token_scopes: List[str] = scope.split(" ")
token_data = TokenData(scopes=token_scopes, username=username)
except (JWTError, ValidationError):
raise credentials_exception
with SessionLocal() as db:
user = get_profile(username=token_data.username, db=db)
if user is None:
raise credentials_exception
for scope in security_scopes.scopes:
if scope not in token_scopes:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="not enough permissions",
headers={"WWW-Authenticate": authenticate_value},
)
return user
async def get_current_active_user(
current_user: Annotated[Profile, Security(get_current_user, scopes=["me"])],
) -> ProfileModel:
if not current_user.enabled:
raise HTTPException(status_code=400, detail="Inactive user")
user_model = ProfileModel(username=current_user.user_name, email=current_user.email,
first_name=current_user.first_name, last_name=current_user.last_name,
active=current_user.enabled)
return user_model
def get_current_user_from_token(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")
logger.info("username/email extracted is ", username)
if username is None:
raise credentials_exception
except JWTError:
raise credentials_exception
with SessionLocal() as db:
user = get_profile(username=username, db=db)
if user is None:
raise credentials_exception
return user
+35 -36
View File
@@ -1,7 +1,6 @@
from datetime import datetime
from typing import List
from sqlalchemy import ForeignKey
from sqlalchemy import Column, ForeignKey, Integer, String, Boolean
from sqlalchemy.orm import relationship, mapped_column, Mapped
from src.db.models.base import Base, BaseMixin
@@ -9,23 +8,23 @@ from src.db.models.base import Base, BaseMixin
class Profile(Base, BaseMixin):
__tablename__ = 'profile'
first_name: Mapped[str]
last_name: Mapped[str]
user_name: Mapped[str] = mapped_column(nullable=False)
email: Mapped[str]
password: Mapped[str]
enabled: Mapped[bool]
assignments: Mapped[List["Assignment"]] = relationship(back_populates="profile")
tokens: Mapped[List["Token"]] = relationship(back_populates="profile")
first_name = Column(String)
last_name = Column(String)
user_name = Column(String, nullable=False)
email = Column(String)
password = Column(String)
enabled = Column(Boolean)
assignments = relationship("Assignment")
tokens = relationship("Token")
def get_full_name(self) -> str:
full_name: str = ""
full_name = ""
if self.first_name is not None:
full_name += str(self.first_name)
full_name += self.first_name
if self.last_name is not None:
if len(full_name) > 0:
full_name += " "
full_name += str(self.last_name)
full_name += self.last_name
return full_name
def __str__(self):
@@ -34,42 +33,42 @@ class Profile(Base, BaseMixin):
class Token(Base, BaseMixin):
__tablename__ = "token"
token: Mapped[str] = mapped_column(nullable=False, unique=True)
name: Mapped[str]
last_used_date: Mapped[datetime]
enabled: Mapped[bool]
profile_id = mapped_column(ForeignKey("profile.id"), nullable=False)
profile: Mapped[Profile] = relationship(back_populates="tokens")
token = Column(String, nullable=False, unique=True)
name = Column(String)
last_used_date: Mapped[datetime] = mapped_column()
enabled = Column(Boolean)
profile_id = Column(String, ForeignKey("profile.id"), nullable=False)
profile = relationship("Profile", back_populates="tokens")
class Permission(Base, BaseMixin):
__tablename__ = "permission"
name: Mapped[str] = mapped_column(nullable=False)
assignments: Mapped[List["Assignment"]] = relationship(back_populates="permission")
name = Column(String, nullable=False)
assignments = relationship("Assignment")
class Assignment(Base, BaseMixin):
__tablename__ = "assignment"
profile_id = mapped_column(ForeignKey("profile.id"), nullable=False)
profile: Mapped[Profile] = relationship(back_populates="assignments")
permission_id = mapped_column(ForeignKey("permission.id"), nullable=False)
permission: Mapped[Permission] = relationship(back_populates="assignments")
profile_id = Column(String, ForeignKey("profile.id"), nullable=False)
profile = relationship("Profile", back_populates="assignments")
permission_id = Column(String, ForeignKey("permission.id"), nullable=False)
permission = relationship("Permission", back_populates="assignments")
class MailAccount(Base, BaseMixin):
__tablename__ = "mail_account"
host: Mapped[str]
port: Mapped[int]
protocol: Mapped[str]
user_name: Mapped[str]
password: Mapped[str]
start_tls: Mapped[bool]
host = Column(String)
port = Column(Integer)
protocol = Column(String)
user_name = Column(String)
password = Column(String)
start_tls = Column(Boolean)
class Mail(Base, BaseMixin):
__tablename__ = "mail"
folder: Mapped[str]
subject: Mapped[str]
body: Mapped[str]
sent_date: Mapped[datetime]
received_date: Mapped[datetime]
folder: Mapped[str] = mapped_column()
subject: Mapped[str] = mapped_column()
body: Mapped[str] = mapped_column()
sent_date: Mapped[datetime] = mapped_column()
received_date: Mapped[datetime] = mapped_column()
+8 -5
View File
@@ -1,8 +1,7 @@
from typing import Optional
import uuid
from datetime import datetime
from sqlalchemy import func
from sqlalchemy import func, Column, String, Boolean
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
@@ -11,17 +10,21 @@ 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()))
# created_date = Column(DateTime)
created_date: Mapped[datetime] = mapped_column(default=func.now())
# last_modified_date = Column(DateTime)
last_modified_date: Mapped[datetime] = mapped_column(default=func.now())
# version = Column(Integer)
version: Mapped[int] = mapped_column(default=0)
class BaseVideoMixin:
cloud_link = Column(String)
file_name = Column(String)
cloud_link = Column(String, nullable=True)
file_name = Column(String, nullable=True)
path = Column(String)
review = Column(Boolean)
title = Column(String)
url = Column(String, unique=True)
url = Column(String, nullable=True)
should_download = Column(Boolean)
+22 -24
View File
@@ -1,52 +1,50 @@
from typing import List
from sqlalchemy import Column, ForeignKey, Integer, String
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy.orm import relationship
from src.db.models.base import Base, BaseMixin
class Article(Base, BaseMixin):
__tablename__ = 'article'
title: Mapped[str] = mapped_column(unique=True)
title = Column(String, unique=True)
article_authors = relationship("ArticleAuthor")
class Author(Base, BaseMixin):
__tablename__ = 'author'
first_name: Mapped[str]
last_name: Mapped[str]
article_authors: Mapped[List["ArticleAuthor"]] = relationship(back_populates="author")
book_authors: Mapped[List["BookAuthor"]] = relationship(back_populates="author")
first_name = Column(String)
last_name = Column(String)
article_authors = relationship("ArticleAuthor")
book_authors = relationship("BookAuthor")
class BookshelfPublisher(Base, BaseMixin):
__tablename__ = 'bookshelf_publisher'
name: Mapped[str] = mapped_column(unique=True)
books: Mapped[List["Book"]] = relationship(back_populates="publisher")
name = Column(String, unique=True)
books = relationship("Book")
class Book(Base, BaseMixin):
__tablename__ = 'book'
isbn: Mapped[str] = mapped_column(unique=True)
title: Mapped[str]
year: Mapped[int] = mapped_column(nullable=False)
publisher_id: Mapped[str] = mapped_column(ForeignKey("bookshelf_publisher.id"), nullable=False)
publisher: Mapped[BookshelfPublisher] = relationship(back_populates="books")
book_authors: Mapped[List["BookAuthor"]] = relationship(back_populates="book")
isbn = Column(String, unique=True)
title = Column(String)
year = Column(Integer, nullable=False)
publisher_id = Column(String, ForeignKey('bookshelf_publisher.id'), nullable=False)
publisher = relationship('BookshelfPublisher', back_populates="books")
book_authors = relationship("BookAuthor")
class ArticleAuthor(Base, BaseMixin):
__tablename__ = 'article_author'
article_id: Mapped[str] = mapped_column(ForeignKey("article.id"), nullable=False)
article: Mapped[Article] = relationship(back_populates="article_authors")
author_id: Mapped[str] = mapped_column(ForeignKey("author.id"), nullable=False)
author: Mapped[Author] = relationship(back_populates="article_authors")
article_id = Column(String, ForeignKey('article.id'), nullable=False)
article = relationship('Article', back_populates="article_authors")
author_id = Column(String, ForeignKey('author.id'), nullable=False)
author = relationship('Author', back_populates="article_authors")
class BookAuthor(Base, BaseMixin):
__tablename__ = 'book_author'
author_id: Mapped[str] = mapped_column(ForeignKey("author.id"), nullable=False)
author: Mapped[Author] = relationship(back_populates="book_authors")
book_id: Mapped[str] = mapped_column(ForeignKey("book.id"), nullable=False)
book: Mapped[Book] = relationship(back_populates="book_authors")
author_id = Column(String, ForeignKey('author.id'), nullable=False)
author = relationship('Author', back_populates="book_authors")
book_id = Column(String, ForeignKey('book.id'), nullable=False)
book = relationship('Book', back_populates="book_authors")
+61 -91
View File
@@ -2,7 +2,7 @@ import uuid
from datetime import datetime
from typing import AnyStr, Dict, List, Optional, Any
from natsort import natsorted
from sqlalchemy import ForeignKey, func
from sqlalchemy import Column, ForeignKey, Integer, String, Boolean, func
from sqlalchemy.orm import relationship, Mapped, mapped_column
from src.db.models.base import Base, BaseMixin
@@ -14,12 +14,12 @@ class Publisher(Base):
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: Mapped[str] = mapped_column(unique=True)
weblink: Mapped[Optional[str]]
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(back_populates="imprints", remote_side=[id])
imprints: Mapped[List['Publisher']] = relationship(back_populates="parent_publisher")
comics: Mapped[List["Comic"]] = relationship(back_populates="publisher")
parent_publisher: Mapped[Optional['Publisher']] = relationship("Publisher", back_populates="imprints", remote_side=[id])
imprints: Mapped[List['Publisher']] = relationship('Publisher', back_populates="parent_publisher")
comics = relationship("Comic")
def __repr__(self):
return f'Publisher({self.id} {self.name})'
@@ -30,17 +30,17 @@ class Publisher(Base):
class Comic(Base, BaseMixin):
__tablename__ = 'comic'
title: Mapped[str] = mapped_column(unique=True)
publisher_id: Mapped[str] = mapped_column(ForeignKey('publisher.id'), nullable=False)
publisher: Mapped[Publisher] = relationship(back_populates="comics")
current_order: Mapped[bool]
completed: Mapped[bool]
weblink: Mapped[Optional[str]]
issues: Mapped[List["Issue"]] = relationship(back_populates="comic", order_by="Issue.issue_number")
story_arcs: Mapped[List["StoryArc"]] = relationship(back_populates="comic")
trade_paperbacks: Mapped[List["TradePaperback"]] = relationship(back_populates="comic")
volumes: Mapped[List["Volume"]] = relationship(back_populates="comic")
comic_works: Mapped[List["ComicWork"]] = relationship(back_populates="comic")
title = Column(String, 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)
issues = relationship("Issue", order_by="Issue.issue_number")
story_arcs = relationship("StoryArc")
trade_paperbacks = relationship("TradePaperback")
volumes = relationship("Volume")
comic_works = relationship("ComicWork")
def __repr__(self):
return f'Comic({self.id} {self.version} {self.title} {self.publisher.name})'
@@ -66,69 +66,51 @@ class Comic(Base, BaseMixin):
class Volume(Base, BaseMixin):
__tablename__ = "volume"
name: Mapped[str] = mapped_column(nullable=False)
comic_id: Mapped[str] = mapped_column(ForeignKey("comic.id"), nullable=False)
comic: Mapped[Comic] = relationship(back_populates="volumes")
story_arcs: Mapped[List["StoryArc"]] = relationship(back_populates="volume")
issues: Mapped[List["Issue"]] = relationship(back_populates="volume")
name = Column(String, nullable=False)
comic_id = Column(String, ForeignKey("comic.id"), nullable=False)
comic = relationship("Comic", back_populates="volumes")
story_arcs = relationship("StoryArc")
issues = relationship("Issue")
class TradePaperback(Base, BaseMixin):
__tablename__ = "trade_paperback"
name: Mapped[str] = mapped_column(nullable=False)
issue_start: Mapped[int]
issue_end: Mapped[int]
comic_id: Mapped[str] = mapped_column(ForeignKey("comic.id"), nullable=False)
comic: Mapped[Comic] = relationship(back_populates="trade_paperbacks")
name = Column(String, nullable=False)
issue_start = Column(Integer)
issue_end = Column(Integer)
comic_id = Column(String, ForeignKey("comic.id"), nullable=False)
comic = relationship("Comic", back_populates="trade_paperbacks")
class StoryArc(Base, BaseMixin):
__tablename__ = "story_arc"
name: Mapped[str] = mapped_column(nullable=False)
comic_id: Mapped[str] = mapped_column(ForeignKey("comic.id"), nullable=False)
comic: Mapped[Comic] = relationship(back_populates="story_arcs")
volume_id: Mapped[str] = mapped_column(ForeignKey("volume.id"), nullable=True)
volume: Mapped[Volume] = relationship(back_populates="story_arcs")
issues: Mapped[List["Issue"]] = relationship(back_populates="story_arc")
name = Column(String, nullable=False)
comic_id = Column(String, ForeignKey("comic.id"), nullable=False)
comic = relationship("Comic", back_populates="story_arcs")
volume_id = Column(String, ForeignKey("volume.id"), nullable=True)
volume = relationship("Volume", back_populates="story_arcs")
issues = relationship("Issue")
class Issue(Base, BaseMixin):
__tablename__ = "issue"
issue_number: Mapped[str]
title: Mapped[Optional[str]]
published_on: Mapped[Optional[datetime]]
in_stock: Mapped[bool]
is_read: Mapped[bool]
comic_id: Mapped[str] = mapped_column(ForeignKey("comic.id"), nullable=False)
comic: Mapped[Comic] = relationship(back_populates="issues")
volume_id: Mapped[str] = mapped_column(ForeignKey("volume.id"), nullable=True)
volume: Mapped[Volume] = relationship(back_populates="issues")
story_arc_id: Mapped[str] = mapped_column(ForeignKey("story_arc.id"), nullable=True)
story_arc: Mapped[StoryArc] = relationship(back_populates="issues")
issue_works: Mapped[List["IssueWork"]] = relationship(back_populates="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)
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
def get_full_title(self) -> str:
full_title: str = str(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
def get_full_title(self) -> AnyStr:
full_title: AnyStr = self.issue_number
if self.title:
full_title += ": " + self.title
full_title += str(": " + self.title)
return full_title
def get_artists(self) -> Dict[Any, List[Any]]:
@@ -145,10 +127,10 @@ class Issue(Base, BaseMixin):
class Artist(Base, BaseMixin):
__tablename__ = "artist"
name: Mapped[str] = mapped_column(nullable=False)
weblink: Mapped[str] = mapped_column(nullable=True)
comic_works: Mapped[List["ComicWork"]] = relationship(back_populates="artist")
issue_works: Mapped[List["IssueWork"]] = relationship(back_populates="artist")
name = Column(String, nullable=False)
weblink = Column(String, nullable=True)
comic_works = relationship("ComicWork")
issue_works = relationship("IssueWork")
def get_comics(self) -> Dict[Any, List[Comic]]:
works: Dict[Any, List[Comic]] = {}
@@ -164,20 +146,9 @@ class Artist(Base, BaseMixin):
class WorkType(Base, BaseMixin):
__tablename__ = "worktype"
name: Mapped[str] = mapped_column(nullable=False, unique=True)
comic_works: Mapped[List["ComicWork"]] = relationship(back_populates="work_type")
issue_works: Mapped[List["IssueWork"]] = relationship(back_populates="work_type")
def get_artists(self) -> Dict[str, List[Artist]]:
works: Dict[str, List[Artist]] = {}
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
name = Column(String, nullable=False, unique=True)
comic_works = relationship("ComicWork")
issue_works = relationship("IssueWork")
def get_artists(self) -> Dict[str, List[str]]:
works: Dict[str, List[str]] = {}
@@ -199,12 +170,12 @@ class WorkType(Base, BaseMixin):
class ComicWork(Base, BaseMixin):
__tablename__ = "comic_work"
comic_id: Mapped[str] = mapped_column(ForeignKey("comic.id"), nullable=False)
comic: Mapped[Comic] = relationship(back_populates="comic_works")
artist_id: Mapped[str] = mapped_column(ForeignKey("artist.id"), nullable=False)
artist: Mapped[Artist] = relationship(back_populates="comic_works")
work_type_id: Mapped[str] = mapped_column(ForeignKey("worktype.id"), nullable=False)
work_type: Mapped[WorkType] = relationship(back_populates="comic_works")
comic_id = Column(String, ForeignKey("comic.id"), nullable=False)
comic = relationship("Comic", back_populates="comic_works")
artist_id = Column(String, ForeignKey("artist.id"), nullable=False)
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):
@@ -215,4 +186,3 @@ class IssueWork(Base, BaseMixin):
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")
+76 -53
View File
@@ -3,96 +3,119 @@ import re
import subprocess
from datetime import datetime
from pathlib import Path
from typing import List, Optional
import requests
from bs4 import BeautifulSoup
from sqlalchemy import ForeignKey
from sqlalchemy.orm import Mapped, relationship, mapped_column
from sqlalchemy import Column, String, ForeignKey, Boolean
from sqlalchemy.orm import relationship
from src.db.models.base import Base, BaseMixin, BaseVideoMixin
class MediaFile(Base, BaseMixin, BaseVideoMixin):
"""
MediaFile represents video link.
"""
__tablename__ = "media_file"
media_actor_files: Mapped[List["MediaActorFile"]] = relationship(
back_populates="media_file"
)
__tablename__ = 'media_file'
media_actor_files = relationship("MediaActorFile")
def __repr__(self):
return f"MediaFile({self.id} {self.title} {self.title})"
return f'MediaFile({self.id} {self.title} {self.title})'
def __str__(self):
return f"{self.title}({self.id})"
return f'{self.title}({self.id})'
def update_title(self):
"""
Update title from url.
"""
def update_title(self) -> None:
logging.info(f"update title for {self.url}")
try:
r = requests.get(str(self.url))
soup = BeautifulSoup(r.content, "html.parser")
title = soup.title.get_text() # type: ignore
self.title = title
self.review = False
except:
self.title = None
self.review = True
self.last_modified_date = datetime.now()
def download_file(self, download_dir: str, dl_tool: str):
logging.info(f"download file for {self.url} to {download_dir}")
result = subprocess.run([dl_tool, self.url], cwd=download_dir, capture_output=True, text=True) # type: ignore
if result.returncode == 0:
output = result.stdout
output = re.sub(' +', ' ', output)
lines_list = output.splitlines()
file_name = self.__parse_output__(lines_list)
if file_name is None:
self.review = True
self.should_download = True
self.file_name = None
else:
download_file = Path(file_name)
self.should_download = False
self.file_name = download_file.name
self.cloud_link = str(download_file.absolute())
self.last_modified_date = datetime.now()
def __parse_output__(self, lines_list):
self.file_name = None
for line in lines_list:
if 'has already been downloaded' in line:
end_len = len(' has already been downloaded')
self.file_name = line[11:-end_len]
if 'Destination' in line:
line_len = len(line)
start_len = len('[download] Destination: ')
file_len = line_len - start_len
self.file_name = line[-file_len:]
return self.file_name
class MediaActor(Base, BaseMixin):
__tablename__ = 'media_actor'
name = Column(String)
url = Column(String, unique=True, nullable=True)
media_actor_files = relationship("MediaActorFile")
def __repr__(self) -> str:
return f"MediaActor({self.id} {self.name} {self.url})"
return f'MediaActor({self.id} {self.name} {self.url})'
def __str__(self) -> str:
return f"{self.url}({self.id})"
return f'{self.url}({self.id})'
class MediaActorFile(Base, BaseMixin):
__tablename__ = "media_actor_file"
media_actor_id: Mapped[str] = mapped_column(
ForeignKey("media_actor.id"), nullable=False
)
media_actor: Mapped[MediaActor] = relationship(back_populates="media_actor_files")
media_file_id: Mapped[str] = mapped_column(
ForeignKey("media_file.id"), nullable=True
)
media_file: Mapped[MediaFile] = relationship(back_populates="media_actor_files")
__tablename__ = 'media_actor_file'
media_actor_id = Column(String, ForeignKey("media_actor.id"), nullable=False)
media_actor = relationship("MediaActor", back_populates="media_actor_files")
media_file_id = Column(String, ForeignKey("media_file.id"), nullable=True)
media_file = relationship("MediaFile", back_populates="media_actor_files")
def __repr__(self):
return f"MediaActorFile({self.id} {self.media_actor_id} {self.media_file_id})"
return f'MediaActorFile({self.id} {self.media_actor_id} {self.media_file_id})'
def __str__(self) -> str:
return f"{self.id} {self.media_actor_id} {self.media_file_id}"
return f'{self.id} {self.media_actor_id} {self.media_file_id}'
class MediaArticle(Base, BaseMixin):
__tablename__ = "media_article"
review: Mapped[bool]
title: Mapped[str]
url: Mapped[str] = mapped_column(unique=True)
__tablename__ = 'media_article'
review = Column(Boolean)
title = Column(String)
url = Column(String, unique=True)
class MediaVideo(Base, BaseMixin):
"""
MediaFile represents video link.
"""
__tablename__ = "media_video"
cloud_link: Mapped[str]
file_name: Mapped[str]
path: Mapped[str]
review: Mapped[bool]
title: Mapped[str]
url: Mapped[str] = mapped_column(unique=True)
should_download: Mapped[bool]
__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})"
return f'MediaFile({self.id} {self.title} {self.url})'
def __str__(self):
if self.title is None:
return f"{self.url}({self.id})"
return f'{self.url}({self.id})'
else:
return f'{self.title}({self.id})'

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