Compare commits

..

47 Commits

Author SHA1 Message Date
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
d05d947d54 Import cleanup
All checks were successful
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 18s
Build on push / query (push) Successful in 17s
Build on push / dependency (push) Successful in 17s
Build on push / application (push) Successful in 15s
Build on push / translation (push) Successful in 18s
Build on push / mail (push) Successful in 18s
Build on push / database (push) Successful in 20s
Build on push / auth (push) Successful in 17s
Build on push / api (push) Successful in 13s
2025-09-26 00:03:12 +02:00
0529269747 Fixed formatting
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 19s
Build on push / query (push) Successful in 19s
Build on push / dependency (push) Successful in 14s
Build on push / mail (push) Successful in 16s
Build on push / application (push) Successful in 19s
Build on push / database (push) Successful in 20s
Build on push / translation (push) Successful in 20s
Build on push / auth (push) Successful in 14s
Build on push / api (push) Successful in 15s
2025-09-25 10:37:29 +02:00
e3e1703ff8 Fixed versioning
Some checks failed
Test before pr merge / test-lint (pull_request) Failing after 6s
Build on push / prepare (push) Successful in 9s
Build on push / query (push) Successful in 19s
Build on push / core (push) Successful in 19s
Build on push / dependency (push) Successful in 14s
Build on push / api (push) Has been cancelled
Build on push / auth (push) Has been cancelled
Build on push / application (push) Has been cancelled
Build on push / mail (push) Has been cancelled
Build on push / translation (push) Has been cancelled
Build on push / database (push) Has been cancelled
2025-09-25 10:36:36 +02:00
cf4aa8291f Minor DI fixes & cleanup
Some checks failed
Test before pr merge / test-lint (pull_request) Failing after 6s
Build on push / prepare (push) Successful in 9s
Build on push / core (push) Successful in 19s
Build on push / query (push) Successful in 27s
Build on push / dependency (push) Successful in 18s
Build on push / application (push) Successful in 15s
Build on push / database (push) Successful in 20s
Build on push / translation (push) Successful in 19s
Build on push / mail (push) Successful in 20s
Build on push / auth (push) Successful in 14s
Build on push / api (push) Successful in 14s
2025-09-25 10:29:40 +02:00
55a727c482 Modularization
Some checks failed
Test before pr merge / test-lint (pull_request) Failing after 7s
Build on push / prepare (push) Successful in 10s
Build on push / core (push) Successful in 18s
Build on push / query (push) Successful in 17s
Build on push / dependency (push) Successful in 17s
Build on push / application (push) Successful in 16s
Build on push / mail (push) Successful in 15s
Build on push / database (push) Successful in 15s
Build on push / translation (push) Successful in 18s
Build on push / auth (push) Successful in 23s
Build on push / api (push) Successful in 16s
2025-09-25 09:42:07 +02:00
ecb92fca3e Minor fixes 2025-09-25 08:46:02 +02:00
0ca5e5757a Added hosted services #186
Some checks failed
Test before pr merge / test-lint (pull_request) Failing after 6s
Build on push / prepare (push) Successful in 8s
Build on push / query (push) Successful in 18s
Build on push / core (push) Successful in 18s
Build on push / dependency (push) Successful in 17s
Build on push / application (push) Successful in 15s
Build on push / translation (push) Successful in 17s
Build on push / database (push) Successful in 18s
Build on push / mail (push) Successful in 24s
Build on push / auth (push) Successful in 14s
Build on push / api (push) Successful in 14s
2025-09-25 00:54:09 +02:00
75417966eb Moved general example 2025-09-25 00:15:26 +02:00
15d3c59f02 StartupTask #186 2025-09-25 00:11:26 +02:00
6a3fdb3ebd Fixed formatting #186
All checks were successful
Test before pr merge / test-lint (pull_request) Successful in 7s
Build on push / prepare (push) Successful in 9s
Build on push / core (push) Successful in 17s
Build on push / query (push) Successful in 17s
Build on push / dependency (push) Successful in 17s
Build on push / application (push) Successful in 16s
Build on push / database (push) Successful in 17s
Build on push / mail (push) Successful in 18s
Build on push / translation (push) Successful in 18s
Build on push / auth (push) Successful in 14s
Build on push / api (push) Successful in 14s
2025-09-24 21:48:57 +02:00
b49f663ae0 API scoped requests #186
Some checks failed
Test before pr merge / test-lint (pull_request) Failing after 6s
Build on push / prepare (push) Successful in 9s
Build on push / query (push) Successful in 18s
Build on push / core (push) Successful in 21s
Build on push / dependency (push) Successful in 14s
Build on push / api (push) Has been cancelled
Build on push / auth (push) Has been cancelled
Build on push / application (push) Has been cancelled
Build on push / database (push) Has been cancelled
Build on push / mail (push) Has been cancelled
Build on push / translation (push) Has been cancelled
2025-09-24 21:47:52 +02:00
287f5e3149 New implementation of scopes #186 2025-09-24 21:27:28 +02:00
4c8cd988cc Removed ServiceProviderABC #186 2025-09-24 20:53:01 +02:00
cdb4a0fb34 DI Provider ctx #186 2025-09-24 20:46:43 +02:00
cf8edafd39 Added enumerable order & added array & removed collection
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 19s
Build on push / query (push) Successful in 20s
Build on push / dependency (push) Successful in 17s
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 / database (push) Successful in 24s
Build on push / auth (push) Successful in 18s
Build on push / api (push) Successful in 14s
2025-09-24 19:41:12 +02:00
01a2ff7166 Added query@v2 2025-09-24 19:09:11 +02:00
2da6d679ad Moved test projects 2025-09-24 16:57:24 +02:00
a1cfe76047 Added cache 2025-09-24 12:04:37 +02:00
c71a3df62c More efficient wrapped logger by getting service type not service
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 / core (push) Successful in 17s
Build on push / query (push) Successful in 24s
Build on push / dependency (push) Successful in 17s
Build on push / application (push) Successful in 15s
Build on push / translation (push) Successful in 15s
Build on push / database (push) Successful in 19s
Build on push / mail (push) Successful in 19s
Build on push / auth (push) Successful in 14s
Build on push / api (push) Successful in 14s
2025-09-24 08:28:50 +02:00
e296c0992b Merge pull request 'Added structured and wrapped logger #187' (#193) from #187_structured_logging into dev
All checks were successful
Test before pr merge / test-lint (pull_request) Successful in 6s
Build on push / prepare (push) Successful in 10s
Build on push / query (push) Successful in 17s
Build on push / core (push) Successful in 18s
Build on push / dependency (push) Successful in 18s
Build on push / mail (push) Successful in 15s
Build on push / translation (push) Successful in 15s
Build on push / database (push) Successful in 19s
Build on push / application (push) Successful in 19s
Build on push / auth (push) Successful in 14s
Build on push / api (push) Successful in 17s
Reviewed-on: #193
2025-09-23 23:35:55 +02:00
6639946346 Improved wrapped logging #187
All checks were successful
Test before pr merge / test-lint (pull_request) Successful in 6s
2025-09-23 23:34:45 +02:00
b9ac11e15f Added structured and wrapped logger #187
All checks were successful
Test before pr merge / test-lint (pull_request) Successful in 5s
2025-09-22 23:24:46 +02:00
335 changed files with 6055 additions and 2419 deletions

View File

@@ -25,7 +25,11 @@ jobs:
git tag git tag
DATE=$(date +'%Y.%m.%d') DATE=$(date +'%Y.%m.%d')
TAG_COUNT=$(git tag -l "${DATE}.*" | wc -l) TAG_COUNT=$(git tag -l "${DATE}.*" | wc -l)
BUILD_NUMBER=$(($TAG_COUNT + 1)) if [ "$TAG_COUNT" -eq 0 ]; then
BUILD_NUMBER=0
else
BUILD_NUMBER=$(($TAG_COUNT + 1))
fi
VERSION_SUFFIX=${{ inputs.version_suffix }} VERSION_SUFFIX=${{ inputs.version_suffix }}
if [ -n "$VERSION_SUFFIX" ] && [ "$VERSION_SUFFIX" = "dev" ]; then if [ -n "$VERSION_SUFFIX" ] && [ "$VERSION_SUFFIX" = "dev" ]; then

3
.gitignore vendored
View File

@@ -139,3 +139,6 @@ PythonImportHelper-v2-Completion.json
# cpl unittest stuff # cpl unittest stuff
unittests/test_*_playground unittests/test_*_playground
# cpl logs
**/logs/*.jsonl

147
example/api/src/main.py Normal file
View File

@@ -0,0 +1,147 @@
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.application.application_builder import ApplicationBuilder
from cpl.auth.schema import User, Role
from cpl.core.configuration import Configuration
from cpl.core.console import Console
from cpl.core.environment import Environment
from cpl.core.utils.cache import Cache
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 service import PingService
from test_data_seeder import TestDataSeeder
def main():
builder = ApplicationBuilder[GraphQLApp](GraphQLApp)
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_host_name()}.json", optional=True)
# builder.services.add_logging()
(
builder.services.add_structured_logging()
.add_transient(PingService)
.add_module(MySQLModule)
.add_module(ApiModule)
.add_module(GraphQLModule)
.add_module(GraphQLAuthModule)
.add_scoped(ScopedService)
.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.with_logging()
app.with_migrations("./scripts")
app.with_authentication()
app.with_authorization()
app.with_route(
path="/route1",
fn=lambda r: JSONResponse("route1"),
method="GET",
# authentication=True,
# permissions=[Permissions.administrator],
)
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
user_cache = provider.get_service(Cache[User])
role_cache = provider.get_service(Cache[Role])
if role_cache == user_cache:
raise Exception("Cache service is not working")
s1 = provider.get_service(ScopedService)
s2 = provider.get_service(ScopedService)
if s1.name == s2.name:
raise Exception("Scoped service is not working")
with provider.create_scope() as scope:
s3 = scope.get_service(ScopedService)
s4 = scope.get_service(ScopedService)
if s3.name != s4.name:
raise Exception("Scoped service is not working")
if s1.name == s3.name:
raise Exception("Scoped service is not working")
Console.write_line(
s1.name,
s2.name,
s3.name,
s4.name,
)
app.run()
if __name__ == "__main__":
main()

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,21 @@
from urllib.request import Request
from service import PingService
from starlette.responses import JSONResponse
from cpl.api import APILogger
from cpl.api.router import Router
from cpl.core.console import Console
from cpl.dependency import ServiceProvider
from scoped_service import ScopedService
@Router.authenticate()
# @Router.authorize(permissions=[Permissions.administrator])
# @Router.authorize(policies=["test"])
@Router.get(f"/ping")
async def ping(r: Request, ping: PingService, logger: APILogger, provider: ServiceProvider, scoped: ScopedService):
logger.info(f"Ping: {ping}")
Console.write_line(scoped.name)
return JSONResponse(ping.ping(r))

View File

@@ -0,0 +1,14 @@
from cpl.core.console.console import Console
from cpl.core.utils.string import String
class ScopedService:
def __init__(self):
self._name = String.random(8)
@property
def name(self) -> str:
return self._name
def run(self):
Console.write_line(f"Im {self._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

@@ -3,7 +3,8 @@ from cpl.auth.keycloak import KeycloakAdmin
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.log import LoggerABC from cpl.core.log import LoggerABC
from cpl.dependency import ServiceProviderABC 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: ServiceProviderABC): 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 model.city import City
class CityDao(DbModelDaoABC[City]): class CityDao(DbModelDaoABC[City]):
def __init__(self): def __init__(self):
DbModelDaoABC.__init__(self, __name__, City, "city") DbModelDaoABC.__init__(self, City, "city")
self.attribute(City.name, str) self.attribute(City.name, str)
self.attribute(City.zip, int) self.attribute(City.zip, int)

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

@@ -5,7 +5,7 @@ from model.user import User
class UserDao(DbModelDaoABC[User]): class UserDao(DbModelDaoABC[User]):
def __init__(self): def __init__(self):
DbModelDaoABC.__init__(self, __name__, User, "users") DbModelDaoABC.__init__(self, User, "users")
self.attribute(User.name, str) self.attribute(User.name, str)
self.attribute(User.city_id, int, db_name="CityId") self.attribute(User.city_id, int, db_name="CityId")

View File

@@ -1,11 +1,14 @@
from cpl import auth from cpl import auth
from cpl.application.abc.startup_abc import StartupABC from cpl.application.abc.startup_abc import StartupABC
from cpl.auth import permission from cpl.auth import permission
from cpl.auth.auth_module import AuthModule
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.dependency import ServiceCollection from cpl.dependency import ServiceCollection
from model.city_dao import CityDao from model.city_dao import CityDao
from model.user_dao import UserDao from model.user_dao import UserDao
@@ -21,9 +24,10 @@ class Startup(StartupABC):
@staticmethod @staticmethod
async def configure_services(services: ServiceCollection): async def configure_services(services: ServiceCollection):
services.add_module(mysql) services.add_module(MySQLModule)
services.add_module(auth) services.add_module(DatabaseModule)
services.add_module(permission) services.add_module(AuthModule)
services.add_module(PermissionsModule)
services.add_transient(DataAccessObjectABC, UserDao) services.add_transient(DataAccessObjectABC, UserDao)
services.add_transient(DataAccessObjectABC, CityDao) services.add_transient(DataAccessObjectABC, CityDao)

View File

@@ -0,0 +1,45 @@
from cpl.application.abc import ApplicationABC
from cpl.core.console.console import Console
from cpl.dependency import ServiceProvider
from test_abc import TestABC
from test_service import TestService
from di_tester_service import DITesterService
from tester import Tester
class Application(ApplicationABC):
def __init__(self, services: ServiceProvider):
ApplicationABC.__init__(self, services)
def _part_of_scoped(self):
ts: TestService = self._services.get_service(TestService)
ts.run()
def main(self):
with self._services.create_scope() as scope:
Console.write_line("Scope1")
ts: TestService = scope.get_service(TestService)
ts.run()
dit: DITesterService = scope.get_service(DITesterService)
dit.run()
if ts.name != dit.name:
raise Exception("DI is broken!")
with self._services.create_scope() as scope:
Console.write_line("Scope2")
ts: TestService = scope.get_service(TestService)
ts.run()
dit: DITesterService = scope.get_service(DITesterService)
dit.run()
if ts.name != dit.name:
raise Exception("DI is broken!")
Console.write_line("Global")
self._part_of_scoped()
#from static_test import StaticTest
#StaticTest.test()
self._services.get_service(Tester)
Console.write_line(self._services.get_services(TestABC))

View File

@@ -1,11 +1,15 @@
from cpl.core.console.console import Console from cpl.core.console.console import Console
from di.test_service import TestService from test_service import TestService
class DITesterService: class DITesterService:
def __init__(self, ts: TestService): def __init__(self, ts: TestService):
self._ts = ts self._ts = ts
@property
def name(self) -> str:
return self._ts.name
def run(self): def run(self):
Console.write_line("DIT: ") Console.write_line("DIT: ")
self._ts.run() self._ts.run()

View File

@@ -1,7 +1,7 @@
from cpl.application import ApplicationBuilder from cpl.application import ApplicationBuilder
from di.application import Application from application import Application
from di.startup import Startup from startup import Startup
def main(): def main():

27
example/di/src/startup.py Normal file
View File

@@ -0,0 +1,27 @@
from cpl.application.abc import StartupABC
from cpl.dependency import ServiceProvider, ServiceCollection
from di_tester_service import DITesterService
from test1_service import Test1Service
from test2_service import Test2Service
from test_abc import TestABC
from test_service import TestService
from tester import Tester
class Startup(StartupABC):
def __init__(self):
StartupABC.__init__(self)
@staticmethod
def configure_configuration(): ...
@staticmethod
def configure_services(services: ServiceCollection) -> ServiceProvider:
services.add_scoped(TestService)
services.add_scoped(DITesterService)
services.add_singleton(TestABC, Test1Service)
services.add_singleton(TestABC, Test2Service)
services.add_singleton(Tester)
return services.build()

View File

@@ -0,0 +1,10 @@
from cpl.dependency import ServiceProvider, ServiceProvider
from cpl.dependency.inject import inject
from test_service import TestService
class StaticTest:
@staticmethod
@inject
def test(services: ServiceProvider, t1: TestService):
t1.run()

View File

@@ -1,12 +1,12 @@
import string import string
from cpl.core.console.console import Console from cpl.core.console.console import Console
from cpl.core.utils.string import String from cpl.core.utils.string import String
from di.test_abc import TestABC from test_abc import TestABC
class Test1Service(TestABC): class Test1Service(TestABC):
def __init__(self): def __init__(self):
TestABC.__init__(self, String.random_string(string.ascii_lowercase, 8)) TestABC.__init__(self, String.random(8))
def run(self): def run(self):
Console.write_line(f"Im {self._name}") Console.write_line(f"Im {self._name}")

View File

@@ -1,12 +1,12 @@
import string import string
from cpl.core.console.console import Console from cpl.core.console.console import Console
from cpl.core.utils.string import String from cpl.core.utils.string import String
from di.test_abc import TestABC from test_abc import TestABC
class Test2Service(TestABC): class Test2Service(TestABC):
def __init__(self): def __init__(self):
TestABC.__init__(self, String.random_string(string.ascii_lowercase, 8)) TestABC.__init__(self, String.random(8))
def run(self): def run(self):
Console.write_line(f"Im {self._name}") Console.write_line(f"Im {self._name}")

View File

@@ -1,5 +1,3 @@
import string
from cpl.core.console.console import Console from cpl.core.console.console import Console
from cpl.core.utils.string import String from cpl.core.utils.string import String
@@ -8,5 +6,9 @@ class TestService:
def __init__(self): def __init__(self):
self._name = String.random(8) self._name = String.random(8)
@property
def name(self) -> str:
return self._name
def run(self): def run(self):
Console.write_line(f"Im {self._name}") Console.write_line(f"Im {self._name}")

7
example/di/src/tester.py Normal file
View File

@@ -0,0 +1,7 @@
from cpl.core.console.console import Console
from test_abc import TestABC
class Tester:
def __init__(self, t1: TestABC, t2: TestABC, t3: TestABC, t: list[TestABC]):
Console.write_line("Tester:", t, t1, t2, t3)

View File

@@ -1,23 +1,25 @@
import asyncio
import time import time
from typing import Optional
from cpl.application.abc import ApplicationABC from cpl.application.abc import ApplicationABC
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.dependency import ServiceProviderABC
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.core.pipes import IPAddressPipe from cpl.core.pipes import IPAddressPipe
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.extension.list import List from cpl.query import List
from scoped_service import ScopedService
from test_service import TestService from test_service import TestService
from test_settings import TestSettings from test_settings import TestSettings
class Application(ApplicationABC): class Application(ApplicationABC):
def __init__(self, services: ServiceProviderABC): 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,10 +37,10 @@ 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(int, 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())
Console.spinner("Test", self._wait, 2, spinner_foreground_color="red") Console.spinner("Test", self._wait, 2, spinner_foreground_color="red")
test: TestService = self._services.get_service(TestService) test: TestService = self._services.get_service(TestService)
ip_pipe: IPAddressPipe = self._services.get_service(IPAddressPipe) ip_pipe: IPAddressPipe = self._services.get_service(IPAddressPipe)
@@ -48,10 +50,21 @@ class Application(ApplicationABC):
Console.write_line(f"DI working: {test == test2 and ip_pipe != ip_pipe2}") Console.write_line(f"DI working: {test == test2 and ip_pipe != ip_pipe2}")
Console.write_line(self._services.get_service(LoggerABC)) Console.write_line(self._services.get_service(LoggerABC))
scope = self._services.create_scope() root_scoped_service = self._services.get_service(ScopedService)
Console.write_line("scope", scope) with self._services.create_scope() as scope:
with self._services.create_scope() as s: s_srvc1 = scope.get_service(ScopedService)
Console.write_line("with scope", s) s_srvc2 = scope.get_service(ScopedService)
Console.write_line(root_scoped_service)
Console.write_line(s_srvc1)
Console.write_line(s_srvc2)
if root_scoped_service == s_srvc1 or s_srvc1 != s_srvc2:
raise Exception("Root scoped service should not be equal to scoped service")
root_scoped_service2 = self._services.get_service(ScopedService)
Console.write_line(root_scoped_service2)
if root_scoped_service == root_scoped_service2:
raise Exception("Root scoped service should be equal to root scoped service 2")
test_settings = Configuration.get(TestSettings) test_settings = Configuration.get(TestSettings)
Console.write_line(test_settings.value) Console.write_line(test_settings.value)
@@ -62,3 +75,9 @@ 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 < 500:
Console.write_line("Running...")
x += 1
await asyncio.sleep(5)

View File

@@ -0,0 +1,30 @@
import asyncio
from datetime import datetime
from cpl.core.console import Console
from cpl.core.time.cron import Cron
from cpl.dependency.hosted.cronjob import CronjobABC
from cpl.dependency.hosted.hosted_service import HostedService
class Hosted(HostedService):
def __init__(self):
self._stopped = False
async def start(self):
Console.write_line("Hosted Service Started")
while not self._stopped:
Console.write_line("Hosted Service Running")
await asyncio.sleep(5)
async def stop(self):
Console.write_line("Hosted Service Stopped")
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

@@ -0,0 +1,10 @@
from cpl.core.console import Console
class ScopedService:
def __init__(self):
self.value = "I am a scoped service"
Console.write_line(self.value, self)
def get_value(self):
return self.value

View File

@@ -1,9 +1,11 @@
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 hosted_service import Hosted, MyCronJob
from scoped_service import ScopedService
from test_service import TestService from test_service import TestService
@@ -18,6 +20,9 @@ class Startup(StartupABC):
@staticmethod @staticmethod
def configure_services(services: ServiceCollection): def configure_services(services: ServiceCollection):
services.add_logging() services.add_logging()
services.add_module(mail) services.add_module(MailModule)
services.add_transient(IPAddressPipe) services.add_transient(IPAddressPipe)
services.add_singleton(TestService) services.add_singleton(TestService)
services.add_scoped(ScopedService)
services.add_hosted_service(Hosted)
services.add_hosted_service(MyCronJob)

View File

@@ -1,10 +1,10 @@
from cpl.application.abc import ApplicationExtensionABC from cpl.application.abc import ApplicationExtensionABC
from cpl.core.console import Console from cpl.core.console import Console
from cpl.dependency import ServiceProviderABC from cpl.dependency import ServiceProvider
class TestExtension(ApplicationExtensionABC): class TestExtension(ApplicationExtensionABC):
@staticmethod @staticmethod
def run(services: ServiceProviderABC): def run(services: ServiceProvider):
Console.write_line("Hello World from App Extension") Console.write_line("Hello World from App Extension")

View File

@@ -1,10 +1,10 @@
from cpl.core.console.console import Console from cpl.core.console.console import Console
from cpl.dependency import ServiceProviderABC from cpl.dependency import ServiceProvider
from cpl.core.pipes.ip_address_pipe import IPAddressPipe from cpl.core.pipes.ip_address_pipe import IPAddressPipe
class TestService: class TestService:
def __init__(self, provider: ServiceProviderABC): def __init__(self, provider: ServiceProvider):
self._provider = provider self._provider = provider
def run(self): def run(self):

60
example/query/main.py Normal file
View File

@@ -0,0 +1,60 @@
from cpl.core.console import Console
from cpl.core.utils.benchmark import Benchmark
from cpl.query.enumerable import Enumerable
from cpl.query.immutable_list import ImmutableList
from cpl.query.list import List
from cpl.query.set import Set
def _default():
Console.write_line(Enumerable.empty().to_list())
Console.write_line(Enumerable.range(0, 100).length)
Console.write_line(Enumerable.range(0, 100).to_list())
Console.write_line(Enumerable.range(0, 100).where(lambda x: x % 2 == 0).length)
Console.write_line(
Enumerable.range(0, 100).where(lambda x: x % 2 == 0).to_list().select(lambda x: str(x)).to_list()
)
Console.write_line(List)
s =Enumerable.range(0, 10).to_set()
Console.write_line(s)
s.add(1)
Console.write_line(s)
data = Enumerable(
[
{"name": "Alice", "age": 30},
{"name": "Dave", "age": 35},
{"name": "Charlie", "age": 25},
{"name": "Bob", "age": 25},
]
)
Console.write_line(data.order_by(lambda x: x["age"]).to_list())
Console.write_line(data.order_by(lambda x: x["age"]).then_by(lambda x: x["name"]).to_list())
Console.write_line(data.order_by(lambda x: x["name"]).then_by(lambda x: x["age"]).to_list())
def t_benchmark(data: list):
Benchmark.all("Enumerable", lambda: Enumerable(data).where(lambda x: x % 2 == 0).select(lambda x: x * 2).to_list())
Benchmark.all("Set", lambda: Set(data).where(lambda x: x % 2 == 0).select(lambda x: x * 2).to_list())
Benchmark.all("List", lambda: List(data).where(lambda x: x % 2 == 0).select(lambda x: x * 2).to_list())
Benchmark.all(
"ImmutableList", lambda: ImmutableList(data).where(lambda x: x % 2 == 0).select(lambda x: x * 2).to_list()
)
Benchmark.all("List comprehension", lambda: [x * 2 for x in data if x % 2 == 0])
def main():
N = 1_000_000
data = list(range(N))
t_benchmark(data)
Console.write_line()
_default()
if __name__ == "__main__":
main()

View File

@@ -1,14 +1,14 @@
from cpl.application import ApplicationABC from cpl.application import ApplicationABC
from cpl.core.configuration import ConfigurationABC from cpl.core.configuration import ConfigurationABC
from cpl.core.console import Console from cpl.core.console import Console
from cpl.dependency import ServiceProviderABC from cpl.dependency import ServiceProvider
from cpl.translation.translate_pipe import TranslatePipe from cpl.translation.translate_pipe import TranslatePipe
from cpl.translation.translation_service_abc import TranslationServiceABC from cpl.translation.translation_service_abc import TranslationServiceABC
from cpl.translation.translation_settings import TranslationSettings from cpl.translation.translation_settings import TranslationSettings
class Application(ApplicationABC): class Application(ApplicationABC):
def __init__(self, config: ConfigurationABC, services: ServiceProviderABC): def __init__(self, config: ConfigurationABC, services: ServiceProvider):
ApplicationABC.__init__(self, config, services) ApplicationABC.__init__(self, config, services)
self._translate: TranslatePipe = services.get_service(TranslatePipe) self._translate: TranslatePipe = services.get_service(TranslatePipe)

View File

@@ -1,6 +1,6 @@
from cpl.application import StartupABC from cpl.application import StartupABC
from cpl.core.configuration import ConfigurationABC from cpl.core.configuration import ConfigurationABC
from cpl.dependency import ServiceProviderABC, ServiceCollection from cpl.dependency import ServiceProvider, ServiceCollection
from cpl.core.environment import Environment from cpl.core.environment import Environment
@@ -12,6 +12,6 @@ class Startup(StartupABC):
configuration.add_json_file("appsettings.json") configuration.add_json_file("appsettings.json")
return configuration return configuration
def configure_services(self, services: ServiceCollection, environment: Environment) -> ServiceProviderABC: def configure_services(self, services: ServiceCollection, environment: Environment) -> ServiceProvider:
services.add_translation() services.add_translation()
return services.build() return services.build()

View File

@@ -1,35 +1,4 @@
from cpl.dependency.service_collection import ServiceCollection as _ServiceCollection
from .error import APIError, AlreadyExists, EndpointNotImplemented, Forbidden, NotFound, Unauthorized from .error import APIError, AlreadyExists, EndpointNotImplemented, Forbidden, NotFound, Unauthorized
from .logger import APILogger from .logger import APILogger
from .settings import ApiSettings from .settings import ApiSettings
from .api_module import ApiModule
def add_api(collection: _ServiceCollection):
try:
from cpl.database import mysql
collection.add_module(mysql)
except ImportError as e:
from cpl.core.errors import dependency_error
dependency_error("cpl-database", e)
try:
from cpl import auth
from cpl.auth import permission
collection.add_module(auth)
collection.add_module(permission)
except ImportError as e:
from cpl.core.errors import dependency_error
dependency_error("cpl-auth", e)
from cpl.api.registry.policy import PolicyRegistry
from cpl.api.registry.route import RouteRegistry
collection.add_singleton(PolicyRegistry)
collection.add_singleton(RouteRegistry)
_ServiceCollection.with_module(add_api, __name__)

View File

@@ -1 +1 @@
from .asgi_middleware_abc import ASGIMiddleware from .asgi_middleware_abc import ASGIMiddleware

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 +1 @@
from .web_app import WebApp from .web_app import WebApp

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,44 +25,46 @@ 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.application.abc.application_abc import ApplicationABC from cpl.auth.auth_module import AuthModule
from cpl.core.configuration import Configuration from cpl.auth.permission.permission_module import PermissionsModule
from cpl.dependency.service_provider_abc import ServiceProviderABC from cpl.core.configuration.configuration import Configuration
from cpl.dependency.inject import inject
_logger = APILogger("API") 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: ServiceProviderABC): def __init__(self, services: ServiceProvider, modules: Modules, required_modules: list[str | object] = None):
super().__init__(services, [auth, api]) 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._api_settings = Configuration.get(ApiSettings) self._api_settings = Configuration.get(ApiSettings)
self._policies = services.get_service(PolicyRegistry) self._policies = services.get_service(PolicyRegistry)
self._routes = services.get_service(RouteRegistry) self._routes = services.get_service(RouteRegistry)
self._middleware: list[Middleware] = [ self._middleware: list[Middleware] = []
Middleware(RequestMiddleware),
Middleware(LoggingMiddleware),
]
self._exception_handlers: Mapping[Any, ExceptionHandler] = { self._exception_handlers: Mapping[Any, ExceptionHandler] = {
Exception: self._handle_exception, Exception: self._handle_exception,
APIError: self._handle_exception, APIError: self._handle_exception,
} }
@staticmethod self.with_middleware(RequestMiddleware)
async def _handle_exception(request: Request, exc: Exception): self.with_middleware(LoggingMiddleware)
async def _handle_exception(self, request: Request, exc: Exception):
if isinstance(exc, APIError): if isinstance(exc, APIError):
_logger.error(exc) self._logger.error(exc)
return JSONResponse({"error": str(exc)}, status_code=exc.status_code) return JSONResponse({"error": str(exc)}, status_code=exc.status_code)
if hasattr(request.state, "request_id"): if hasattr(request.state, "request_id"):
_logger.error(f"Request {request.state.request_id}", exc) self._logger.error(f"Request {request.state.request_id}", exc)
else: else:
_logger.error("Request unknown", exc) self._logger.error("Request unknown", exc)
return JSONResponse({"error": str(exc)}, status_code=500) return JSONResponse({"error": str(exc)}, status_code=500)
@@ -69,27 +72,23 @@ class WebApp(ApplicationABC):
origins = self._api_settings.allowed_origins origins = self._api_settings.allowed_origins
if origins is None or origins == "": if origins is None or origins == "":
_logger.warning("No allowed origins specified, allowing all origins") self._logger.warning("No allowed origins specified, allowing all origins")
return ["*"] return ["*"]
_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"
@@ -104,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],
@@ -133,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,
@@ -164,13 +169,37 @@ 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()
if isinstance(middleware, Middleware): if isinstance(middleware, Middleware):
self._middleware.append(middleware) self._middleware.append(inject(middleware))
elif callable(middleware): elif callable(middleware):
self._middleware.append(Middleware(middleware)) self._middleware.append(Middleware(inject(middleware)))
else: else:
raise ValueError("middleware must be of type starlette.middleware.Middleware or a callable") raise ValueError("middleware must be of type starlette.middleware.Middleware or a callable")
@@ -181,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 = []
@@ -191,11 +221,11 @@ class WebApp(ApplicationABC):
if isinstance(policy, dict): if isinstance(policy, dict):
for name, resolver in policy.items(): for name, resolver in policy.items():
if not isinstance(name, str): if not isinstance(name, str):
_logger.warning(f"Skipping policy at index {i}, name must be a string") self._logger.warning(f"Skipping policy at index {i}, name must be a string")
continue continue
if not callable(resolver): if not callable(resolver):
_logger.warning(f"Skipping policy {name}, resolver must be callable") self._logger.warning(f"Skipping policy {name}, resolver must be callable")
continue continue
_policies.append(Policy(name, resolver)) _policies.append(Policy(name, resolver))
@@ -203,24 +233,20 @@ class WebApp(ApplicationABC):
_policies.append(policy) _policies.append(policy)
self._policies.extend_policies(_policies) self._policies.extend(_policies)
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:
_logger.fatal(f"Authorization policy '{policy_name}' not found")
async def main(self): async def main(self):
_logger.debug(f"Preparing API") self._logger.debug(f"Preparing API")
self._validate_policies() self._validate_policies()
if self._app is None: if self._app is None:
routes = [route.to_starlette(self._services.inject) for route in self._routes.all()] routes = [route.to_starlette(inject) for route in self._routes.all()]
app = Starlette( app = Starlette(
routes=routes, routes=routes,
@@ -238,7 +264,7 @@ class WebApp(ApplicationABC):
else: else:
app = self._app app = self._app
_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"
@@ -246,4 +272,4 @@ class WebApp(ApplicationABC):
server = uvicorn.Server(config) server = uvicorn.Server(config)
await server.serve() await server.serve()
_logger.info("Shutdown API") self._logger.info("Shutdown API")

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

@@ -1,7 +1,7 @@
from cpl.core.log.logger import Logger from cpl.core.log.wrapped_logger import WrappedLogger
class APILogger(Logger): class APILogger(WrappedLogger):
def __init__(self, source: str): def __init__(self):
Logger.__init__(self, source, "api") WrappedLogger.__init__(self, "api")

View File

@@ -2,24 +2,22 @@ from keycloak import KeycloakAuthenticationError
from starlette.types import Scope, Receive, Send from starlette.types import Scope, Receive, Send
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.error import Unauthorized from cpl.api.error import Unauthorized
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
from cpl.dependency import ServiceProviderABC
_logger = APILogger(__name__)
class AuthenticationMiddleware(ASGIMiddleware): class AuthenticationMiddleware(ASGIMiddleware):
@ServiceProviderABC.inject def __init__(self, app, logger: APILogger, keycloak: KeycloakClient, user_dao: UserDao):
def __init__(self, app, keycloak: KeycloakClient, user_dao: AuthUserDao):
ASGIMiddleware.__init__(self, app) ASGIMiddleware.__init__(self, app)
self._logger = logger
self._keycloak = keycloak self._keycloak = keycloak
self._user_dao = user_dao self._user_dao = user_dao
@@ -28,11 +26,26 @@ class AuthenticationMiddleware(ASGIMiddleware):
url = request.url.path url = request.url.path
if url not in Router.get_auth_required_routes(): if url not in Router.get_auth_required_routes():
_logger.trace(f"No authentication required for {url}") 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():
self._logger.trace(f"No authentication required for {url}")
return await self._app(scope, receive, send) return await self._app(scope, receive, send)
if not request.headers.get("Authorization"): if not request.headers.get("Authorization"):
_logger.debug(f"Unauthorized access to {url}, missing Authorization header") self._logger.debug(f"Unauthorized access to {url}, missing Authorization header")
return await Unauthorized(f"Missing header Authorization").asgi_response(scope, receive, send) return await Unauthorized(f"Missing header Authorization").asgi_response(scope, receive, send)
auth_header = request.headers.get("Authorization", None) auth_header = request.headers.get("Authorization", None)
@@ -41,7 +54,7 @@ class AuthenticationMiddleware(ASGIMiddleware):
token = auth_header.split("Bearer ")[1] token = auth_header.split("Bearer ")[1]
if not await self._verify_login(token): if not await self._verify_login(token):
_logger.debug(f"Unauthorized access to {url}, invalid token") self._logger.debug(f"Unauthorized access to {url}, invalid token")
return await Unauthorized("Invalid token").asgi_response(scope, receive, send) return await Unauthorized("Invalid token").asgi_response(scope, receive, send)
# check user exists in db, if not create # check user exists in db, if not create
@@ -51,7 +64,7 @@ class AuthenticationMiddleware(ASGIMiddleware):
user = await self._get_or_crate_user(keycloak_id) user = await self._get_or_crate_user(keycloak_id)
if user.deleted: if user.deleted:
_logger.debug(f"Unauthorized access to {url}, user is deleted") self._logger.debug(f"Unauthorized access to {url}, user is deleted")
return await Unauthorized("User is deleted").asgi_response(scope, receive, send) return await Unauthorized("User is deleted").asgi_response(scope, receive, send)
request.state.user = user request.state.user = user
@@ -59,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)
@@ -73,8 +86,8 @@ class AuthenticationMiddleware(ASGIMiddleware):
token_info = self._keycloak.introspect(token) token_info = self._keycloak.introspect(token)
return token_info.get("active", False) return token_info.get("active", False)
except KeycloakAuthenticationError as e: except KeycloakAuthenticationError as e:
_logger.debug(f"Keycloak authentication error: {e}") self._logger.debug(f"Keycloak authentication error: {e}")
return False return False
except Exception as e: except Exception as e:
_logger.error(f"Unexpected error during token verification: {e}") self._logger.error(f"Unexpected error during token verification: {e}")
return False return False

View File

@@ -7,19 +7,17 @@ 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
from cpl.dependency.service_provider_abc import ServiceProviderABC
_logger = APILogger(__name__)
class AuthorizationMiddleware(ASGIMiddleware): class AuthorizationMiddleware(ASGIMiddleware):
@ServiceProviderABC.inject def __init__(self, app, logger: APILogger, policies: PolicyRegistry, user_dao: UserDao):
def __init__(self, app, policies: PolicyRegistry, user_dao: AuthUserDao):
ASGIMiddleware.__init__(self, app) ASGIMiddleware.__init__(self, app)
self._logger = logger
self._policies = policies self._policies = policies
self._user_dao = user_dao self._user_dao = user_dao
@@ -28,7 +26,7 @@ class AuthorizationMiddleware(ASGIMiddleware):
url = request.url.path url = request.url.path
if url not in Router.get_authorization_rules_paths(): if url not in Router.get_authorization_rules_paths():
_logger.trace(f"No authorization required for {url}") self._logger.trace(f"No authorization required for {url}")
return await self._app(scope, receive, send) return await self._app(scope, receive, send)
user = get_user() user = get_user()
@@ -64,7 +62,7 @@ class AuthorizationMiddleware(ASGIMiddleware):
for policy_name in rule["policies"]: for policy_name in rule["policies"]:
policy = self._policies.get(policy_name) policy = self._policies.get(policy_name)
if not policy: if not policy:
_logger.warning(f"Authorization policy '{policy_name}' not found") self._logger.warning(f"Authorization policy '{policy_name}' not found")
continue continue
if not await policy.resolve(user): if not await policy.resolve(user):

View File

@@ -7,14 +7,14 @@ from cpl.api.abc.asgi_middleware_abc import ASGIMiddleware
from cpl.api.logger import APILogger from cpl.api.logger import APILogger
from cpl.api.middleware.request import get_request from cpl.api.middleware.request import get_request
_logger = APILogger(__name__)
class LoggingMiddleware(ASGIMiddleware): class LoggingMiddleware(ASGIMiddleware):
def __init__(self, app): def __init__(self, app, logger: APILogger):
ASGIMiddleware.__init__(self, app) ASGIMiddleware.__init__(self, app)
self._logger = logger
async def __call__(self, scope: Scope, receive: Receive, send: Send): async def __call__(self, scope: Scope, receive: Receive, send: Send):
if scope["type"] != "http": if scope["type"] != "http":
await self._call_next(scope, receive, send) await self._call_next(scope, receive, send)
@@ -53,9 +53,8 @@ class LoggingMiddleware(ASGIMiddleware):
} }
return {key: value for key, value in headers.items() if key in relevant_keys} return {key: value for key, value in headers.items() if key in relevant_keys}
@classmethod async def _log_request(self, request: Request):
async def _log_request(cls, request: Request): self._logger.debug(
_logger.debug(
f"Request {getattr(request.state, 'request_id', '-')}: {request.method}@{request.url.path} from {request.client.host}" f"Request {getattr(request.state, 'request_id', '-')}: {request.method}@{request.url.path} from {request.client.host}"
) )
@@ -64,7 +63,7 @@ class LoggingMiddleware(ASGIMiddleware):
user = get_user() user = get_user()
request_info = { request_info = {
"headers": cls._filter_relevant_headers(dict(request.headers)), "headers": self._filter_relevant_headers(dict(request.headers)),
"args": dict(request.query_params), "args": dict(request.query_params),
"form-data": ( "form-data": (
await request.form() await request.form()
@@ -78,10 +77,9 @@ class LoggingMiddleware(ASGIMiddleware):
), ),
} }
_logger.trace(f"Request {getattr(request.state, 'request_id', '-')}: {request_info}") self._logger.trace(f"Request {getattr(request.state, 'request_id', '-')}: {request_info}")
@staticmethod async def _log_after_request(self, request: Request, status_code: int, duration: float):
async def _log_after_request(request: Request, status_code: int, duration: float): self._logger.info(
_logger.info(
f"Request finished {getattr(request.state, 'request_id', '-')}: {status_code}-{request.method}@{request.url.path} from {request.client.host} in {duration:.2f}ms" f"Request finished {getattr(request.state, 'request_id', '-')}: {status_code}-{request.method}@{request.url.path} from {request.client.host} in {duration:.2f}ms"
) )

View File

@@ -5,35 +5,49 @@ 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.service_provider import ServiceProvider
_request_context: ContextVar[Union[TRequest, None]] = ContextVar("request", default=None) _request_context: ContextVar[Union[TRequest, None]] = ContextVar("request", default=None)
_logger = APILogger(__name__)
class RequestMiddleware(ASGIMiddleware): class RequestMiddleware(ASGIMiddleware):
def __init__(self, app): 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._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._app(scope, receive, send) await self._try_set_user(request)
with self._provider.create_scope():
inject(await self._app(scope, receive, send))
finally: finally:
await self.clean_request_data() await self.clean_request_data()
async def set_request_data(self, request: TRequest): async def set_request_data(self, request: TRequest):
request.state.request_id = uuid4() request.state.request_id = uuid4()
request.state.start_time = time.time() request.state.start_time = time.time()
_logger.trace(f"Set new current request: {request.state.request_id}") self._logger.trace(f"Set new current request: {request.state.request_id}")
self._ctx_token = _request_context.set(request) self._ctx_token = _request_context.set(request)
@@ -45,9 +59,40 @@ class RequestMiddleware(ASGIMiddleware):
if self._ctx_token is None: if self._ctx_token is None:
return return
_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

@@ -1,3 +1,3 @@
from .api_route import ApiRoute from .api_route import ApiRoute
from .policy import Policy from .policy import Policy
from .validation_match import ValidationMatch from .validation_match import ValidationMatch

View File

@@ -1,5 +1,5 @@
from asyncio import iscoroutinefunction from asyncio import iscoroutinefunction
from typing import Optional, Any, Coroutine, Awaitable from typing import Optional
from cpl.api.typing import PolicyResolver from cpl.api.typing import PolicyResolver
from cpl.core.ctx import get_user from cpl.core.ctx import get_user

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