Compare commits

...

63 Commits

Author SHA1 Message Date
cc76227199 Moved hosted service base
All checks were successful
Test before pr merge / test-lint (pull_request) Successful in 10s
Build on push / prepare (push) Successful in 13s
Build on push / query (push) Successful in 24s
Build on push / core (push) Successful in 25s
Build on push / dependency (push) Successful in 18s
Build on push / cli (push) Successful in 24s
Build on push / application (push) Successful in 18s
Build on push / mail (push) Successful in 19s
Build on push / database (push) Successful in 23s
Build on push / translation (push) Successful in 26s
Build on push / auth (push) Successful in 17s
Build on push / api (push) Successful in 22s
2025-10-22 11:40:25 +02:00
dfbb0a8c1f Updated cpl structure model json handling
All checks were successful
Test before pr merge / test-lint (pull_request) Successful in 7s
Build on push / prepare (push) Successful in 12s
Build on push / query (push) Successful in 19s
Build on push / core (push) Successful in 19s
Build on push / cli (push) Successful in 19s
Build on push / dependency (push) Successful in 19s
Build on push / database (push) Successful in 16s
Build on push / application (push) Successful in 17s
Build on push / translation (push) Successful in 19s
Build on push / mail (push) Successful in 22s
Build on push / auth (push) Successful in 15s
Build on push / api (push) Successful in 15s
2025-10-19 20:17:17 +02:00
472aba5990 Removed ws with_parents
All checks were successful
Test before pr merge / test-lint (pull_request) Successful in 7s
Build on push / prepare (push) Successful in 10s
Build on push / query (push) Successful in 19s
Build on push / core (push) Successful in 22s
Build on push / cli (push) Successful in 16s
Build on push / dependency (push) Successful in 19s
Build on push / mail (push) Successful in 17s
Build on push / application (push) Successful in 21s
Build on push / translation (push) Successful in 20s
Build on push / database (push) Successful in 21s
Build on push / auth (push) Successful in 19s
Build on push / api (push) Successful in 15s
2025-10-19 20:03:43 +02:00
9c75008c9f Add cpl-mail project
All checks were successful
Test before pr merge / test-lint (pull_request) Successful in 8s
Build on push / prepare (push) Successful in 10s
Build on push / query (push) Successful in 18s
Build on push / core (push) Successful in 19s
Build on push / dependency (push) Successful in 19s
Build on push / cli (push) Successful in 24s
Build on push / mail (push) Successful in 17s
Build on push / translation (push) Successful in 16s
Build on push / database (push) Successful in 21s
Build on push / application (push) Successful in 21s
Build on push / auth (push) Successful in 15s
Build on push / api (push) Successful in 15s
2025-10-19 20:03:43 +02:00
df2c2b5b56 Workspace check project existence
All checks were successful
Test before pr merge / test-lint (pull_request) Successful in 6s
Build on push / prepare (push) Successful in 11s
Build on push / query (push) Successful in 19s
Build on push / core (push) Successful in 19s
Build on push / dependency (push) Successful in 19s
Build on push / cli (push) Successful in 19s
Build on push / application (push) Successful in 16s
Build on push / translation (push) Successful in 17s
Build on push / database (push) Successful in 20s
Build on push / mail (push) Successful in 20s
Build on push / auth (push) Successful in 15s
Build on push / api (push) Successful in 16s
2025-10-19 19:56:59 +02:00
e3b19c9984 Fixed init command regarding structure
All checks were successful
Test before pr merge / test-lint (pull_request) Successful in 7s
Build on push / prepare (push) Successful in 10s
Build on push / core (push) Successful in 19s
Build on push / query (push) Successful in 19s
Build on push / cli (push) Successful in 16s
Build on push / dependency (push) Successful in 19s
Build on push / application (push) Successful in 20s
Build on push / database (push) Successful in 21s
Build on push / translation (push) Successful in 20s
Build on push / mail (push) Successful in 23s
Build on push / auth (push) Successful in 15s
Build on push / api (push) Successful in 14s
2025-10-19 19:48:41 +02:00
91269f351b Fixed init command regarding package handling
All checks were successful
Test before pr merge / test-lint (pull_request) Successful in 7s
Build on push / prepare (push) Successful in 11s
Build on push / core (push) Successful in 19s
Build on push / query (push) Successful in 23s
Build on push / cli (push) Successful in 16s
Build on push / dependency (push) Successful in 19s
Build on push / database (push) Successful in 16s
Build on push / application (push) Successful in 18s
Build on push / mail (push) Successful in 20s
Build on push / translation (push) Successful in 20s
Build on push / auth (push) Successful in 19s
Build on push / api (push) Successful in 15s
2025-10-19 19:37:13 +02:00
5d07973940 Fixed init command regarding package handling
Some checks failed
Test before pr merge / test-lint (pull_request) Failing after 7s
Build on push / prepare (push) Successful in 11s
Build on push / core (push) Successful in 19s
Build on push / query (push) Successful in 20s
Build on push / cli (push) Successful in 19s
Build on push / dependency (push) Successful in 22s
Build on push / database (push) Successful in 16s
Build on push / translation (push) Successful in 16s
Build on push / mail (push) Successful in 17s
Build on push / application (push) Successful in 17s
Build on push / auth (push) Successful in 18s
Build on push / api (push) Successful in 16s
2025-10-19 19:32:14 +02:00
0a9b7b81be Fixed init command regarding name None
All checks were successful
Test before pr merge / test-lint (pull_request) Successful in 7s
Build on push / prepare (push) Successful in 11s
Build on push / query (push) Successful in 19s
Build on push / core (push) Successful in 24s
Build on push / cli (push) Successful in 19s
Build on push / dependency (push) Successful in 18s
Build on push / database (push) Successful in 17s
Build on push / mail (push) Successful in 18s
Build on push / application (push) Successful in 20s
Build on push / translation (push) Successful in 20s
Build on push / auth (push) Successful in 15s
Build on push / api (push) Successful in 15s
2025-10-19 19:27:18 +02:00
21c88bc442 Fixed install.sh after renaming
All checks were successful
Test before pr merge / test-lint (pull_request) Successful in 7s
Build on push / prepare (push) Successful in 10s
Build on push / query (push) Successful in 19s
Build on push / core (push) Successful in 20s
Build on push / dependency (push) Successful in 15s
Build on push / cli (push) Successful in 18s
Build on push / database (push) Successful in 20s
Build on push / translation (push) Successful in 20s
Build on push / application (push) Successful in 21s
Build on push / mail (push) Successful in 24s
Build on push / auth (push) Successful in 15s
Build on push / api (push) Successful in 18s
2025-10-19 19:17:14 +02:00
04e0884e1c Added readme for devs
Some checks failed
Test before pr merge / test-lint (pull_request) Successful in 6s
Build on push / prepare (push) Successful in 12s
Build on push / core (push) Successful in 19s
Build on push / query (push) Successful in 19s
Build on push / api (push) Has been cancelled
Build on push / application (push) Has been cancelled
Build on push / auth (push) Has been cancelled
Build on push / database (push) Has been cancelled
Build on push / dependency (push) Has been cancelled
Build on push / cli (push) Has been cancelled
Build on push / mail (push) Has been cancelled
Build on push / translation (push) Has been cancelled
2025-10-19 19:16:39 +02:00
2ac7fa4568 Fixed dev build
All checks were successful
Test before pr merge / test-lint (pull_request) Successful in 6s
Build on push / prepare (push) Successful in 14s
Build on push / core (push) Successful in 21s
Build on push / query (push) Successful in 22s
Build on push / cli (push) Successful in 19s
Build on push / dependency (push) Successful in 21s
Build on push / translation (push) Successful in 16s
Build on push / application (push) Successful in 22s
Build on push / mail (push) Successful in 24s
Build on push / database (push) Successful in 26s
Build on push / auth (push) Successful in 15s
Build on push / api (push) Successful in 15s
2025-10-19 14:42:40 +02:00
43947055e5 Merge pull request 'cli' (#199) from cli into dev
Some checks failed
Test before pr merge / test-lint (pull_request) Successful in 7s
Build on push / prepare (push) Successful in 13s
Build on push / core (push) Failing after 9s
Build on push / query (push) Failing after 10s
Build on push / api (push) Has been skipped
Build on push / application (push) Has been skipped
Build on push / auth (push) Has been skipped
Build on push / cli (push) Has been skipped
Build on push / database (push) Has been skipped
Build on push / dependency (push) Has been skipped
Build on push / mail (push) Has been skipped
Build on push / translation (push) Has been skipped
Reviewed-on: #199
2025-10-19 14:40:45 +02:00
ba9edfa3fc Added cli dev build
All checks were successful
Test before pr merge / test-lint (pull_request) Successful in 6s
2025-10-19 14:40:24 +02:00
6d3e435da6 cpl new use venv & install deps on creation
All checks were successful
Test before pr merge / test-lint (pull_request) Successful in 6s
2025-10-19 14:39:27 +02:00
76b44ca517 Added cpl new
All checks were successful
Test before pr merge / test-lint (pull_request) Successful in 7s
2025-10-18 18:19:39 +02:00
8ebac05dd8 Cleanup
Some checks failed
Test before pr merge / test-lint (pull_request) Failing after 7s
2025-10-16 18:19:36 +02:00
98ed458d7c Added cpl generate
All checks were successful
Test before pr merge / test-lint (pull_request) Successful in 7s
2025-10-16 12:47:06 +02:00
8d0bc13cc0 Added live dev server
All checks were successful
Test before pr merge / test-lint (pull_request) Successful in 6s
2025-10-13 06:22:04 +02:00
33728cdec3 Added internal extra url
Some checks failed
Test before pr merge / test-lint (pull_request) Failing after 6s
2025-10-12 20:45:11 +02:00
f1604f1477 Removed ref for dev
Some checks failed
Test before pr merge / test-lint (pull_request) Failing after 7s
2025-10-12 20:22:40 +02:00
a02c101438 Improved spinner
Some checks failed
Test before pr merge / test-lint (pull_request) Failing after 8s
2025-10-12 20:02:45 +02:00
9647923647 Improved build 2025-10-12 18:19:58 +02:00
883fa2d691 Added build command
Some checks failed
Test before pr merge / test-lint (pull_request) Failing after 7s
2025-10-11 18:25:32 +02:00
3c26c73b41 Added run command
Some checks failed
Test before pr merge / test-lint (pull_request) Failing after 7s
2025-10-11 16:31:04 +02:00
faedf328cb Added build command
Some checks failed
Test before pr merge / test-lint (pull_request) Failing after 7s
2025-10-11 15:50:38 +02:00
849dd7a733 [WIP] Started run command 2025-10-11 14:13:28 +02:00
6e0ae1f25e Cleanup & fixes 2025-10-11 11:43:23 +02:00
45dcb400da Added remove & add commands
All checks were successful
Test before pr merge / test-lint (pull_request) Successful in 7s
2025-10-11 11:05:04 +02:00
104b736778 Moved commands by context 2025-10-11 09:39:46 +02:00
90ff8d466d Renamed project dirs
All checks were successful
Test before pr merge / test-lint (pull_request) Successful in 6s
2025-10-11 09:32:13 +02:00
f1aaaf2a5b Set version to all packages
All checks were successful
Test before pr merge / test-lint (pull_request) Successful in 9s
2025-10-09 21:24:25 +02:00
5f8519d4b3 Added version
All checks were successful
Test before pr merge / test-lint (pull_request) Successful in 7s
2025-10-09 21:08:05 +02:00
c4334f32ed Fixed install & added update 2025-10-09 19:59:02 +02:00
0a6a17acf6 Added cpl-cli 2025-10-09 17:17:39 +02:00
c02903b009 Merge pull request 'master' (#198) from master into dev
All checks were successful
Test before pr merge / test-lint (pull_request) Successful in 7s
Build on push / prepare (push) Successful in 11s
Build on push / query (push) Successful in 19s
Build on push / core (push) Successful in 19s
Build on push / dependency (push) Successful in 18s
Build on push / mail (push) Successful in 16s
Build on push / translation (push) Successful in 19s
Build on push / database (push) Successful in 20s
Build on push / application (push) Successful in 29s
Build on push / auth (push) Successful in 15s
Build on push / api (push) Successful in 18s
Reviewed-on: #198
2025-10-08 21:31:55 +02:00
b6cf5962aa Merge pull request 'Removed tools & docs' (#197) from dev_cleanup into master
Reviewed-on: #197
2025-10-08 21:30:47 +02:00
d3084041a9 Removed tools & docs 2025-10-08 21:30:01 +02:00
d4af923e4c Merge pull request 'Added gql base #181' (#196) from #181_gql into dev
All checks were successful
Test before pr merge / test-lint (pull_request) Successful in 7s
Build on push / prepare (push) Successful in 12s
Build on push / query (push) Successful in 23s
Build on push / core (push) Successful in 23s
Build on push / dependency (push) Successful in 29s
Build on push / translation (push) Successful in 17s
Build on push / application (push) Successful in 22s
Build on push / database (push) Successful in 22s
Build on push / mail (push) Successful in 23s
Build on push / auth (push) Successful in 15s
Build on push / api (push) Successful in 15s
Reviewed-on: #196
Closes #181
2025-10-08 21:25:41 +02:00
545540d05d Added subscriptions final #181
All checks were successful
Test before pr merge / test-lint (pull_request) Successful in 7s
2025-10-08 21:22:51 +02:00
3774cef56a Updated permissions #181
All checks were successful
Test before pr merge / test-lint (pull_request) Successful in 10s
2025-10-08 17:27:46 +02:00
cdb5e4ff89 [WIP] Subscriptions
All checks were successful
Test before pr merge / test-lint (pull_request) Successful in 6s
2025-10-04 06:57:14 +02:00
e362b7fb61 Added/Fixed api_key/user/role gql #181
Some checks failed
Test before pr merge / test-lint (pull_request) Failing after 6s
2025-10-04 05:44:13 +02:00
262e26cb83 Internal api key gql #181 2025-10-04 05:44:13 +02:00
5b3872a1fe Added possibility to put auth schema to root graphs #181 2025-10-04 05:44:13 +02:00
e7e3712e08 Renamed AuthUsers -> Users & completed user gql #181 2025-10-04 05:44:13 +02:00
df69f1c725 Recursive filter #181 2025-10-04 05:44:13 +02:00
39351a5eb9 Recursive types #181 2025-10-04 05:44:13 +02:00
39d06dfe48 Added mutations #181 2025-10-04 05:44:13 +02:00
3286a95cbf require any #181 2025-10-04 05:44:13 +02:00
6f46b94998 [WIP] with authentication #181 2025-10-04 05:44:13 +02:00
20e5da5770 Recursive complex filtering #181 2025-10-04 05:44:13 +02:00
a12a4082db Dao complex filtering #181 2025-10-04 05:44:13 +02:00
d8c60defba Further gql improvements & added test data #181 2025-10-04 05:44:13 +02:00
ada50c693e Changed to strawberry #181 2025-10-04 05:44:13 +02:00
a35b44b3b5 [WIP] collection #181 2025-10-04 05:44:13 +02:00
683805137a Added arguments to field #181 2025-10-04 05:44:13 +02:00
b0f1fb9839 Removed query base #181 2025-10-04 05:44:13 +02:00
e1ab9cf0db Added gql base #181 2025-10-04 05:44:13 +02:00
685c20e3bf Added cron jobs as hosted services
All checks were successful
Test before pr merge / test-lint (pull_request) Successful in 6s
Build on push / prepare (push) Successful in 11s
Build on push / core (push) Successful in 18s
Build on push / query (push) Successful in 19s
Build on push / dependency (push) Successful in 14s
Build on push / database (push) Successful in 15s
Build on push / application (push) Successful in 18s
Build on push / mail (push) Successful in 19s
Build on push / translation (push) Successful in 29s
Build on push / auth (push) Successful in 18s
Build on push / api (push) Successful in 18s
2025-10-04 05:38:21 +02:00
e0f6e1c241 Console & file logging format msg seperate & removed timestamp from console & minor fixes to di
All checks were successful
Test before pr merge / test-lint (pull_request) Successful in 7s
Build on push / prepare (push) Successful in 10s
Build on push / core (push) Successful in 18s
Build on push / query (push) Successful in 19s
Build on push / dependency (push) Successful in 14s
Build on push / translation (push) Successful in 16s
Build on push / mail (push) Successful in 17s
Build on push / application (push) Successful in 19s
Build on push / database (push) Successful in 19s
Build on push / auth (push) Successful in 25s
Build on push / api (push) Successful in 14s
2025-09-26 15:48:33 +02:00
c410a692be Better modules
All checks were successful
Test before pr merge / test-lint (pull_request) Successful in 6s
Build on push / prepare (push) Successful in 9s
Build on push / core (push) Successful in 18s
Build on push / query (push) Successful in 19s
Build on push / dependency (push) Successful in 17s
Build on push / mail (push) Successful in 16s
Build on push / translation (push) Successful in 16s
Build on push / application (push) Successful in 18s
Build on push / database (push) Successful in 19s
Build on push / auth (push) Successful in 17s
Build on push / api (push) Successful in 14s
2025-09-26 12:55:00 +02:00
56a16cbeba Module dependencies as static var
Some checks failed
Test before pr merge / test-lint (pull_request) Failing after 6s
Build on push / prepare (push) Successful in 10s
Build on push / query (push) Successful in 19s
Build on push / core (push) Successful in 20s
Build on push / dependency (push) Successful in 17s
Build on push / application (push) Successful in 15s
Build on push / database (push) Successful in 16s
Build on push / mail (push) Successful in 18s
Build on push / translation (push) Successful in 22s
Build on push / auth (push) Successful in 18s
Build on push / api (push) Successful in 17s
2025-09-26 08:46:30 +02:00
405 changed files with 7034 additions and 1016 deletions

View File

@@ -16,61 +16,68 @@ jobs:
uses: ./.gitea/workflows/package.yaml uses: ./.gitea/workflows/package.yaml
needs: [ prepare, application, auth, core, dependency ] needs: [ prepare, application, auth, core, dependency ]
with: with:
working_directory: src/cpl-api working_directory: src/api
secrets: inherit secrets: inherit
application: application:
uses: ./.gitea/workflows/package.yaml uses: ./.gitea/workflows/package.yaml
needs: [ prepare, core, dependency ] needs: [ prepare, core, dependency ]
with: with:
working_directory: src/cpl-application working_directory: src/application
secrets: inherit secrets: inherit
auth: auth:
uses: ./.gitea/workflows/package.yaml uses: ./.gitea/workflows/package.yaml
needs: [ prepare, core, dependency, database ] needs: [ prepare, core, dependency, database ]
with: with:
working_directory: src/cpl-auth working_directory: src/auth
secrets: inherit
cli:
uses: ./.gitea/workflows/package.yaml
needs: [ prepare, core ]
with:
working_directory: src/cli
secrets: inherit secrets: inherit
core: core:
uses: ./.gitea/workflows/package.yaml uses: ./.gitea/workflows/package.yaml
needs: [prepare] needs: [prepare]
with: with:
working_directory: src/cpl-core working_directory: src/core
secrets: inherit secrets: inherit
database: database:
uses: ./.gitea/workflows/package.yaml uses: ./.gitea/workflows/package.yaml
needs: [ prepare, core, dependency ] needs: [ prepare, core, dependency ]
with: with:
working_directory: src/cpl-database working_directory: src/database
secrets: inherit secrets: inherit
dependency: dependency:
uses: ./.gitea/workflows/package.yaml uses: ./.gitea/workflows/package.yaml
needs: [ prepare, core ] needs: [ prepare, core ]
with: with:
working_directory: src/cpl-dependency working_directory: src/dependency
secrets: inherit secrets: inherit
mail: mail:
uses: ./.gitea/workflows/package.yaml uses: ./.gitea/workflows/package.yaml
needs: [ prepare, core, dependency ] needs: [ prepare, core, dependency ]
with: with:
working_directory: src/cpl-mail working_directory: src/mail
secrets: inherit secrets: inherit
query: query:
uses: ./.gitea/workflows/package.yaml uses: ./.gitea/workflows/package.yaml
needs: [prepare] needs: [prepare]
with: with:
working_directory: src/cpl-query working_directory: src/query
secrets: inherit secrets: inherit
translation: translation:
uses: ./.gitea/workflows/package.yaml uses: ./.gitea/workflows/package.yaml
needs: [ prepare, core, dependency ] needs: [ prepare, core, dependency ]
with: with:
working_directory: src/cpl-translation working_directory: src/translation
secrets: inherit secrets: inherit

View File

@@ -36,6 +36,12 @@ jobs:
echo "Set version to $(cat /workspace/sh-edraft.de/cpl/version.txt)" echo "Set version to $(cat /workspace/sh-edraft.de/cpl/version.txt)"
cat pyproject.toml cat pyproject.toml
- name: Set package version
run: |
sed -i -E "s/^__version__ = \".*\"/__version__ = \"$(cat /workspace/sh-edraft.de/cpl/version.txt)\"/" cpl/*/__init__.py
echo "Set version to $(cat /workspace/sh-edraft.de/cpl/version.txt)"
cat cpl/*/__init__.py
- name: Set pip conf - name: Set pip conf
run: | run: |
cat > .pip.conf <<'EOF' cat > .pip.conf <<'EOF'

View File

@@ -0,0 +1,22 @@
## Prepare for development
After cloning the repository, run the following commands to set up your development environment:
```bash
python -m venv .venv
source .venv/bin/activate
# On Windows use `.venv\Scripts\activate`
# On Windows with git bash use `source .venv/Scripts/activate`
bash install.sh
```
Install cpl-cli as a development package:
```bash
pip install -e src/core
pip install -e src/cli
# test with:
cpl v
```
When using Pycharm, mark all directories under `src/` as "Sources Root" and `exa` to ensure proper module resolution.

12
cpl.workspace.json Normal file
View File

@@ -0,0 +1,12 @@
{
"name": "cpl",
"projects": [
"src/cli/cpl.project.json",
"src/core/cpl.project.json",
"src/mail/cpl.project.json"
],
"defaultProject": "cpl-cli",
"scripts": {
"format": "black src"
}
}

View File

@@ -1,49 +1,117 @@
from starlette.responses import JSONResponse from starlette.responses import JSONResponse
from cpl.dependency.event_bus import EventBusABC
from cpl.graphql.event_bus.memory import InMemoryEventBus
from queries.cities import CityGraphType, CityFilter, CitySort
from queries.hello import UserGraphType # , UserFilter, UserSort, UserGraphType
from queries.user import UserFilter, UserSort
from cpl.api.api_module import ApiModule from cpl.api.api_module import ApiModule
from cpl.api.application.web_app import WebApp
from cpl.application.application_builder import ApplicationBuilder from cpl.application.application_builder import ApplicationBuilder
from cpl.auth.permission.permissions import Permissions from cpl.auth.schema import User, Role
from cpl.auth.schema import AuthUser, Role
from cpl.core.configuration import Configuration from cpl.core.configuration import Configuration
from cpl.core.console import Console from cpl.core.console import Console
from cpl.core.environment import Environment from cpl.core.environment import Environment
from cpl.core.utils.cache import Cache from cpl.core.utils.cache import Cache
from cpl.database.mysql.mysql_module import MySQLModule from cpl.database.mysql.mysql_module import MySQLModule
from cpl.graphql.application.graphql_app import GraphQLApp
from cpl.graphql.auth.graphql_auth_module import GraphQLAuthModule
from cpl.graphql.graphql_module import GraphQLModule
from model.author_dao import AuthorDao
from model.author_query import AuthorGraphType, AuthorFilter, AuthorSort
from model.post_dao import PostDao
from model.post_query import PostFilter, PostSort, PostGraphType, PostMutation, PostSubscription
from permissions import PostPermissions
from queries.hello import HelloQuery
from scoped_service import ScopedService from scoped_service import ScopedService
from service import PingService from service import PingService
from test_data_seeder import TestDataSeeder
def main(): def main():
builder = ApplicationBuilder[WebApp](WebApp) builder = ApplicationBuilder[GraphQLApp](GraphQLApp)
Configuration.add_json_file(f"appsettings.json") Configuration.add_json_file(f"appsettings.json")
Configuration.add_json_file(f"appsettings.{Environment.get_environment()}.json") Configuration.add_json_file(f"appsettings.{Environment.get_environment()}.json")
Configuration.add_json_file(f"appsettings.{Environment.get_host_name()}.json", optional=True) Configuration.add_json_file(f"appsettings.{Environment.get_host_name()}.json", optional=True)
# builder.services.add_logging() # builder.services.add_logging()
builder.services.add_structured_logging() (
builder.services.add_transient(PingService) builder.services.add_structured_logging()
builder.services.add_module(MySQLModule) .add_transient(PingService)
builder.services.add_module(ApiModule) .add_module(MySQLModule)
.add_module(ApiModule)
builder.services.add_scoped(ScopedService) .add_module(GraphQLModule)
.add_module(GraphQLAuthModule)
builder.services.add_cache(AuthUser) .add_scoped(ScopedService)
builder.services.add_cache(Role) .add_singleton(EventBusABC, InMemoryEventBus)
.add_cache(User)
.add_cache(Role)
.add_transient(CityGraphType)
.add_transient(CityFilter)
.add_transient(CitySort)
.add_transient(UserGraphType)
.add_transient(UserFilter)
.add_transient(UserSort)
# .add_transient(UserGraphType)
# .add_transient(UserFilter)
# .add_transient(UserSort)
.add_transient(HelloQuery)
# test data
.add_singleton(TestDataSeeder)
# authors
.add_transient(AuthorDao)
.add_transient(AuthorGraphType)
.add_transient(AuthorFilter)
.add_transient(AuthorSort)
# posts
.add_transient(PostDao)
.add_transient(PostGraphType)
.add_transient(PostFilter)
.add_transient(PostSort)
.add_transient(PostMutation)
.add_transient(PostSubscription)
)
app = builder.build() app = builder.build()
app.with_logging() app.with_logging()
app.with_database() app.with_migrations("./scripts")
app.with_authentication() app.with_authentication()
app.with_authorization() app.with_authorization()
app.with_route(path="/route1", fn=lambda r: JSONResponse("route1"), method="GET", authentication=True, permissions=[Permissions.administrator]) app.with_route(
path="/route1",
fn=lambda r: JSONResponse("route1"),
method="GET",
# authentication=True,
# permissions=[Permissions.administrator],
)
app.with_routes_directory("routes") app.with_routes_directory("routes")
schema = app.with_graphql()
schema.query.string_field("ping", resolver=lambda: "pong")
schema.query.with_query("hello", HelloQuery)
schema.query.dao_collection_field(AuthorGraphType, AuthorDao, "authors", AuthorFilter, AuthorSort)
(
schema.query.dao_collection_field(PostGraphType, PostDao, "posts", PostFilter, PostSort)
# .with_require_any_permission(PostPermissions.read)
.with_public()
)
schema.mutation.with_mutation("post", PostMutation).with_public()
schema.subscription.with_subscription(PostSubscription)
app.with_auth_root_queries(True)
app.with_auth_root_mutations(True)
app.with_playground()
app.with_graphiql()
app.with_permissions(PostPermissions)
provider = builder.service_provider provider = builder.service_provider
user_cache = provider.get_service(Cache[AuthUser]) user_cache = provider.get_service(Cache[User])
role_cache = provider.get_service(Cache[Role]) role_cache = provider.get_service(Cache[Role])
if role_cache == user_cache: if role_cache == user_cache:

View File

@@ -0,0 +1,30 @@
from datetime import datetime
from typing import Self
from cpl.core.typing import SerialId
from cpl.database.abc import DbModelABC
class Author(DbModelABC[Self]):
def __init__(
self,
id: int,
first_name: str,
last_name: str,
deleted: bool = False,
editor_id: SerialId | None = None,
created: datetime | None = None,
updated: datetime | None = None,
):
DbModelABC.__init__(self, id, deleted, editor_id, created, updated)
self._first_name = first_name
self._last_name = last_name
@property
def first_name(self) -> str:
return self._first_name
@property
def last_name(self) -> str:
return self._last_name

View File

@@ -0,0 +1,11 @@
from cpl.database.abc import DbModelDaoABC
from model.author import Author
class AuthorDao(DbModelDaoABC):
def __init__(self):
DbModelDaoABC.__init__(self, Author, "authors")
self.attribute(Author.first_name, str, db_name="firstname")
self.attribute(Author.last_name, str, db_name="lastname")

View File

@@ -0,0 +1,37 @@
from cpl.graphql.schema.db_model_graph_type import DbModelGraphType
from cpl.graphql.schema.filter.db_model_filter import DbModelFilter
from cpl.graphql.schema.sort.sort import Sort
from cpl.graphql.schema.sort.sort_order import SortOrder
from model.author import Author
class AuthorFilter(DbModelFilter[Author]):
def __init__(self):
DbModelFilter.__init__(self, public=True)
self.int_field("id")
self.string_field("firstName")
self.string_field("lastName")
class AuthorSort(Sort[Author]):
def __init__(self):
Sort.__init__(self)
self.field("id", SortOrder)
self.field("firstName", SortOrder)
self.field("lastName", SortOrder)
class AuthorGraphType(DbModelGraphType[Author]):
def __init__(self):
DbModelGraphType.__init__(self, public=True)
self.int_field(
"id",
resolver=lambda root: root.id,
).with_public(True)
self.string_field(
"firstName",
resolver=lambda root: root.first_name,
).with_public(True)
self.string_field(
"lastName",
resolver=lambda root: root.last_name,
).with_public(True)

View File

@@ -0,0 +1,44 @@
from datetime import datetime
from typing import Self
from cpl.core.typing import SerialId
from cpl.database.abc import DbModelABC
class Post(DbModelABC[Self]):
def __init__(
self,
id: int,
author_id: SerialId,
title: str,
content: str,
deleted: bool = False,
editor_id: SerialId | None = None,
created: datetime | None = None,
updated: datetime | None = None,
):
DbModelABC.__init__(self, id, deleted, editor_id, created, updated)
self._author_id = author_id
self._title = title
self._content = content
@property
def author_id(self) -> SerialId:
return self._author_id
@property
def title(self) -> str:
return self._title
@title.setter
def title(self, value: str):
self._title = value
@property
def content(self) -> str:
return self._content
@content.setter
def content(self, value: str):
self._content = value

View File

@@ -0,0 +1,15 @@
from cpl.database.abc import DbModelDaoABC
from model.author_dao import AuthorDao
from model.post import Post
class PostDao(DbModelDaoABC[Post]):
def __init__(self, authors: AuthorDao):
DbModelDaoABC.__init__(self, Post, "posts")
self.attribute(Post.author_id, int, db_name="authorId")
self.reference("author", "id", Post.author_id, "authors", authors)
self.attribute(Post.title, str)
self.attribute(Post.content, str)

View File

@@ -0,0 +1,148 @@
from cpl.dependency.event_bus import EventBusABC
from cpl.graphql.query_context import QueryContext
from cpl.graphql.schema.db_model_graph_type import DbModelGraphType
from cpl.graphql.schema.filter.db_model_filter import DbModelFilter
from cpl.graphql.schema.input import Input
from cpl.graphql.schema.mutation import Mutation
from cpl.graphql.schema.sort.sort import Sort
from cpl.graphql.schema.sort.sort_order import SortOrder
from cpl.graphql.schema.subscription import Subscription
from model.author_dao import AuthorDao
from model.author_query import AuthorGraphType, AuthorFilter
from model.post import Post
from model.post_dao import PostDao
class PostFilter(DbModelFilter[Post]):
def __init__(self):
DbModelFilter.__init__(self, public=True)
self.int_field("id")
self.filter_field("author", AuthorFilter)
self.string_field("title")
self.string_field("content")
class PostSort(Sort[Post]):
def __init__(self):
Sort.__init__(self)
self.field("id", SortOrder)
self.field("title", SortOrder)
self.field("content", SortOrder)
class PostGraphType(DbModelGraphType[Post]):
def __init__(self, authors: AuthorDao):
DbModelGraphType.__init__(self, public=True)
self.int_field(
"id",
resolver=lambda root: root.id,
).with_optional().with_public(True)
async def _a(root: Post):
return await authors.get_by_id(root.author_id)
def r_name(ctx: QueryContext):
return ctx.user.username == "admin"
self.object_field("author", AuthorGraphType, resolver=_a).with_public(True) # .with_require_any([], [r_name]))
self.string_field(
"title",
resolver=lambda root: root.title,
).with_public(True)
self.string_field(
"content",
resolver=lambda root: root.content,
).with_public(True)
class PostCreateInput(Input[Post]):
title: str
content: str
author_id: int
def __init__(self):
Input.__init__(self)
self.string_field("title").with_required()
self.string_field("content").with_required()
self.int_field("author_id").with_required()
class PostUpdateInput(Input[Post]):
title: str
content: str
author_id: int
def __init__(self):
Input.__init__(self)
self.int_field("id").with_required()
self.string_field("title").with_required(False)
self.string_field("content").with_required(False)
class PostSubscription(Subscription):
def __init__(self, bus: EventBusABC):
Subscription.__init__(self)
self._bus = bus
def selector(event: Post, info) -> bool:
return event.id == 101
self.subscription_field("postChange", PostGraphType, selector).with_public()
class PostMutation(Mutation):
def __init__(self, posts: PostDao, authors: AuthorDao, bus: EventBusABC):
Mutation.__init__(self)
self._posts = posts
self._authors = authors
self._bus = bus
self.field("create", int, resolver=self.create_post).with_public().with_required().with_argument(
"input",
PostCreateInput,
).with_required()
self.field("update", bool, resolver=self.update_post).with_public().with_required().with_argument(
"input",
PostUpdateInput,
).with_required()
self.field("delete", bool, resolver=self.delete_post).with_public().with_required().with_argument(
"id",
int,
).with_required()
self.field("restore", bool, resolver=self.restore_post).with_public().with_required().with_argument(
"id",
int,
).with_required()
async def create_post(self, input: PostCreateInput) -> int:
return await self._posts.create(Post(0, input.author_id, input.title, input.content))
async def update_post(self, input: PostUpdateInput) -> bool:
post = await self._posts.get_by_id(input.id)
if post is None:
return False
post.title = input.title if input.title is not None else post.title
post.content = input.content if input.content is not None else post.content
await self._posts.update(post)
await self._bus.publish("postChange", post)
return True
async def delete_post(self, id: int) -> bool:
post = await self._posts.get_by_id(id)
if post is None:
return False
await self._posts.delete(post)
return True
async def restore_post(self, id: int) -> bool:
post = await self._posts.get_by_id(id)
if post is None:
return False
await self._posts.restore(post)
return True

View File

@@ -0,0 +1,8 @@
from enum import Enum
class PostPermissions(Enum):
read = "post.read"
write = "post.write"
delete = "post.delete"

View File

@@ -0,0 +1,39 @@
from cpl.graphql.schema.filter.filter import Filter
from cpl.graphql.schema.graph_type import GraphType
from cpl.graphql.schema.sort.sort import Sort
from cpl.graphql.schema.sort.sort_order import SortOrder
class City:
def __init__(self, id: int, name: str):
self.id = id
self.name = name
class CityFilter(Filter[City]):
def __init__(self):
Filter.__init__(self)
self.field("id", int)
self.field("name", str)
class CitySort(Sort[City]):
def __init__(self):
Sort.__init__(self)
self.field("id", SortOrder)
self.field("name", SortOrder)
class CityGraphType(GraphType[City]):
def __init__(self):
GraphType.__init__(self)
self.int_field(
"id",
resolver=lambda root: root.id,
)
self.string_field(
"name",
resolver=lambda root: root.name,
)

View File

@@ -0,0 +1,70 @@
from queries.cities import CityFilter, CitySort, CityGraphType, City
from queries.user import User, UserFilter, UserSort, UserGraphType
from cpl.api.middleware.request import get_request
from cpl.auth.schema import UserDao, User
from cpl.graphql.schema.filter.filter import Filter
from cpl.graphql.schema.graph_type import GraphType
from cpl.graphql.schema.query import Query
from cpl.graphql.schema.sort.sort import Sort
from cpl.graphql.schema.sort.sort_order import SortOrder
users = [User(i, f"User {i}") for i in range(1, 101)]
cities = [City(i, f"City {i}") for i in range(1, 101)]
# class UserFilter(Filter[User]):
# def __init__(self):
# Filter.__init__(self)
# self.field("id", int)
# self.field("username", str)
#
#
# class UserSort(Sort[User]):
# def __init__(self):
# Sort.__init__(self)
# self.field("id", SortOrder)
# self.field("username", SortOrder)
#
# class UserGraphType(GraphType[User]):
#
# def __init__(self):
# GraphType.__init__(self)
#
# self.int_field(
# "id",
# resolver=lambda root: root.id,
# )
# self.string_field(
# "username",
# resolver=lambda root: root.username,
# )
class HelloQuery(Query):
def __init__(self):
Query.__init__(self)
self.string_field(
"message",
resolver=lambda name: f"Hello {name} {get_request().state.request_id}",
).with_argument("name", str, "Name to greet", "world")
self.collection_field(
UserGraphType,
"users",
UserFilter,
UserSort,
resolver=lambda: users,
)
self.collection_field(
CityGraphType,
"cities",
CityFilter,
CitySort,
resolver=lambda: cities,
)
# self.dao_collection_field(
# UserGraphType,
# UserDao,
# "Users",
# UserFilter,
# UserSort,
# )

View File

@@ -0,0 +1,39 @@
from cpl.graphql.schema.filter.filter import Filter
from cpl.graphql.schema.graph_type import GraphType
from cpl.graphql.schema.sort.sort import Sort
from cpl.graphql.schema.sort.sort_order import SortOrder
class User:
def __init__(self, id: int, name: str):
self.id = id
self.name = name
class UserFilter(Filter[User]):
def __init__(self):
Filter.__init__(self)
self.field("id", int)
self.field("name", str)
class UserSort(Sort[User]):
def __init__(self):
Sort.__init__(self)
self.field("id", SortOrder)
self.field("name", SortOrder)
class UserGraphType(GraphType[User]):
def __init__(self):
GraphType.__init__(self)
self.int_field(
"id",
resolver=lambda root: root.id,
)
self.string_field(
"name",
resolver=lambda root: root.name,
)

View File

@@ -0,0 +1,22 @@
CREATE TABLE IF NOT EXISTS `authors` (
`id` INT(30) NOT NULL AUTO_INCREMENT,
`firstname` VARCHAR(64) NOT NULL,
`lastname` VARCHAR(64) NOT NULL,
deleted BOOLEAN NOT NULL DEFAULT FALSE,
editorId INT NULL,
created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY(`id`)
);
CREATE TABLE IF NOT EXISTS `posts` (
`id` INT(30) NOT NULL AUTO_INCREMENT,
`authorId` INT(30) NOT NULL REFERENCES `authors`(`id`) ON DELETE CASCADE,
`title` TEXT NOT NULL,
`content` TEXT NOT NULL,
deleted BOOLEAN NOT NULL DEFAULT FALSE,
editorId INT NULL,
created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY(`id`)
);

View File

@@ -0,0 +1,48 @@
from faker import Faker
from cpl.database.abc import DataSeederABC
from cpl.query import Enumerable
from model.author import Author
from model.author_dao import AuthorDao
from model.post import Post
from model.post_dao import PostDao
fake = Faker()
class TestDataSeeder(DataSeederABC):
def __init__(self, authors: AuthorDao, posts: PostDao):
DataSeederABC.__init__(self)
self._authors = authors
self._posts = posts
async def seed(self):
if await self._authors.count() == 0:
await self._seed_authors()
if await self._posts.count() == 0:
await self._seed_posts()
async def _seed_authors(self):
authors = Enumerable.range(0, 35).select(
lambda x: Author(
0,
fake.first_name(),
fake.last_name(),
)
).to_list()
await self._authors.create_many(authors, skip_editor=True)
async def _seed_posts(self):
posts = Enumerable.range(0, 100).select(
lambda x: Post(
id=0,
author_id=fake.random_int(min=1, max=35),
title=fake.sentence(nb_words=6),
content=fake.paragraph(nb_sentences=6),
)
).to_list()
await self._posts.create_many(posts, skip_editor=True)

View File

@@ -4,6 +4,7 @@ from cpl.core.console import Console
from cpl.core.environment import Environment from cpl.core.environment import Environment
from cpl.core.log import LoggerABC from cpl.core.log import LoggerABC
from cpl.dependency import ServiceProvider from cpl.dependency import ServiceProvider
from cpl.dependency.typing import Modules
from model.city import City from model.city import City
from model.city_dao import CityDao from model.city_dao import CityDao
from model.user import User from model.user import User
@@ -11,8 +12,8 @@ from model.user_dao import UserDao
class Application(ApplicationABC): class Application(ApplicationABC):
def __init__(self, services: ServiceProvider): def __init__(self, services: ServiceProvider, modules: Modules):
ApplicationABC.__init__(self, services) ApplicationABC.__init__(self, services, modules)
self._logger = services.get_service(LoggerABC) self._logger = services.get_service(LoggerABC)

View File

@@ -3,6 +3,7 @@ from cpl.application import ApplicationBuilder
from cpl.auth.permission.permissions_registry import PermissionsRegistry from cpl.auth.permission.permissions_registry import PermissionsRegistry
from cpl.core.console import Console from cpl.core.console import Console
from cpl.core.log import LogLevel from cpl.core.log import LogLevel
from cpl.database import DatabaseModule
from custom_permissions import CustomPermissions from custom_permissions import CustomPermissions
from startup import Startup from startup import Startup
@@ -10,13 +11,12 @@ from startup import Startup
def main(): def main():
builder = ApplicationBuilder(Application).with_startup(Startup) builder = ApplicationBuilder(Application).with_startup(Startup)
builder.services.add_logging() builder.services.add_logging()
app = builder.build() app = builder.build()
app.with_logging(LogLevel.trace) app.with_logging(LogLevel.trace)
app.with_permissions(CustomPermissions) app.with_permissions(CustomPermissions)
app.with_migrations("./scripts") app.with_migrations("./scripts")
app.with_seeders() # app.with_seeders()
Console.write_line(CustomPermissions.test.value in PermissionsRegistry.get()) Console.write_line(CustomPermissions.test.value in PermissionsRegistry.get())
app.run() app.run()

View File

@@ -5,16 +5,16 @@ from cpl.core.typing import SerialId
from cpl.database.abc.db_model_abc import DbModelABC from cpl.database.abc.db_model_abc import DbModelABC
class City(DbModelABC): class City(DbModelABC[Self]):
def __init__( def __init__(
self, self,
id: int, id: int,
name: str, name: str,
zip: str, zip: str,
deleted: bool = False, deleted: bool = False,
editor_id: Optional[SerialId] = None, editor_id: SerialId | None = None,
created: Optional[datetime] = None, created: datetime | None= None,
updated: Optional[datetime] = None, updated: datetime | None= None,
): ):
DbModelABC.__init__(self, id, deleted, editor_id, created, updated) DbModelABC.__init__(self, id, deleted, editor_id, created, updated)
self._name = name self._name = name

View File

@@ -5,7 +5,7 @@ from cpl.core.typing import SerialId
from cpl.database.abc.db_model_abc import DbModelABC from cpl.database.abc.db_model_abc import DbModelABC
class User(DbModelABC): class User(DbModelABC[Self]):
def __init__( def __init__(
self, self,
@@ -13,9 +13,9 @@ class User(DbModelABC):
name: str, name: str,
city_id: int = 0, city_id: int = 0,
deleted: bool = False, deleted: bool = False,
editor_id: Optional[SerialId] = None, editor_id: SerialId | None = None,
created: Optional[datetime] = None, created: datetime | None= None,
updated: Optional[datetime] = None, updated: datetime | None= None,
): ):
DbModelABC.__init__(self, id, deleted, editor_id, created, updated) DbModelABC.__init__(self, id, deleted, editor_id, created, updated)
self._name = name self._name = name

View File

@@ -6,7 +6,7 @@ from cpl.auth.permission.permission_module import PermissionsModule
from cpl.core.configuration import Configuration from cpl.core.configuration import Configuration
from cpl.core.environment import Environment from cpl.core.environment import Environment
from cpl.core.log import Logger, LoggerABC from cpl.core.log import Logger, LoggerABC
from cpl.database import mysql from cpl.database import mysql, DatabaseModule
from cpl.database.abc.data_access_object_abc import DataAccessObjectABC from cpl.database.abc.data_access_object_abc import DataAccessObjectABC
from cpl.database.mysql.mysql_module import MySQLModule from cpl.database.mysql.mysql_module import MySQLModule
from cpl.dependency import ServiceCollection from cpl.dependency import ServiceCollection
@@ -25,6 +25,7 @@ class Startup(StartupABC):
@staticmethod @staticmethod
async def configure_services(services: ServiceCollection): async def configure_services(services: ServiceCollection):
services.add_module(MySQLModule) services.add_module(MySQLModule)
services.add_module(DatabaseModule)
services.add_module(AuthModule) services.add_module(AuthModule)
services.add_module(PermissionsModule) services.add_module(PermissionsModule)

View File

@@ -1,3 +1,4 @@
import asyncio
import time import time
from cpl.application.abc import ApplicationABC from cpl.application.abc import ApplicationABC
@@ -7,6 +8,7 @@ from cpl.core.environment import Environment
from cpl.core.log import LoggerABC from cpl.core.log import LoggerABC
from cpl.core.pipes import IPAddressPipe from cpl.core.pipes import IPAddressPipe
from cpl.dependency import ServiceProvider from cpl.dependency import ServiceProvider
from cpl.dependency.typing import Modules
from cpl.mail import EMail, EMailClientABC from cpl.mail import EMail, EMailClientABC
from cpl.query import List from cpl.query import List
from scoped_service import ScopedService from scoped_service import ScopedService
@@ -16,8 +18,8 @@ from test_settings import TestSettings
class Application(ApplicationABC): class Application(ApplicationABC):
def __init__(self, services: ServiceProvider): def __init__(self, services: ServiceProvider, modules: Modules):
ApplicationABC.__init__(self, services) ApplicationABC.__init__(self, services, modules)
self._logger = self._services.get_service(LoggerABC) self._logger = self._services.get_service(LoggerABC)
self._mailer = self._services.get_service(EMailClientABC) self._mailer = self._services.get_service(EMailClientABC)
@@ -35,7 +37,7 @@ class Application(ApplicationABC):
def _wait(time_ms: int): def _wait(time_ms: int):
time.sleep(time_ms) time.sleep(time_ms)
def main(self): async def main(self):
self._logger.debug(f"Host: {Environment.get_host_name()}") self._logger.debug(f"Host: {Environment.get_host_name()}")
self._logger.debug(f"Environment: {Environment.get_environment()}") self._logger.debug(f"Environment: {Environment.get_environment()}")
Console.write_line(List(range(0, 10)).select(lambda x: f"x={x}").to_list()) Console.write_line(List(range(0, 10)).select(lambda x: f"x={x}").to_list())
@@ -73,9 +75,3 @@ class Application(ApplicationABC):
test_settings1 = Configuration.get(TestSettings) test_settings1 = Configuration.get(TestSettings)
Console.write_line(test_settings1.value) Console.write_line(test_settings1.value)
# self.test_send_mail() # self.test_send_mail()
x = 0
while x < 5:
Console.write_line("Running...")
x += 1
time.sleep(5)

View File

@@ -1,8 +1,10 @@
import asyncio import asyncio
import time from datetime import datetime
from cpl.core.console import Console from cpl.core.console import Console
from cpl.dependency.hosted.hosted_service import HostedService from cpl.core.time.cron import Cron
from cpl.core.service.cronjob import CronjobABC
from cpl.core.service.hosted_service import HostedService
class Hosted(HostedService): class Hosted(HostedService):
@@ -17,4 +19,12 @@ class Hosted(HostedService):
async def stop(self): async def stop(self):
Console.write_line("Hosted Service Stopped") Console.write_line("Hosted Service Stopped")
self._stopped = True self._stopped = True
class MyCronJob(CronjobABC):
def __init__(self):
CronjobABC.__init__(self, Cron("*/1 * * * *")) # Every minute
async def loop(self):
Console.write_line(f"[{datetime.now()}] Hello from Cronjob!")

View File

@@ -1,11 +1,13 @@
from application import Application from application import Application
from cpl.application import ApplicationBuilder from cpl.application import ApplicationBuilder
from cpl.core.console import Console
from test_extension import TestExtension from test_extension import TestExtension
from startup import Startup from startup import Startup
from test_startup_extension import TestStartupExtension from test_startup_extension import TestStartupExtension
def main(): def main():
Console.write_line("\n\n--- Application Starting ---\n")
app_builder = ApplicationBuilder(Application) app_builder = ApplicationBuilder(Application)
app_builder.with_startup(Startup) app_builder.with_startup(Startup)
app_builder.with_extension(TestStartupExtension) app_builder.with_extension(TestStartupExtension)

View File

@@ -1,11 +1,10 @@
from cpl import mail
from cpl.application.abc import StartupABC from cpl.application.abc import StartupABC
from cpl.core.configuration import Configuration from cpl.core.configuration import Configuration
from cpl.core.environment import Environment from cpl.core.environment import Environment
from cpl.core.pipes import IPAddressPipe from cpl.core.pipes import IPAddressPipe
from cpl.dependency import ServiceCollection from cpl.dependency import ServiceCollection
from cpl.mail.mail_module import MailModule from cpl.mail.mail_module import MailModule
from hosted_service import Hosted from hosted_service import Hosted, MyCronJob
from scoped_service import ScopedService from scoped_service import ScopedService
from test_service import TestService from test_service import TestService
@@ -26,3 +25,4 @@ class Startup(StartupABC):
services.add_singleton(TestService) services.add_singleton(TestService)
services.add_scoped(ScopedService) services.add_scoped(ScopedService)
services.add_hosted_service(Hosted) services.add_hosted_service(Hosted)
services.add_hosted_service(MyCronJob)

View File

@@ -1,15 +1,15 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail set -euo pipefail
# Find and combine requirements from src/cpl-*/requirements.txt, # Find and combine requirements from src/*/requirements.txt,
# filtering out lines whose *package name* starts with "cpl-". # filtering out lines whose *package name* starts with "cpl-".
# Works with pinned versions, extras, markers, editable installs, and VCS refs. # Works with pinned versions, extras, markers, editable installs, and VCS refs.
shopt -s nullglob shopt -s nullglob
req_files=(src/cpl-*/requirements.txt) req_files=(src/*/requirements.txt)
if ((${#req_files[@]} == 0)); then if ((${#req_files[@]} == 0)); then
echo "No requirements files found at src/cpl-*/requirements.txt" >&2 echo "No requirements files found at src/*/requirements.txt" >&2
exit 1 exit 1
fi fi

View File

@@ -2,3 +2,5 @@ from .error import APIError, AlreadyExists, EndpointNotImplemented, Forbidden, N
from .logger import APILogger from .logger import APILogger
from .settings import ApiSettings from .settings import ApiSettings
from .api_module import ApiModule from .api_module import ApiModule
__version__ = "1.0.0"

View File

@@ -0,0 +1,45 @@
from abc import ABC
from enum import Enum
from typing import Self
from starlette.applications import Starlette
from cpl.api.model.api_route import ApiRoute
from cpl.api.model.validation_match import ValidationMatch
from cpl.api.typing import HTTPMethods, PartialMiddleware, TEndpoint, PolicyInput
from cpl.application.abc.application_abc import ApplicationABC
from cpl.dependency.service_provider import ServiceProvider
from cpl.dependency.typing import Modules
class WebAppABC(ApplicationABC, ABC):
def __init__(self, services: ServiceProvider, modules: Modules, required_modules: list[str | object] = None):
ApplicationABC.__init__(self, services, modules, required_modules)
def with_routes_directory(self, directory: str) -> Self: ...
def with_app(self, app: Starlette) -> Self: ...
def with_routes(
self,
routes: list[ApiRoute],
method: HTTPMethods,
authentication: bool = False,
roles: list[str | Enum] = None,
permissions: list[str | Enum] = None,
policies: list[str] = None,
match: ValidationMatch = None,
) -> Self: ...
def with_route(
self,
path: str,
fn: TEndpoint,
method: HTTPMethods,
authentication: bool = False,
roles: list[str | Enum] = None,
permissions: list[str | Enum] = None,
policies: list[str] = None,
match: ValidationMatch = None,
) -> Self: ...
def with_middleware(self, middleware: PartialMiddleware) -> Self: ...
def with_authentication(self) -> Self: ...
def with_authorization(self, *policies: list[PolicyInput] | PolicyInput) -> Self: ...

View File

@@ -0,0 +1,22 @@
from cpl.api import ApiSettings
from cpl.api.registry.policy import PolicyRegistry
from cpl.api.registry.route import RouteRegistry
from cpl.auth.auth_module import AuthModule
from cpl.auth.permission.permission_module import PermissionsModule
from cpl.database.database_module import DatabaseModule
from cpl.dependency import ServiceCollection
from cpl.dependency.module.module import Module
class ApiModule(Module):
config = [ApiSettings]
singleton = [
PolicyRegistry,
RouteRegistry,
]
@staticmethod
def register(collection: ServiceCollection):
collection.add_module(DatabaseModule)
collection.add_module(AuthModule)
collection.add_module(PermissionsModule)

View File

@@ -1,6 +1,6 @@
import os import os
from enum import Enum from enum import Enum
from typing import Mapping, Any, Callable, Self, Union from typing import Mapping, Any, Self
import uvicorn import uvicorn
from starlette.applications import Starlette from starlette.applications import Starlette
@@ -10,7 +10,8 @@ from starlette.requests import Request
from starlette.responses import JSONResponse from starlette.responses import JSONResponse
from starlette.types import ExceptionHandler from starlette.types import ExceptionHandler
from cpl import api, auth from cpl.api.abc.web_app_abc import WebAppABC
from cpl.api.api_module import ApiModule
from cpl.api.error import APIError from cpl.api.error import APIError
from cpl.api.logger import APILogger from cpl.api.logger import APILogger
from cpl.api.middleware.authentication import AuthenticationMiddleware from cpl.api.middleware.authentication import AuthenticationMiddleware
@@ -24,21 +25,20 @@ from cpl.api.registry.policy import PolicyRegistry
from cpl.api.registry.route import RouteRegistry from cpl.api.registry.route import RouteRegistry
from cpl.api.router import Router from cpl.api.router import Router
from cpl.api.settings import ApiSettings from cpl.api.settings import ApiSettings
from cpl.api.typing import HTTPMethods, PartialMiddleware, PolicyResolver from cpl.api.typing import HTTPMethods, PartialMiddleware, TEndpoint, PolicyInput
from cpl.api.api_module import ApiModule
from cpl.application.abc.application_abc import ApplicationABC
from cpl.auth.auth_module import AuthModule from cpl.auth.auth_module import AuthModule
from cpl.auth.permission.permission_module import PermissionsModule from cpl.auth.permission.permission_module import PermissionsModule
from cpl.core.configuration import Configuration from cpl.core.configuration.configuration import Configuration
from cpl.dependency.inject import inject from cpl.dependency.inject import inject
from cpl.dependency.service_provider import ServiceProvider from cpl.dependency.service_provider import ServiceProvider
from cpl.dependency.typing import Modules
PolicyInput = Union[dict[str, PolicyResolver], Policy]
class WebApp(ApplicationABC): class WebApp(WebAppABC):
def __init__(self, services: ServiceProvider): def __init__(self, services: ServiceProvider, modules: Modules, required_modules: list[str | object] = None):
super().__init__(services, [AuthModule, PermissionsModule, ApiModule]) WebAppABC.__init__(
self, services, modules, [AuthModule, PermissionsModule, ApiModule] + (required_modules or [])
)
self._app: Starlette | None = None self._app: Starlette | None = None
self._logger = services.get_service(APILogger) self._logger = services.get_service(APILogger)
@@ -78,21 +78,17 @@ class WebApp(ApplicationABC):
self._logger.debug(f"Allowed origins: {origins}") self._logger.debug(f"Allowed origins: {origins}")
return origins.split(",") return origins.split(",")
def with_database(self) -> Self:
self.with_migrations()
self.with_seeders()
return self
def with_app(self, app: Starlette) -> Self:
assert app is not None, "app must not be None"
assert isinstance(app, Starlette), "app must be an instance of Starlette"
self._app = app
return self
def _check_for_app(self): def _check_for_app(self):
if self._app is not None: if self._app is not None:
raise ValueError("App is already set, cannot add routes or middleware") raise ValueError("App is already set, cannot add routes or middleware")
def _validate_policies(self):
for rule in Router.get_authorization_rules():
for policy_name in rule["policies"]:
policy = self._policies.get(policy_name)
if not policy:
self._logger.fatal(f"Authorization policy '{policy_name}' not found")
def with_routes_directory(self, directory: str) -> Self: def with_routes_directory(self, directory: str) -> Self:
self._check_for_app() self._check_for_app()
assert directory is not None, "directory must not be None" assert directory is not None, "directory must not be None"
@@ -107,6 +103,12 @@ class WebApp(ApplicationABC):
return self return self
def with_app(self, app: Starlette) -> Self:
assert app is not None, "app must not be None"
assert isinstance(app, Starlette), "app must be an instance of Starlette"
self._app = app
return self
def with_routes( def with_routes(
self, self,
routes: list[ApiRoute], routes: list[ApiRoute],
@@ -136,7 +138,7 @@ class WebApp(ApplicationABC):
def with_route( def with_route(
self, self,
path: str, path: str,
fn: Callable[[Request], Any], fn: TEndpoint,
method: HTTPMethods, method: HTTPMethods,
authentication: bool = False, authentication: bool = False,
roles: list[str | Enum] = None, roles: list[str | Enum] = None,
@@ -167,6 +169,30 @@ class WebApp(ApplicationABC):
return self return self
def with_websocket(
self,
path: str,
fn: TEndpoint,
authentication: bool = False,
roles: list[str | Enum] = None,
permissions: list[str | Enum] = None,
policies: list[str] = None,
match: ValidationMatch = None,
) -> Self:
self._check_for_app()
assert path is not None, "path must not be None"
assert fn is not None, "fn must not be None"
Router.websocket(path, registry=self._routes)(fn)
if authentication:
Router.authenticate()(fn)
if roles or permissions or policies:
Router.authorize(roles, permissions, policies, match)(fn)
return self
def with_middleware(self, middleware: PartialMiddleware) -> Self: def with_middleware(self, middleware: PartialMiddleware) -> Self:
self._check_for_app() self._check_for_app()
@@ -184,6 +210,7 @@ class WebApp(ApplicationABC):
return self return self
def with_authorization(self, *policies: list[PolicyInput] | PolicyInput) -> Self: def with_authorization(self, *policies: list[PolicyInput] | PolicyInput) -> Self:
self._check_for_app()
if policies: if policies:
_policies = [] _policies = []
@@ -211,12 +238,8 @@ class WebApp(ApplicationABC):
self.with_middleware(AuthorizationMiddleware) self.with_middleware(AuthorizationMiddleware)
return self return self
def _validate_policies(self): async def _log_before_startup(self):
for rule in Router.get_authorization_rules(): self._logger.info(f"Start API on {self._api_settings.host}:{self._api_settings.port}")
for policy_name in rule["policies"]:
policy = self._policies.get(policy_name)
if not policy:
self._logger.fatal(f"Authorization policy '{policy_name}' not found")
async def main(self): async def main(self):
self._logger.debug(f"Preparing API") self._logger.debug(f"Preparing API")
@@ -241,7 +264,7 @@ class WebApp(ApplicationABC):
else: else:
app = self._app app = self._app
self._logger.info(f"Start API on {self._api_settings.host}:{self._api_settings.port}") await self._log_before_startup()
config = uvicorn.Config( config = uvicorn.Config(
app, host=self._api_settings.host, port=self._api_settings.port, log_config=None, loop="asyncio" app, host=self._api_settings.host, port=self._api_settings.port, log_config=None, loop="asyncio"

View File

@@ -8,7 +8,7 @@ class APIError(HTTPException):
status_code = 500 status_code = 500
def __init__(self, message: str = ""): def __init__(self, message: str = ""):
super().__init__(self.status_code, message) HTTPException.__init__(self, self.status_code, message)
self._message = message self._message = message
@property @property

View File

@@ -7,13 +7,13 @@ from cpl.api.logger import APILogger
from cpl.api.middleware.request import get_request from cpl.api.middleware.request import get_request
from cpl.api.router import Router from cpl.api.router import Router
from cpl.auth.keycloak import KeycloakClient from cpl.auth.keycloak import KeycloakClient
from cpl.auth.schema import AuthUserDao, AuthUser from cpl.auth.schema import UserDao, User
from cpl.core.ctx import set_user from cpl.core.ctx import set_user
class AuthenticationMiddleware(ASGIMiddleware): class AuthenticationMiddleware(ASGIMiddleware):
def __init__(self, app, logger: APILogger, keycloak: KeycloakClient, user_dao: AuthUserDao): def __init__(self, app, logger: APILogger, keycloak: KeycloakClient, user_dao: UserDao):
ASGIMiddleware.__init__(self, app) ASGIMiddleware.__init__(self, app)
self._logger = logger self._logger = logger
@@ -25,6 +25,21 @@ class AuthenticationMiddleware(ASGIMiddleware):
request = get_request() request = get_request()
url = request.url.path url = request.url.path
if url not in Router.get_auth_required_routes():
self._logger.trace(f"No authentication required for {url}")
return await self._app(scope, receive, send)
user = getattr(request.state, "user", None)
if not user or user.deleted:
self._logger.debug(f"Unauthorized access to {url}, user missing or deleted")
return await Unauthorized("Unauthorized").asgi_response(scope, receive, send)
return await self._call_next(scope, receive, send)
async def _old_call__(self, scope: Scope, receive: Receive, send: Send):
request = get_request()
url = request.url.path
if url not in Router.get_auth_required_routes(): if url not in Router.get_auth_required_routes():
self._logger.trace(f"No authentication required for {url}") self._logger.trace(f"No authentication required for {url}")
return await self._app(scope, receive, send) return await self._app(scope, receive, send)
@@ -57,12 +72,12 @@ class AuthenticationMiddleware(ASGIMiddleware):
return await self._call_next(scope, receive, send) return await self._call_next(scope, receive, send)
async def _get_or_crate_user(self, keycloak_id: str) -> AuthUser: async def _get_or_crate_user(self, keycloak_id: str) -> User:
existing = await self._user_dao.find_by_keycloak_id(keycloak_id) existing = await self._user_dao.find_by_keycloak_id(keycloak_id)
if existing is not None: if existing is not None:
return existing return existing
user = AuthUser(0, keycloak_id) user = User(0, keycloak_id)
uid = await self._user_dao.create(user) uid = await self._user_dao.create(user)
return await self._user_dao.get_by_id(uid) return await self._user_dao.get_by_id(uid)

View File

@@ -7,13 +7,13 @@ from cpl.api.middleware.request import get_request
from cpl.api.model.validation_match import ValidationMatch from cpl.api.model.validation_match import ValidationMatch
from cpl.api.registry.policy import PolicyRegistry from cpl.api.registry.policy import PolicyRegistry
from cpl.api.router import Router from cpl.api.router import Router
from cpl.auth.schema._administration.auth_user_dao import AuthUserDao from cpl.auth.schema._administration.user_dao import UserDao
from cpl.core.ctx.user_context import get_user from cpl.core.ctx.user_context import get_user
class AuthorizationMiddleware(ASGIMiddleware): class AuthorizationMiddleware(ASGIMiddleware):
def __init__(self, app, logger: APILogger, policies: PolicyRegistry, user_dao: AuthUserDao): def __init__(self, app, logger: APILogger, policies: PolicyRegistry, user_dao: UserDao):
ASGIMiddleware.__init__(self, app) ASGIMiddleware.__init__(self, app)
self._logger = logger self._logger = logger

View File

@@ -5,10 +5,15 @@ from uuid import uuid4
from starlette.requests import Request from starlette.requests import Request
from starlette.types import Scope, Receive, Send from starlette.types import Scope, Receive, Send
from starlette.websockets import WebSocket
from cpl.api.abc.asgi_middleware_abc import ASGIMiddleware from cpl.api.abc.asgi_middleware_abc import ASGIMiddleware
from cpl.api.logger import APILogger from cpl.api.logger import APILogger
from cpl.api.typing import TRequest from cpl.api.typing import TRequest
from cpl.auth.keycloak.keycloak_client import KeycloakClient
from cpl.auth.schema import User
from cpl.auth.schema._administration.user_dao import UserDao
from cpl.core.ctx import set_user
from cpl.dependency.inject import inject from cpl.dependency.inject import inject
from cpl.dependency.service_provider import ServiceProvider from cpl.dependency.service_provider import ServiceProvider
@@ -17,19 +22,23 @@ _request_context: ContextVar[Union[TRequest, None]] = ContextVar("request", defa
class RequestMiddleware(ASGIMiddleware): class RequestMiddleware(ASGIMiddleware):
def __init__(self, app, provider: ServiceProvider, logger: APILogger): def __init__(self, app, provider: ServiceProvider, logger: APILogger, keycloak: KeycloakClient, user_dao: UserDao):
ASGIMiddleware.__init__(self, app) ASGIMiddleware.__init__(self, app)
self._provider = provider self._provider = provider
self._logger = logger self._logger = logger
self._keycloak = keycloak
self._user_dao = user_dao
self._ctx_token = None self._ctx_token = None
async def __call__(self, scope: Scope, receive: Receive, send: Send): async def __call__(self, scope: Scope, receive: Receive, send: Send):
request = Request(scope, receive, send) request = Request(scope, receive, send) if scope["type"] != "websocket" else WebSocket(scope, receive, send)
await self.set_request_data(request) await self.set_request_data(request)
try: try:
await self._try_set_user(request)
with self._provider.create_scope(): with self._provider.create_scope():
inject(await self._app(scope, receive, send)) inject(await self._app(scope, receive, send))
finally: finally:
@@ -53,6 +62,37 @@ class RequestMiddleware(ASGIMiddleware):
self._logger.trace(f"Clearing current request: {request.state.request_id}") self._logger.trace(f"Clearing current request: {request.state.request_id}")
_request_context.reset(self._ctx_token) _request_context.reset(self._ctx_token)
async def _try_set_user(self, request: Request):
auth_header = request.headers.get("Authorization")
if not auth_header or not auth_header.startswith("Bearer "):
return
token = auth_header.split("Bearer ")[1]
try:
token_info = self._keycloak.introspect(token)
if not token_info.get("active", False):
return
keycloak_id = self._keycloak.get_user_id(token)
if not keycloak_id:
return
user = await self._user_dao.find_by_keycloak_id(keycloak_id)
if not user:
user = User(0, keycloak_id)
uid = await self._user_dao.create(user)
user = await self._user_dao.get_by_id(uid)
if user.deleted:
return
request.state.user = user
set_user(user)
self._logger.trace(f"User {user.id} bound to request {request.state.request_id}")
except Exception as e:
self._logger.debug(f"Silent user binding failed: {e}")
def get_request() -> Optional[TRequest]: def get_request() -> Optional[TRequest]:
return _request_context.get() return _request_context.get()

View File

@@ -0,0 +1,31 @@
from typing import Callable
import starlette.routing
class WebSocketRoute:
def __init__(self, path: str, fn: Callable, **kwargs):
self._path = path
self._fn = fn
self._kwargs = kwargs
@property
def name(self) -> str:
return self._fn.__name__
@property
def fn(self) -> Callable:
return self._fn
@property
def path(self) -> str:
return self._path
@property
def kwargs(self) -> dict:
return self._kwargs
def to_starlette(self, *args) -> starlette.routing.WebSocketRoute:
return starlette.routing.WebSocketRoute(self._path, self._fn)

View File

@@ -1,32 +1,35 @@
from typing import Optional from typing import Optional, Union
from cpl.api.model.api_route import ApiRoute from cpl.api.model.api_route import ApiRoute
from cpl.api.model.websocket_route import WebSocketRoute
from cpl.core.abc.registry_abc import RegistryABC from cpl.core.abc.registry_abc import RegistryABC
TRoute = Union[ApiRoute, WebSocketRoute]
class RouteRegistry(RegistryABC): class RouteRegistry(RegistryABC):
def __init__(self): def __init__(self):
RegistryABC.__init__(self) RegistryABC.__init__(self)
def extend(self, items: list[ApiRoute]): def extend(self, items: list[TRoute]):
for policy in items: for policy in items:
self.add(policy) self.add(policy)
def add(self, item: ApiRoute): def add(self, item: TRoute):
assert isinstance(item, ApiRoute), "route must be an instance of ApiRoute" assert isinstance(item, (ApiRoute, WebSocketRoute)), "route must be an instance of ApiRoute"
if item.path in self._items: if item.path in self._items:
raise ValueError(f"ApiRoute {item.path} is already registered") raise ValueError(f"ApiRoute {item.path} is already registered")
self._items[item.path] = item self._items[item.path] = item
def set(self, item: ApiRoute): def set(self, item: TRoute):
assert isinstance(item, ApiRoute), "route must be an instance of ApiRoute" assert isinstance(item, ApiRoute), "route must be an instance of ApiRoute"
self._items[item.path] = item self._items[item.path] = item
def get(self, key: str) -> Optional[ApiRoute]: def get(self, key: str) -> Optional[TRoute]:
return self._items.get(key) return self._items.get(key)
def all(self) -> list[ApiRoute]: def all(self) -> list[TRoute]:
return list(self._items.values()) return list(self._items.values())

View File

@@ -91,6 +91,22 @@ class Router:
return inner return inner
@classmethod
def websocket(cls, path: str, registry: RouteRegistry = None, **kwargs):
from cpl.api.model.websocket_route import WebSocketRoute
if not registry:
routes = get_provider().get_service(RouteRegistry)
else:
routes = registry
def inner(fn):
routes.add(WebSocketRoute(path, fn, **kwargs))
setattr(fn, "_route_path", path)
return fn
return inner
@classmethod @classmethod
def route(cls, path: str, method: HTTPMethods, registry: RouteRegistry = None, **kwargs): def route(cls, path: str, method: HTTPMethods, registry: RouteRegistry = None, **kwargs):
from cpl.api.model.api_route import ApiRoute from cpl.api.model.api_route import ApiRoute

View File

@@ -6,7 +6,7 @@ from cpl.core.configuration import ConfigurationModelABC
class ApiSettings(ConfigurationModelABC): class ApiSettings(ConfigurationModelABC):
def __init__(self, src: Optional[dict] = None): def __init__(self, src: Optional[dict] = None):
super().__init__(src) ConfigurationModelABC.__init__(self, src)
self.option("host", str, "0.0.0.0") self.option("host", str, "0.0.0.0")
self.option("port", int, 5000) self.option("port", int, 5000)

View File

@@ -2,13 +2,15 @@ from typing import Union, Literal, Callable, Type, Awaitable
from urllib.request import Request from urllib.request import Request
from starlette.middleware import Middleware from starlette.middleware import Middleware
from starlette.responses import Response
from starlette.types import ASGIApp from starlette.types import ASGIApp
from starlette.websockets import WebSocket from starlette.websockets import WebSocket
from cpl.api.abc.asgi_middleware_abc import ASGIMiddleware from cpl.api.abc.asgi_middleware_abc import ASGIMiddleware
from cpl.auth.schema import AuthUser from cpl.auth.schema import User
TRequest = Union[Request, WebSocket] TRequest = Union[Request, WebSocket]
TEndpoint = Callable[[TRequest, ...], Awaitable[Response]] | Callable[[TRequest, ...], Response]
HTTPMethods = Literal["GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"] HTTPMethods = Literal["GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"]
PartialMiddleware = Union[ PartialMiddleware = Union[
ASGIMiddleware, ASGIMiddleware,
@@ -16,4 +18,5 @@ PartialMiddleware = Union[
Middleware, Middleware,
Callable[[ASGIApp], ASGIApp], Callable[[ASGIApp], ASGIApp],
] ]
PolicyResolver = Callable[[AuthUser], bool | Awaitable[bool]] PolicyResolver = Callable[[User], bool | Awaitable[bool]]
PolicyInput = Union[dict[str, PolicyResolver], "Policy"]

View File

@@ -1,2 +1,4 @@
from .application_builder import ApplicationBuilder from .application_builder import ApplicationBuilder
from .host import Host from .host import Host
__version__ = "1.0.0"

View File

@@ -2,10 +2,12 @@ from abc import ABC, abstractmethod
from typing import Callable, Self from typing import Callable, Self
from cpl.application.host import Host from cpl.application.host import Host
from cpl.core.errors import module_dependency_error
from cpl.core.log.log_level import LogLevel from cpl.core.log.log_level import LogLevel
from cpl.core.log.log_settings import LogSettings from cpl.core.log.log_settings import LogSettings
from cpl.core.log.logger_abc import LoggerABC from cpl.core.log.logger_abc import LoggerABC
from cpl.dependency.service_provider import ServiceProvider from cpl.dependency.service_provider import ServiceProvider
from cpl.dependency.typing import TModule
def __not_implemented__(package: str, func: Callable): def __not_implemented__(package: str, func: Callable):
@@ -21,31 +23,28 @@ class ApplicationABC(ABC):
""" """
@abstractmethod @abstractmethod
def __init__(self, services: ServiceProvider, required_modules: list[str | object] = None): def __init__(
self, services: ServiceProvider, loaded_modules: set[TModule], required_modules: list[str | object] = None
):
self._services = services self._services = services
self._modules = loaded_modules
self._required_modules = ( self._required_modules = (
[x.__name__ if not isinstance(x, str) else x for x in required_modules] if required_modules else [] [x.__name__ if not isinstance(x, str) else x for x in required_modules] if required_modules else []
) )
@property def validate_app_required_modules(self):
def required_modules(self) -> list[str]: modules_names = {x.__name__ for x in self._modules}
return self._required_modules for module in self._required_modules:
if module in modules_names:
continue
@classmethod module_dependency_error(
def extend(cls, name: str | Callable, func: Callable[[Self], Self]): type(self).__name__,
r"""Extend the Application with a custom method module.__name__ if not isinstance(module, str) else module,
ImportError(
Parameters: f"Required module '{module}' for application '{self.__class__.__name__}' is not loaded. Load using 'add_module({module})' method."
name: :class:`str` ),
Name of the method )
func: :class:`Callable[[Self], Self]`
Function that takes the Application as a parameter and returns it
"""
if callable(name):
name = name.__name__
setattr(cls, name, func)
return cls
def with_logging(self, level: LogLevel = None): def with_logging(self, level: LogLevel = None):
if level is None: if level is None:
@@ -57,14 +56,21 @@ class ApplicationABC(ABC):
logger = self._services.get_service(LoggerABC) logger = self._services.get_service(LoggerABC)
logger.set_level(level) logger.set_level(level)
def with_permissions(self, *args, **kwargs): def with_permissions(self, *args):
__not_implemented__("cpl-auth", self.with_permissions) try:
from cpl.auth import AuthModule
def with_migrations(self, *args, **kwargs): AuthModule.with_permissions(*args)
__not_implemented__("cpl-database", self.with_migrations) except ImportError:
__not_implemented__("cpl-auth", self.with_permissions)
def with_seeders(self, *args, **kwargs): def with_migrations(self, *args):
__not_implemented__("cpl-database", self.with_seeders) try:
from cpl.database.database_module import DatabaseModule
DatabaseModule.with_migrations(self._services, *args)
except ImportError:
__not_implemented__("cpl-database", self.with_migrations)
def with_extension(self, func: Callable[[Self, ...], None], *args, **kwargs): def with_extension(self, func: Callable[[Self, ...], None], *args, **kwargs):
r"""Extend the Application with a custom method r"""Extend the Application with a custom method
@@ -84,9 +90,17 @@ class ApplicationABC(ABC):
Called by custom Application.main Called by custom Application.main
""" """
try: try:
for module in self._modules:
if not hasattr(module, "configure") and not callable(getattr(module, "configure")):
continue
module.configure(self._services)
Host.run_app(self.main) Host.run_app(self.main)
except KeyboardInterrupt: except KeyboardInterrupt:
pass pass
finally:
logger = self._services.get_service(LoggerABC)
logger.info("Application shutdown")
@abstractmethod @abstractmethod
def main(self): ... def main(self): ...

View File

@@ -6,7 +6,6 @@ from cpl.application.abc.application_extension_abc import ApplicationExtensionAB
from cpl.application.abc.startup_abc import StartupABC from cpl.application.abc.startup_abc import StartupABC
from cpl.application.abc.startup_extension_abc import StartupExtensionABC from cpl.application.abc.startup_extension_abc import StartupExtensionABC
from cpl.application.host import Host from cpl.application.host import Host
from cpl.core.errors import dependency_error
from cpl.dependency.context import get_provider, use_root_provider from cpl.dependency.context import get_provider, use_root_provider
from cpl.dependency.service_collection import ServiceCollection from cpl.dependency.service_collection import ServiceCollection
@@ -15,7 +14,7 @@ TApp = TypeVar("TApp", bound=ApplicationABC)
class ApplicationBuilder(Generic[TApp]): class ApplicationBuilder(Generic[TApp]):
def __init__(self, app: Type[ApplicationABC]): def __init__(self, app: Type[TApp]):
assert app is not None, "app must not be None" assert app is not None, "app must not be None"
assert issubclass(app, ApplicationABC), "app must be an subclass of ApplicationABC or its subclass" assert issubclass(app, ApplicationABC), "app must be an subclass of ApplicationABC or its subclass"
@@ -43,19 +42,6 @@ class ApplicationBuilder(Generic[TApp]):
return provider return provider
def validate_app_required_modules(self, app: ApplicationABC):
for module in app.required_modules:
if module in self._services.loaded_modules:
continue
dependency_error(
type(app).__name__,
module,
ImportError(
f"Required module '{module}' for application '{app.__class__.__name__}' is not loaded. Load using 'add_module({module})' method."
),
)
def with_startup(self, startup: Type[StartupABC]) -> "ApplicationBuilder": def with_startup(self, startup: Type[StartupABC]) -> "ApplicationBuilder":
self._startup = startup self._startup = startup
return self return self
@@ -84,6 +70,6 @@ class ApplicationBuilder(Generic[TApp]):
Host.run(extension.run, self.service_provider) Host.run(extension.run, self.service_provider)
use_root_provider(self._services.build()) use_root_provider(self._services.build())
app = self._app(self.service_provider) app = self._app(self.service_provider, self._services.loaded_modules)
self.validate_app_required_modules(app) app.validate_app_required_modules()
return app return app

View File

@@ -1,14 +1,34 @@
import asyncio import asyncio
from typing import Callable from typing import Callable
from cpl.dependency import get_provider from cpl.core.property import classproperty
from cpl.dependency.hosted.startup_task import StartupTask from cpl.dependency.context import get_provider, use_root_provider
from cpl.dependency.service_collection import ServiceCollection
from cpl.core.service.startup_task import StartupTask
class Host: class Host:
_loop: asyncio.AbstractEventLoop | None = None _loop: asyncio.AbstractEventLoop | None = None
_tasks: dict = {} _tasks: dict = {}
_service_collection: ServiceCollection | None = None
@classproperty
def services(cls) -> ServiceCollection:
if cls._service_collection is None:
cls._service_collection = ServiceCollection()
return cls._service_collection
@classmethod
def get_provider(cls):
provider = get_provider()
if provider is None:
provider = cls.services.build()
use_root_provider(provider)
return provider
@classmethod @classmethod
def get_loop(cls) -> asyncio.AbstractEventLoop: def get_loop(cls) -> asyncio.AbstractEventLoop:
if cls._loop is None: if cls._loop is None:
@@ -18,7 +38,7 @@ class Host:
@classmethod @classmethod
def run_start_tasks(cls): def run_start_tasks(cls):
provider = get_provider() provider = cls.get_provider()
tasks = provider.get_services(StartupTask) tasks = provider.get_services(StartupTask)
loop = cls.get_loop() loop = cls.get_loop()
@@ -30,7 +50,7 @@ class Host:
@classmethod @classmethod
def run_hosted_services(cls): def run_hosted_services(cls):
provider = get_provider() provider = cls.get_provider()
services = provider.get_hosted_services() services = provider.get_hosted_services()
loop = cls.get_loop() loop = cls.get_loop()
@@ -49,6 +69,10 @@ class Host:
cls._tasks.clear() cls._tasks.clear()
@classmethod
async def wait_for_all(cls):
await asyncio.gather(*cls._tasks.values())
@classmethod @classmethod
def run_app(cls, func: Callable, *args, **kwargs): def run_app(cls, func: Callable, *args, **kwargs):
cls.run_start_tasks() cls.run_start_tasks()
@@ -57,20 +81,14 @@ class Host:
async def runner(): async def runner():
try: try:
if asyncio.iscoroutinefunction(func): if asyncio.iscoroutinefunction(func):
app_task = asyncio.create_task(func(*args, **kwargs)) await func(*args, **kwargs)
else: else:
app_task = cls.get_loop().run_in_executor(None, func, *args, **kwargs) func(*args, **kwargs)
await asyncio.wait(
[app_task, *cls._tasks.values()],
return_when=asyncio.FIRST_COMPLETED,
)
except (KeyboardInterrupt, asyncio.CancelledError): except (KeyboardInterrupt, asyncio.CancelledError):
pass pass
finally:
await cls._stop_all()
cls.get_loop().run_until_complete(runner()) cls.get_loop().run_until_complete(runner())
cls.get_loop().run_until_complete(cls.wait_for_all())
@classmethod @classmethod
def run(cls, func: Callable, *args, **kwargs): def run(cls, func: Callable, *args, **kwargs):

View File

@@ -0,0 +1,8 @@
from cpl.auth import permission as _permission
from cpl.auth.keycloak.keycloak_admin import KeycloakAdmin as _KeycloakAdmin
from cpl.auth.keycloak.keycloak_client import KeycloakClient as _KeycloakClient
from .auth_module import AuthModule
from .keycloak_settings import KeycloakSettings
from .logger import AuthLogger
__version__ = "1.0.0"

View File

@@ -0,0 +1,56 @@
import os
from enum import Enum
from typing import Type
from cpl.auth.keycloak_settings import KeycloakSettings
from cpl.database.database_module import DatabaseModule
from cpl.database.model.server_type import ServerType, ServerTypes
from cpl.database.mysql.mysql_module import MySQLModule
from cpl.database.postgres.postgres_module import PostgresModule
from cpl.dependency.module.module import Module
from cpl.dependency.service_provider import ServiceProvider
from .keycloak.keycloak_admin import KeycloakAdmin
from .keycloak.keycloak_client import KeycloakClient
from .schema._administration.api_key_dao import ApiKeyDao
from .schema._administration.user_dao import UserDao
from .schema._permission.api_key_permission_dao import ApiKeyPermissionDao
from .schema._permission.permission_dao import PermissionDao
from .schema._permission.role_dao import RoleDao
from .schema._permission.role_permission_dao import RolePermissionDao
from .schema._permission.role_user_dao import RoleUserDao
class AuthModule(Module):
dependencies = [DatabaseModule, (MySQLModule, PostgresModule)]
config = [KeycloakSettings]
singleton = [
KeycloakClient,
KeycloakAdmin,
UserDao,
ApiKeyDao,
ApiKeyPermissionDao,
PermissionDao,
RoleDao,
RolePermissionDao,
RoleUserDao,
]
scoped = []
transient = []
@staticmethod
def configure(provider: ServiceProvider):
paths = {
ServerTypes.POSTGRES: "scripts/postgres",
ServerTypes.MYSQL: "scripts/mysql",
}
DatabaseModule.with_migrations(
provider, str(os.path.join(os.path.dirname(os.path.realpath(__file__)), paths[ServerType.server_type]))
)
@staticmethod
def with_permissions(*permissions: Type[Enum]):
from cpl.auth.permission.permissions_registry import PermissionsRegistry
for perm in permissions:
PermissionsRegistry.with_enum(perm)

View File

@@ -2,19 +2,17 @@ from cpl.auth.auth_module import AuthModule
from cpl.auth.permission.permission_seeder import PermissionSeeder from cpl.auth.permission.permission_seeder import PermissionSeeder
from cpl.auth.permission.permissions import Permissions from cpl.auth.permission.permissions import Permissions
from cpl.auth.permission.permissions_registry import PermissionsRegistry from cpl.auth.permission.permissions_registry import PermissionsRegistry
from cpl.auth.permission.role_seeder import RoleSeeder
from cpl.database.abc.data_seeder_abc import DataSeederABC from cpl.database.abc.data_seeder_abc import DataSeederABC
from cpl.dependency.module import Module, TModule from cpl.database.database_module import DatabaseModule
from cpl.dependency.module.module import Module
from cpl.dependency.service_collection import ServiceCollection from cpl.dependency.service_collection import ServiceCollection
class PermissionsModule(Module): class PermissionsModule(Module):
@staticmethod dependencies = [DatabaseModule, AuthModule]
def dependencies() -> list[TModule]: transient = [(DataSeederABC, PermissionSeeder), (DataSeederABC, RoleSeeder)]
from cpl.database.database_module import DatabaseModule
return [DatabaseModule, AuthModule]
@staticmethod @staticmethod
def register(collection: ServiceCollection): def register(collection: ServiceCollection):
collection.add_singleton(DataSeederABC, PermissionSeeder)
PermissionsRegistry.with_enum(Permissions) PermissionsRegistry.with_enum(Permissions)

View File

@@ -1,4 +1,3 @@
from cpl.auth.permission.permissions import Permissions
from cpl.auth.permission.permissions_registry import PermissionsRegistry from cpl.auth.permission.permissions_registry import PermissionsRegistry
from cpl.auth.schema import ( from cpl.auth.schema import (
Permission, Permission,

View File

@@ -0,0 +1,60 @@
from cpl.auth.schema import (
Role,
RolePermission,
PermissionDao,
RoleDao,
RolePermissionDao,
ApiKeyDao,
ApiKeyPermissionDao,
UserDao,
RoleUserDao,
RoleUser,
)
from cpl.database.abc.data_seeder_abc import DataSeederABC
from cpl.database.logger import DBLogger
class RoleSeeder(DataSeederABC):
def __init__(
self,
logger: DBLogger,
permission_dao: PermissionDao,
role_dao: RoleDao,
role_permission_dao: RolePermissionDao,
api_key_dao: ApiKeyDao,
api_key_permission_dao: ApiKeyPermissionDao,
user_dao: UserDao,
role_user_dao: RoleUserDao,
):
DataSeederABC.__init__(self)
self._logger = logger
self._permission_dao = permission_dao
self._role_dao = role_dao
self._role_permission_dao = role_permission_dao
self._api_key_dao = api_key_dao
self._api_key_permission_dao = api_key_permission_dao
self._user_dao = user_dao
self._role_user_dao = role_user_dao
async def seed(self):
self._logger.info("Creating admin role")
roles = await self._role_dao.get_all()
if len(roles) == 0:
rid = await self._role_dao.create(Role(0, "admin", "Default admin role"))
permissions = await self._permission_dao.get_all()
await self._role_permission_dao.create_many(
[RolePermission(0, rid, permission.id) for permission in permissions]
)
role = await self._role_dao.get_by_name("admin")
if len(await role.users) > 0:
return
users = await self._user_dao.get_all()
if len(users) == 0:
return
user = users[0]
self._logger.warning(f"Assigning admin role to first user {user.id}")
await self._role_user_dao.create(RoleUser(0, role.id, user.id))

View File

@@ -1,7 +1,7 @@
from ._administration.api_key import ApiKey from ._administration.api_key import ApiKey
from ._administration.api_key_dao import ApiKeyDao from ._administration.api_key_dao import ApiKeyDao
from ._administration.auth_user import AuthUser from ._administration.user import User
from ._administration.auth_user_dao import AuthUserDao from ._administration.user_dao import UserDao
from ._permission.api_key_permission import ApiKeyPermission from ._permission.api_key_permission import ApiKeyPermission
from ._permission.api_key_permission_dao import ApiKeyPermissionDao from ._permission.api_key_permission_dao import ApiKeyPermissionDao

View File

@@ -1,6 +1,6 @@
import secrets import secrets
from datetime import datetime from datetime import datetime
from typing import Optional, Union from typing import Optional, Union, Self
from async_property import async_property from async_property import async_property
@@ -16,7 +16,7 @@ from cpl.dependency.service_provider import ServiceProvider
_logger = Logger(__name__) _logger = Logger(__name__)
class ApiKey(DbModelABC): class ApiKey(DbModelABC[Self]):
def __init__( def __init__(
self, self,
@@ -25,8 +25,8 @@ class ApiKey(DbModelABC):
key: Union[str, bytes], key: Union[str, bytes],
deleted: bool = False, deleted: bool = False,
editor_id: Optional[Id] = None, editor_id: Optional[Id] = None,
created: Optional[datetime] = None, created: datetime | None = None,
updated: Optional[datetime] = None, updated: datetime | None = None,
): ):
DbModelABC.__init__(self, id, deleted, editor_id, created, updated) DbModelABC.__init__(self, id, deleted, editor_id, created, updated)
self._identifier = identifier self._identifier = identifier

View File

@@ -1,6 +1,6 @@
import uuid import uuid
from datetime import datetime from datetime import datetime
from typing import Optional from typing import Optional, Self
from async_property import async_property from async_property import async_property
from keycloak import KeycloakGetError from keycloak import KeycloakGetError
@@ -10,18 +10,18 @@ from cpl.auth.permission.permissions import Permissions
from cpl.core.typing import SerialId from cpl.core.typing import SerialId
from cpl.database.abc import DbModelABC from cpl.database.abc import DbModelABC
from cpl.database.logger import DBLogger from cpl.database.logger import DBLogger
from cpl.dependency import ServiceProvider from cpl.dependency import get_provider
class AuthUser(DbModelABC): class User(DbModelABC[Self]):
def __init__( def __init__(
self, self,
id: SerialId, id: SerialId,
keycloak_id: str, keycloak_id: str,
deleted: bool = False, deleted: bool = False,
editor_id: Optional[SerialId] = None, editor_id: SerialId | None = None,
created: Optional[datetime] = None, created: datetime | None = None,
updated: Optional[datetime] = None, updated: datetime | None = None,
): ):
DbModelABC.__init__(self, id, deleted, editor_id, created, updated) DbModelABC.__init__(self, id, deleted, editor_id, created, updated)
self._keycloak_id = keycloak_id self._keycloak_id = keycloak_id
@@ -69,21 +69,21 @@ class AuthUser(DbModelABC):
@async_property @async_property
async def permissions(self): async def permissions(self):
from cpl.auth.schema._administration.auth_user_dao import AuthUserDao from cpl.auth.schema._administration.user_dao import UserDao
auth_user_dao: AuthUserDao = get_provider().get_service(AuthUserDao) user_dao: UserDao = get_provider().get_service(UserDao)
return await auth_user_dao.get_permissions(self.id) return await user_dao.get_permissions(self.id)
async def has_permission(self, permission: Permissions) -> bool: async def has_permission(self, permission: Permissions) -> bool:
from cpl.auth.schema._administration.auth_user_dao import AuthUserDao from cpl.auth.schema._administration.user_dao import UserDao
auth_user_dao: AuthUserDao = get_provider().get_service(AuthUserDao) user_dao: UserDao = get_provider().get_service(UserDao)
return await auth_user_dao.has_permission(self.id, permission) return await user_dao.has_permission(self.id, permission)
async def anonymize(self): async def anonymize(self):
from cpl.auth.schema._administration.auth_user_dao import AuthUserDao from cpl.auth.schema._administration.user_dao import UserDao
auth_user_dao: AuthUserDao = get_provider().get_service(AuthUserDao) user_dao: UserDao = get_provider().get_service(UserDao)
self._keycloak_id = str(uuid.UUID(int=0)) self._keycloak_id = str(uuid.UUID(int=0))
await auth_user_dao.update(self) await user_dao.update(self)

View File

@@ -1,19 +1,23 @@
from typing import Optional, Union from typing import Optional, Union
from cpl.auth.permission.permissions import Permissions from cpl.auth.permission.permissions import Permissions
from cpl.auth.schema._administration.auth_user import AuthUser from cpl.auth.schema._permission.permission_dao import PermissionDao
from cpl.auth.schema._permission.permission import Permission
from cpl.auth.schema._administration.user import User
from cpl.database import TableManager from cpl.database import TableManager
from cpl.database.abc import DbModelDaoABC from cpl.database.abc import DbModelDaoABC
from cpl.database.external_data_temp_table_builder import ExternalDataTempTableBuilder from cpl.database.external_data_temp_table_builder import ExternalDataTempTableBuilder
from cpl.dependency import ServiceProvider from cpl.dependency.context import get_provider
class AuthUserDao(DbModelDaoABC[AuthUser]): class UserDao(DbModelDaoABC[User]):
def __init__(self): def __init__(self, permission_dao: PermissionDao):
DbModelDaoABC.__init__(self, AuthUser, TableManager.get("auth_users")) DbModelDaoABC.__init__(self, User, TableManager.get("users"))
self.attribute(AuthUser.keycloak_id, str, db_name="keycloakId") self._permissions = permission_dao
self.attribute(User.keycloak_id, str)
async def get_users(): async def get_users():
return [(x.id, x.username, x.email) for x in await self.get_all()] return [(x.id, x.username, x.email) for x in await self.get_all()]
@@ -27,11 +31,11 @@ class AuthUserDao(DbModelDaoABC[AuthUser]):
.with_value_getter(get_users) .with_value_getter(get_users)
) )
async def get_by_keycloak_id(self, keycloak_id: str) -> AuthUser: async def get_by_keycloak_id(self, keycloak_id: str) -> User:
return await self.get_single_by({AuthUser.keycloak_id: keycloak_id}) return await self.get_single_by({User.keycloak_id: keycloak_id})
async def find_by_keycloak_id(self, keycloak_id: str) -> Optional[AuthUser]: async def find_by_keycloak_id(self, keycloak_id: str) -> Optional[User]:
return await self.find_single_by({AuthUser.keycloak_id: keycloak_id}) return await self.find_single_by({User.keycloak_id: keycloak_id})
async def has_permission(self, user_id: int, permission: Union[Permissions, str]) -> bool: async def has_permission(self, user_id: int, permission: Union[Permissions, str]) -> bool:
from cpl.auth.schema._permission.permission_dao import PermissionDao from cpl.auth.schema._permission.permission_dao import PermissionDao
@@ -54,7 +58,7 @@ class AuthUserDao(DbModelDaoABC[AuthUser]):
return result[0]["count"] > 0 return result[0]["count"] > 0
async def get_permissions(self, user_id: int) -> list[Permissions]: async def get_permissions(self, user_id: int) -> list[Permission]:
result = await self._db.select_map( result = await self._db.select_map(
f""" f"""
SELECT p.* SELECT p.*
@@ -66,4 +70,4 @@ class AuthUserDao(DbModelDaoABC[AuthUser]):
AND ru.deleted = FALSE; AND ru.deleted = FALSE;
""" """
) )
return [Permissions(p["name"]) for p in result] return [self._permissions.to_object(x) for x in result]

View File

@@ -15,9 +15,9 @@ class ApiKeyPermission(DbJoinModelABC):
api_key_id: SerialId, api_key_id: SerialId,
permission_id: SerialId, permission_id: SerialId,
deleted: bool = False, deleted: bool = False,
editor_id: Optional[SerialId] = None, editor_id: SerialId | None = None,
created: Optional[datetime] = None, created: datetime | None = None,
updated: Optional[datetime] = None, updated: datetime | None = None,
): ):
DbJoinModelABC.__init__(self, api_key_id, permission_id, id, deleted, editor_id, created, updated) DbJoinModelABC.__init__(self, api_key_id, permission_id, id, deleted, editor_id, created, updated)
self._api_key_id = api_key_id self._api_key_id = api_key_id

View File

@@ -1,20 +1,20 @@
from datetime import datetime from datetime import datetime
from typing import Optional from typing import Optional, Self
from cpl.core.typing import SerialId from cpl.core.typing import SerialId
from cpl.database.abc import DbModelABC from cpl.database.abc import DbModelABC
class Permission(DbModelABC): class Permission(DbModelABC[Self]):
def __init__( def __init__(
self, self,
id: SerialId, id: SerialId,
name: str, name: str,
description: str, description: str,
deleted: bool = False, deleted: bool = False,
editor_id: Optional[SerialId] = None, editor_id: SerialId | None = None,
created: Optional[datetime] = None, created: datetime | None = None,
updated: Optional[datetime] = None, updated: datetime | None = None,
): ):
DbModelABC.__init__(self, id, deleted, editor_id, created, updated) DbModelABC.__init__(self, id, deleted, editor_id, created, updated)
self._name = name self._name = name

View File

@@ -1,24 +1,24 @@
from datetime import datetime from datetime import datetime
from typing import Optional from typing import Optional, Self
from async_property import async_property from async_property import async_property
from cpl.auth.permission.permissions import Permissions from cpl.auth.permission.permissions import Permissions
from cpl.core.typing import SerialId from cpl.core.typing import SerialId
from cpl.database.abc import DbModelABC from cpl.database.abc import DbModelABC
from cpl.dependency import ServiceProvider from cpl.dependency import ServiceProvider, get_provider
class Role(DbModelABC): class Role(DbModelABC[Self]):
def __init__( def __init__(
self, self,
id: SerialId, id: SerialId,
name: str, name: str,
description: str, description: str,
deleted: bool = False, deleted: bool = False,
editor_id: Optional[SerialId] = None, editor_id: SerialId | None = None,
created: Optional[datetime] = None, created: datetime | None = None,
updated: Optional[datetime] = None, updated: datetime | None = None,
): ):
DbModelABC.__init__(self, id, deleted, editor_id, created, updated) DbModelABC.__init__(self, id, deleted, editor_id, created, updated)
self._name = name self._name = name

View File

@@ -1,46 +1,44 @@
from datetime import datetime from datetime import datetime
from typing import Optional from typing import Self
from async_property import async_property from async_property import async_property
from cpl.core.typing import SerialId from cpl.core.typing import SerialId
from cpl.database.abc import DbModelABC from cpl.database.abc import DbJoinModelABC
from cpl.dependency import ServiceProvider from cpl.dependency import get_provider
class RolePermission(DbModelABC): class RolePermission(DbJoinModelABC[Self]):
def __init__( def __init__(
self, self,
id: SerialId, id: SerialId,
role_id: SerialId, role_id: SerialId,
permission_id: SerialId, permission_id: SerialId,
deleted: bool = False, deleted: bool = False,
editor_id: Optional[SerialId] = None, editor_id: SerialId | None = None,
created: Optional[datetime] = None, created: datetime | None = None,
updated: Optional[datetime] = None, updated: datetime | None = None,
): ):
DbModelABC.__init__(self, id, deleted, editor_id, created, updated) DbJoinModelABC.__init__(self, id, role_id, permission_id, deleted, editor_id, created, updated)
self._role_id = role_id
self._permission_id = permission_id
@property @property
def role_id(self) -> int: def role_id(self) -> int:
return self._role_id return self._source_id
@async_property @async_property
async def role(self): async def role(self):
from cpl.auth.schema._permission.role_dao import RoleDao from cpl.auth.schema._permission.role_dao import RoleDao
role_dao: RoleDao = get_provider().get_service(RoleDao) role_dao: RoleDao = get_provider().get_service(RoleDao)
return await role_dao.get_by_id(self._role_id) return await role_dao.get_by_id(self._source_id)
@property @property
def permission_id(self) -> int: def permission_id(self) -> int:
return self._permission_id return self._foreign_id
@async_property @async_property
async def permission(self): async def permission(self):
from cpl.auth.schema._permission.permission_dao import PermissionDao from cpl.auth.schema._permission.permission_dao import PermissionDao
permission_dao: PermissionDao = get_provider().get_service(PermissionDao) permission_dao: PermissionDao = get_provider().get_service(PermissionDao)
return await permission_dao.get_by_id(self._permission_id) return await permission_dao.get_by_id(self._foreign_id)

View File

@@ -5,7 +5,7 @@ from async_property import async_property
from cpl.core.typing import SerialId from cpl.core.typing import SerialId
from cpl.database.abc import DbJoinModelABC from cpl.database.abc import DbJoinModelABC
from cpl.dependency import ServiceProvider from cpl.dependency import ServiceProvider, get_provider
class RoleUser(DbJoinModelABC): class RoleUser(DbJoinModelABC):
@@ -15,9 +15,9 @@ class RoleUser(DbJoinModelABC):
user_id: SerialId, user_id: SerialId,
role_id: SerialId, role_id: SerialId,
deleted: bool = False, deleted: bool = False,
editor_id: Optional[SerialId] = None, editor_id: SerialId | None = None,
created: Optional[datetime] = None, created: datetime | None = None,
updated: Optional[datetime] = None, updated: datetime | None = None,
): ):
DbJoinModelABC.__init__(self, id, user_id, role_id, deleted, editor_id, created, updated) DbJoinModelABC.__init__(self, id, user_id, role_id, deleted, editor_id, created, updated)
self._user_id = user_id self._user_id = user_id
@@ -29,10 +29,10 @@ class RoleUser(DbJoinModelABC):
@async_property @async_property
async def user(self): async def user(self):
from cpl.auth.schema._administration.auth_user_dao import AuthUserDao from cpl.auth.schema._administration.user_dao import UserDao
auth_user_dao: AuthUserDao = get_provider().get_service(AuthUserDao) user_dao: UserDao = get_provider().get_service(UserDao)
return await auth_user_dao.get_by_id(self._user_id) return await user_dao.get_by_id(self._user_id)
@property @property
def role_id(self) -> int: def role_id(self) -> int:

View File

@@ -1,4 +1,4 @@
CREATE TABLE IF NOT EXISTS administration_auth_users CREATE TABLE IF NOT EXISTS administration_users
( (
id INT AUTO_INCREMENT PRIMARY KEY, id INT AUTO_INCREMENT PRIMARY KEY,
keycloakId CHAR(36) NOT NULL, keycloakId CHAR(36) NOT NULL,
@@ -9,10 +9,10 @@ CREATE TABLE IF NOT EXISTS administration_auth_users
updated TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, updated TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
CONSTRAINT UC_KeycloakId UNIQUE (keycloakId), CONSTRAINT UC_KeycloakId UNIQUE (keycloakId),
CONSTRAINT FK_EditorId FOREIGN KEY (editorId) REFERENCES administration_auth_users (id) CONSTRAINT FK_EditorId FOREIGN KEY (editorId) REFERENCES administration_users (id)
); );
CREATE TABLE IF NOT EXISTS administration_auth_users_history CREATE TABLE IF NOT EXISTS administration_users_history
( (
id INT NOT NULL, id INT NOT NULL,
keycloakId CHAR(36) NOT NULL, keycloakId CHAR(36) NOT NULL,
@@ -23,22 +23,22 @@ CREATE TABLE IF NOT EXISTS administration_auth_users_history
updated TIMESTAMP NOT NULL updated TIMESTAMP NOT NULL
); );
CREATE TRIGGER TR_administration_auth_usersUpdate CREATE TRIGGER TR_administration_usersUpdate
AFTER UPDATE AFTER UPDATE
ON administration_auth_users ON administration_users
FOR EACH ROW FOR EACH ROW
BEGIN BEGIN
INSERT INTO administration_auth_users_history INSERT INTO administration_users_history
(id, keycloakId, deleted, editorId, created, updated) (id, keycloakId, deleted, editorId, created, updated)
VALUES (OLD.id, OLD.keycloakId, OLD.deleted, OLD.editorId, OLD.created, NOW()); VALUES (OLD.id, OLD.keycloakId, OLD.deleted, OLD.editorId, OLD.created, NOW());
END; END;
CREATE TRIGGER TR_administration_auth_usersDelete CREATE TRIGGER TR_administration_usersDelete
AFTER DELETE AFTER DELETE
ON administration_auth_users ON administration_users
FOR EACH ROW FOR EACH ROW
BEGIN BEGIN
INSERT INTO administration_auth_users_history INSERT INTO administration_users_history
(id, keycloakId, deleted, editorId, created, updated) (id, keycloakId, deleted, editorId, created, updated)
VALUES (OLD.id, OLD.keycloakId, 1, OLD.editorId, OLD.created, NOW()); VALUES (OLD.id, OLD.keycloakId, 1, OLD.editorId, OLD.created, NOW());
END; END;

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