diff --git a/example/api/src/main.py b/example/api/src/main.py
index 4732035b..04266833 100644
--- a/example/api/src/main.py
+++ b/example/api/src/main.py
@@ -3,7 +3,7 @@ from starlette.responses import JSONResponse
from cpl.api.api_module import ApiModule
from cpl.api.application.web_app import WebApp
from cpl.application.application_builder import ApplicationBuilder
-from cpl.auth import AuthModule
+from cpl.graphql.application.graphql_app import GraphQLApp
from cpl.auth.permission.permissions import Permissions
from cpl.auth.schema import AuthUser, Role
from cpl.core.configuration import Configuration
@@ -11,12 +11,15 @@ 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.graphql_module import GraphQLModule
+from cpl.graphql.schema.root_query import RootQuery
+from queries.hello import HelloQuery
from scoped_service import ScopedService
from service import PingService
def main():
- builder = ApplicationBuilder[WebApp](WebApp)
+ builder = ApplicationBuilder[GraphQLApp](GraphQLApp)
Configuration.add_json_file(f"appsettings.json")
Configuration.add_json_file(f"appsettings.{Environment.get_environment()}.json")
@@ -27,12 +30,15 @@ def main():
builder.services.add_transient(PingService)
builder.services.add_module(MySQLModule)
builder.services.add_module(ApiModule)
+ builder.services.add_module(GraphQLModule)
builder.services.add_scoped(ScopedService)
builder.services.add_cache(AuthUser)
builder.services.add_cache(Role)
+ builder.services.add_transient(HelloQuery)
+
app = builder.build()
app.with_logging()
@@ -48,6 +54,13 @@ def main():
)
app.with_routes_directory("routes")
+ schema = app.with_graphql()
+ schema.query.string_field("ping", resolver=lambda *_: "pong")
+ schema.query.with_query("hello", HelloQuery)
+
+ app.with_playground()
+ app.with_graphiql()
+
provider = builder.service_provider
user_cache = provider.get_service(Cache[AuthUser])
role_cache = provider.get_service(Cache[Role])
diff --git a/example/api/src/queries/__init__.py b/example/api/src/queries/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/example/api/src/queries/hello.py b/example/api/src/queries/hello.py
new file mode 100644
index 00000000..eb512dbb
--- /dev/null
+++ b/example/api/src/queries/hello.py
@@ -0,0 +1,13 @@
+import graphene
+
+from cpl.graphql.schema.query import Query
+
+
+class HelloQuery(Query):
+ def __init__(self):
+ Query.__init__(self)
+ self.string_field(
+ "message",
+ args={"name": graphene.String(default_value="world")},
+ resolver=lambda *_, name: f"Hello {name}",
+ )
diff --git a/src/cpl-api/cpl/api/abc/web_app_abc.py b/src/cpl-api/cpl/api/abc/web_app_abc.py
new file mode 100644
index 00000000..fa7eec6e
--- /dev/null
+++ b/src/cpl-api/cpl/api/abc/web_app_abc.py
@@ -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: ...
diff --git a/src/cpl-api/cpl/api/application/web_app.py b/src/cpl-api/cpl/api/application/web_app.py
index 476e54d2..deeb2710 100644
--- a/src/cpl-api/cpl/api/application/web_app.py
+++ b/src/cpl-api/cpl/api/application/web_app.py
@@ -1,6 +1,6 @@
import os
from enum import Enum
-from typing import Mapping, Any, Callable, Self, Union
+from typing import Mapping, Any, Self
import uvicorn
from starlette.applications import Starlette
@@ -10,6 +10,7 @@ from starlette.requests import Request
from starlette.responses import JSONResponse
from starlette.types import ExceptionHandler
+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.logger import APILogger
@@ -24,8 +25,7 @@ from cpl.api.registry.policy import PolicyRegistry
from cpl.api.registry.route import RouteRegistry
from cpl.api.router import Router
from cpl.api.settings import ApiSettings
-from cpl.api.typing import HTTPMethods, PartialMiddleware, PolicyResolver
-from cpl.application.abc.application_abc import ApplicationABC
+from cpl.api.typing import HTTPMethods, PartialMiddleware, TEndpoint, PolicyInput
from cpl.auth.auth_module import AuthModule
from cpl.auth.permission.permission_module import PermissionsModule
from cpl.core.configuration.configuration import Configuration
@@ -33,12 +33,10 @@ from cpl.dependency.inject import inject
from cpl.dependency.service_provider import ServiceProvider
from cpl.dependency.typing import Modules
-PolicyInput = Union[dict[str, PolicyResolver], Policy]
-
-class WebApp(ApplicationABC):
- def __init__(self, services: ServiceProvider, modules: Modules):
- super().__init__(services, modules, [AuthModule, PermissionsModule, ApiModule])
+class WebApp(WebAppABC):
+ def __init__(self, services: ServiceProvider, modules: Modules, required_modules: list[str | object] = None):
+ WebAppABC.__init__(self, services, modules, [AuthModule, PermissionsModule, ApiModule] + (required_modules or []))
self._app: Starlette | None = None
self._logger = services.get_service(APILogger)
@@ -78,16 +76,17 @@ class WebApp(ApplicationABC):
self._logger.debug(f"Allowed origins: {origins}")
return origins.split(",")
- 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):
if self._app is not None:
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:
self._check_for_app()
assert directory is not None, "directory must not be None"
@@ -102,6 +101,12 @@ class WebApp(ApplicationABC):
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(
self,
routes: list[ApiRoute],
@@ -131,7 +136,7 @@ class WebApp(ApplicationABC):
def with_route(
self,
path: str,
- fn: Callable[[Request], Any],
+ fn: TEndpoint,
method: HTTPMethods,
authentication: bool = False,
roles: list[str | Enum] = None,
@@ -179,6 +184,7 @@ class WebApp(ApplicationABC):
return self
def with_authorization(self, *policies: list[PolicyInput] | PolicyInput) -> Self:
+ self._check_for_app()
if policies:
_policies = []
@@ -206,13 +212,6 @@ class WebApp(ApplicationABC):
self.with_middleware(AuthorizationMiddleware)
return self
- 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")
-
async def main(self):
self._logger.debug(f"Preparing API")
self._validate_policies()
diff --git a/src/cpl-api/cpl/api/error.py b/src/cpl-api/cpl/api/error.py
index 50329e98..8fad7e5e 100644
--- a/src/cpl-api/cpl/api/error.py
+++ b/src/cpl-api/cpl/api/error.py
@@ -8,7 +8,7 @@ class APIError(HTTPException):
status_code = 500
def __init__(self, message: str = ""):
- super().__init__(self.status_code, message)
+ HTTPException.__init__(self, self.status_code, message)
self._message = message
@property
diff --git a/src/cpl-api/cpl/api/settings.py b/src/cpl-api/cpl/api/settings.py
index 2f11f5d7..900c2dd2 100644
--- a/src/cpl-api/cpl/api/settings.py
+++ b/src/cpl-api/cpl/api/settings.py
@@ -6,7 +6,7 @@ from cpl.core.configuration import ConfigurationModelABC
class ApiSettings(ConfigurationModelABC):
def __init__(self, src: Optional[dict] = None):
- super().__init__(src)
+ ConfigurationModelABC.__init__(self, src)
self.option("host", str, "0.0.0.0")
self.option("port", int, 5000)
diff --git a/src/cpl-api/cpl/api/typing.py b/src/cpl-api/cpl/api/typing.py
index c8319900..a62d4927 100644
--- a/src/cpl-api/cpl/api/typing.py
+++ b/src/cpl-api/cpl/api/typing.py
@@ -2,6 +2,7 @@ from typing import Union, Literal, Callable, Type, Awaitable
from urllib.request import Request
from starlette.middleware import Middleware
+from starlette.responses import Response
from starlette.types import ASGIApp
from starlette.websockets import WebSocket
@@ -9,6 +10,7 @@ from cpl.api.abc.asgi_middleware_abc import ASGIMiddleware
from cpl.auth.schema import AuthUser
TRequest = Union[Request, WebSocket]
+TEndpoint = Callable[[TRequest, ...], Awaitable[Response]] | Callable[[TRequest, ...], Response]
HTTPMethods = Literal["GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"]
PartialMiddleware = Union[
ASGIMiddleware,
@@ -17,3 +19,4 @@ PartialMiddleware = Union[
Callable[[ASGIApp], ASGIApp],
]
PolicyResolver = Callable[[AuthUser], bool | Awaitable[bool]]
+PolicyInput = Union[dict[str, PolicyResolver], "Policy"]
diff --git a/src/cpl-application/cpl/application/abc/application_abc.py b/src/cpl-application/cpl/application/abc/application_abc.py
index 59c43b88..a90db406 100644
--- a/src/cpl-application/cpl/application/abc/application_abc.py
+++ b/src/cpl-application/cpl/application/abc/application_abc.py
@@ -56,7 +56,7 @@ class ApplicationABC(ABC):
module_dependency_error(
type(self).__name__,
- module.__name__,
+ module.__name__ if not isinstance(module, str) else module,
ImportError(
f"Required module '{module}' for application '{self.__class__.__name__}' is not loaded. Load using 'add_module({module})' method."
),
diff --git a/src/cpl-auth/cpl/auth/schema/_administration/auth_user.py b/src/cpl-auth/cpl/auth/schema/_administration/auth_user.py
index cae14f97..5409e468 100644
--- a/src/cpl-auth/cpl/auth/schema/_administration/auth_user.py
+++ b/src/cpl-auth/cpl/auth/schema/_administration/auth_user.py
@@ -10,7 +10,7 @@ from cpl.auth.permission.permissions import Permissions
from cpl.core.typing import SerialId
from cpl.database.abc import DbModelABC
from cpl.database.logger import DBLogger
-from cpl.dependency import ServiceProvider
+from cpl.dependency import get_provider
class AuthUser(DbModelABC):
@@ -87,3 +87,4 @@ class AuthUser(DbModelABC):
self._keycloak_id = str(uuid.UUID(int=0))
await auth_user_dao.update(self)
+
diff --git a/src/cpl-dependency/cpl/dependency/module/module_abc.py b/src/cpl-dependency/cpl/dependency/module/module_abc.py
index 971a721c..9cf0c9f8 100644
--- a/src/cpl-dependency/cpl/dependency/module/module_abc.py
+++ b/src/cpl-dependency/cpl/dependency/module/module_abc.py
@@ -8,7 +8,7 @@ class ModuleABC(ABC):
__OPTIONAL_VARS = ["dependencies", "configuration", "singleton", "scoped", "transient", "hosted"]
def __init_subclass__(cls):
- super().__init_subclass__()
+ ABC.__init_subclass__()
if f"{cls.__module__}.{cls.__name__}" == "cpl.dependency.module.module.Module":
return
diff --git a/src/cpl-graphql/cpl/graphql/__init__.py b/src/cpl-graphql/cpl/graphql/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/src/cpl-graphql/cpl/graphql/_endpoints/__init__.py b/src/cpl-graphql/cpl/graphql/_endpoints/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/src/cpl-graphql/cpl/graphql/_endpoints/graphiql.py b/src/cpl-graphql/cpl/graphql/_endpoints/graphiql.py
new file mode 100644
index 00000000..2aedb538
--- /dev/null
+++ b/src/cpl-graphql/cpl/graphql/_endpoints/graphiql.py
@@ -0,0 +1,37 @@
+from starlette.responses import HTMLResponse
+
+async def graphiql_endpoint(request):
+ return HTMLResponse("""
+
+
+
+
+ GraphiQL
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ """)
diff --git a/src/cpl-graphql/cpl/graphql/_endpoints/graphql.py b/src/cpl-graphql/cpl/graphql/_endpoints/graphql.py
new file mode 100644
index 00000000..0808d704
--- /dev/null
+++ b/src/cpl-graphql/cpl/graphql/_endpoints/graphql.py
@@ -0,0 +1,13 @@
+from starlette.requests import Request
+from starlette.responses import Response, JSONResponse
+
+from cpl.graphql.service.service import GraphQLService
+
+
+async def graphql_endpoint(request: Request, service: GraphQLService) -> Response:
+ body = await request.json()
+ query = body.get("query")
+ variables = body.get("variables")
+
+ response_data = await service.execute(query, variables, request)
+ return JSONResponse(response_data)
diff --git a/src/cpl-graphql/cpl/graphql/_endpoints/playground.py b/src/cpl-graphql/cpl/graphql/_endpoints/playground.py
new file mode 100644
index 00000000..68e59fdf
--- /dev/null
+++ b/src/cpl-graphql/cpl/graphql/_endpoints/playground.py
@@ -0,0 +1,27 @@
+from starlette.requests import Request
+from starlette.responses import Response, HTMLResponse
+
+
+async def playground_endpoint(request: Request) -> Response:
+ return HTMLResponse("""
+
+
+
+
+ GraphQL Playground
+
+
+
+
+
+
+
+
+
+ """)
diff --git a/src/cpl-graphql/cpl/graphql/abc/__init__.py b/src/cpl-graphql/cpl/graphql/abc/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/src/cpl-graphql/cpl/graphql/abc/query_base.py b/src/cpl-graphql/cpl/graphql/abc/query_base.py
new file mode 100644
index 00000000..b0b47424
--- /dev/null
+++ b/src/cpl-graphql/cpl/graphql/abc/query_base.py
@@ -0,0 +1,54 @@
+from typing import Callable, Any, Type
+
+from graphene import ObjectType
+
+
+class QueryBase(ObjectType):
+
+ def __init__(self):
+ from cpl.graphql.schema.field import Field
+
+ ObjectType.__init__(self)
+ self._fields: dict[str, Field] = {}
+
+ def get_fields(self) -> dict[str, "Field"]:
+ return self._fields
+
+ def field(
+ self,
+ name: str,
+ t: type,
+ args: dict[str, Any] | None = None,
+ resolver: Callable | None = None,
+ ):
+ gql_type_map: dict[object, str] = {
+ str: "String",
+ int: "Int",
+ float: "Float",
+ bool: "Boolean",
+ }
+
+ if t not in gql_type_map:
+ raise ValueError(f"Unsupported field type: {t}")
+
+ from cpl.graphql.schema.field import Field
+
+ self._fields[name] = Field(name, "String", resolver, args)
+
+ def with_query(self, name: str, subquery: Type["QueryBase"]):
+ from cpl.graphql.schema.field import Field
+
+ f = Field(name=name, gql_type="Object", resolver=lambda root, info, **kwargs: {}, subquery=subquery)
+ self._fields[name] = f
+
+ def string_field(self, name: str, args: dict[str, Any] | None = None, resolver: Callable | None = None):
+ self.field(name, str, args, resolver)
+
+ def int_field(self, name: str, args: dict[str, Any] | None = None, resolver: Callable | None = None):
+ self.field(name, int, args, resolver)
+
+ def float_field(self, name: str, args: dict[str, Any] | None = None, resolver: Callable | None = None):
+ self.field(name, float, args, resolver)
+
+ def bool_field(self, name: str, args: dict[str, Any] | None = None, resolver: Callable | None = None):
+ self.field(name, bool, args, resolver)
diff --git a/src/cpl-graphql/cpl/graphql/application/__init__.py b/src/cpl-graphql/cpl/graphql/application/__init__.py
new file mode 100644
index 00000000..96b2346c
--- /dev/null
+++ b/src/cpl-graphql/cpl/graphql/application/__init__.py
@@ -0,0 +1 @@
+from .graphql_app import WebApp
diff --git a/src/cpl-graphql/cpl/graphql/application/graphql_app.py b/src/cpl-graphql/cpl/graphql/application/graphql_app.py
new file mode 100644
index 00000000..ad4b06f0
--- /dev/null
+++ b/src/cpl-graphql/cpl/graphql/application/graphql_app.py
@@ -0,0 +1,80 @@
+from enum import Enum
+from typing import Self
+
+from cpl.api.application import WebApp
+from cpl.api.model.validation_match import ValidationMatch
+from cpl.dependency.service_provider import ServiceProvider
+from cpl.dependency.typing import Modules
+from .._endpoints.graphiql import graphiql_endpoint
+from .._endpoints.graphql import graphql_endpoint
+from .._endpoints.playground import playground_endpoint
+from ..graphql_module import GraphQLModule
+from ..service.schema import Schema
+
+
+class GraphQLApp(WebApp):
+ def __init__(self, services: ServiceProvider, modules: Modules):
+ WebApp.__init__(self, services, modules, [GraphQLModule])
+
+ def with_graphql(
+ self,
+ authentication: bool = False,
+ roles: list[str | Enum] = None,
+ permissions: list[str | Enum] = None,
+ policies: list[str] = None,
+ match: ValidationMatch = None,
+ ) -> Schema:
+ self.with_route(
+ path="/api/graphql",
+ fn=graphql_endpoint,
+ method="POST",
+ authentication=authentication,
+ roles=roles,
+ permissions=permissions,
+ policies=policies,
+ match=match,
+ )
+ schema = self._services.get_service(Schema)
+ if schema is None:
+ self._logger.fatal("Could not resolve RootQuery. Make sure GraphQLModule is registered.")
+ return schema
+
+ def with_graphiql(
+ self,
+ authentication: bool = False,
+ roles: list[str | Enum] = None,
+ permissions: list[str | Enum] = None,
+ policies: list[str] = None,
+ match: ValidationMatch = None,
+ ) -> Self:
+ self.with_route(
+ path="/api/graphiql",
+ fn=graphiql_endpoint,
+ method="GET",
+ authentication=authentication,
+ roles=roles,
+ permissions=permissions,
+ policies=policies,
+ match=match,
+ )
+ return self
+
+ def with_playground(
+ self,
+ authentication: bool = False,
+ roles: list[str | Enum] = None,
+ permissions: list[str | Enum] = None,
+ policies: list[str] = None,
+ match: ValidationMatch = None,
+ ) -> Self:
+ self.with_route(
+ path="/api/playground",
+ fn=playground_endpoint,
+ method="GET",
+ authentication=authentication,
+ roles=roles,
+ permissions=permissions,
+ policies=policies,
+ match=match,
+ )
+ return self
diff --git a/src/cpl-graphql/cpl/graphql/graphql_module.py b/src/cpl-graphql/cpl/graphql/graphql_module.py
new file mode 100644
index 00000000..e4cd635c
--- /dev/null
+++ b/src/cpl-graphql/cpl/graphql/graphql_module.py
@@ -0,0 +1,17 @@
+from cpl.api import ApiModule
+from cpl.dependency.module.module import Module
+from cpl.dependency.service_provider import ServiceProvider
+from cpl.graphql.schema.root_query import RootQuery
+from cpl.graphql.service.schema import Schema
+from cpl.graphql.service.service import GraphQLService
+
+
+class GraphQLModule(Module):
+ dependencies = [ApiModule]
+ singleton = [Schema, RootQuery]
+ scoped = [GraphQLService]
+
+ @staticmethod
+ def configure(services: ServiceProvider) -> None:
+ schema = services.get_service(Schema)
+ schema.build()
diff --git a/src/cpl-graphql/cpl/graphql/schema/__init__.py b/src/cpl-graphql/cpl/graphql/schema/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/src/cpl-graphql/cpl/graphql/schema/field.py b/src/cpl-graphql/cpl/graphql/schema/field.py
new file mode 100644
index 00000000..c273b3f3
--- /dev/null
+++ b/src/cpl-graphql/cpl/graphql/schema/field.py
@@ -0,0 +1,30 @@
+from cpl.graphql.abc.query_base import QueryBase
+
+
+class Field:
+ def __init__(self, name: str, gql_type: str, resolver: callable, args: dict | None = None, subquery: QueryBase | None = None):
+ self._name = name
+ self._gql_type = gql_type
+ self._resolver = resolver
+ self._args = args or {}
+ self._subquery: QueryBase | None = subquery
+
+ @property
+ def name(self) -> str:
+ return self._name
+
+ @property
+ def type(self) -> str:
+ return self._gql_type
+
+ @property
+ def resolver(self) -> callable:
+ return self._resolver
+
+ @property
+ def args(self) -> dict:
+ return self._args
+
+ @property
+ def subquery(self) -> QueryBase | None:
+ return self._subquery
\ No newline at end of file
diff --git a/src/cpl-graphql/cpl/graphql/schema/query.py b/src/cpl-graphql/cpl/graphql/schema/query.py
new file mode 100644
index 00000000..32ef46d2
--- /dev/null
+++ b/src/cpl-graphql/cpl/graphql/schema/query.py
@@ -0,0 +1,6 @@
+from cpl.graphql.abc.query_base import QueryBase
+
+
+class Query(QueryBase):
+ def __init__(self):
+ QueryBase.__init__(self)
\ No newline at end of file
diff --git a/src/cpl-graphql/cpl/graphql/schema/root_query.py b/src/cpl-graphql/cpl/graphql/schema/root_query.py
new file mode 100644
index 00000000..85ee1d38
--- /dev/null
+++ b/src/cpl-graphql/cpl/graphql/schema/root_query.py
@@ -0,0 +1,6 @@
+from cpl.graphql.schema.query import Query
+
+
+class RootQuery(Query):
+ def __init__(self):
+ Query.__init__(self)
diff --git a/src/cpl-graphql/cpl/graphql/service/__init__.py b/src/cpl-graphql/cpl/graphql/service/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/src/cpl-graphql/cpl/graphql/service/schema.py b/src/cpl-graphql/cpl/graphql/service/schema.py
new file mode 100644
index 00000000..48ce5c5f
--- /dev/null
+++ b/src/cpl-graphql/cpl/graphql/service/schema.py
@@ -0,0 +1,56 @@
+import graphene
+
+from cpl.api import APILogger
+from cpl.dependency.service_provider import ServiceProvider
+from cpl.graphql.schema.query import Query
+from cpl.graphql.schema.root_query import RootQuery
+
+
+class Schema:
+
+ def __init__(self, logger: APILogger, query: RootQuery, provider: ServiceProvider):
+ self._logger = logger
+ self._provider = provider
+
+ self._query = query
+ self._schema = None
+
+ @property
+ def schema(self) -> graphene.Schema | None:
+ return self._schema
+
+ @property
+ def query(self) -> RootQuery:
+ return self._query
+
+ def build(self) -> graphene.Schema:
+ self._schema = graphene.Schema(
+ query=self.to_graphene(self._query),
+ mutation=None,
+ subscription=None,
+ )
+ return self._schema
+
+ def to_graphene(self, query: Query, name: str | None = None):
+ assert query is not None, "Query cannot be None"
+ attrs = {}
+
+ for field in query.get_fields().values():
+ if field.type == "String":
+ attrs[field.name] = graphene.Field(
+ graphene.String,
+ **field.args,
+ resolver=field.resolver
+ )
+
+ elif field.type == "Object" and field.subquery is not None:
+ subquery = self._provider.get_service(field.subquery)
+ sub = self.to_graphene(subquery, name=field.name.capitalize())
+ attrs[field.name] = graphene.Field(
+ sub,
+ **field.args,
+ resolver=field.resolver
+ )
+
+ class_name = name or query.__class__.__name__
+ return type(class_name, (graphene.ObjectType,), attrs)
diff --git a/src/cpl-graphql/cpl/graphql/service/service.py b/src/cpl-graphql/cpl/graphql/service/service.py
new file mode 100644
index 00000000..d0a65891
--- /dev/null
+++ b/src/cpl-graphql/cpl/graphql/service/service.py
@@ -0,0 +1,31 @@
+from typing import Any, Dict, Optional
+
+from cpl.api.typing import TRequest
+from cpl.graphql.service.schema import Schema
+
+
+class GraphQLService:
+ def __init__(self, schema: Schema):
+ self._schema = schema.schema
+ if self._schema is None:
+ raise ValueError("Schema has not been built. Call schema.build() before using the service.")
+
+ async def execute(
+ self,
+ query: str,
+ variables: Optional[Dict[str, Any]],
+ request: TRequest,
+ ) -> Dict[str, Any]:
+ result = await self._schema.execute_async(
+ query,
+ variable_values=variables,
+ context_value={"request": request},
+ )
+
+ response_data: Dict[str, Any] = {}
+ if result.errors:
+ response_data["errors"] = [str(e) for e in result.errors]
+ if result.data:
+ response_data["data"] = result.data
+
+ return response_data
diff --git a/src/cpl-graphql/cpl/graphql/typing.py b/src/cpl-graphql/cpl/graphql/typing.py
new file mode 100644
index 00000000..58587f3f
--- /dev/null
+++ b/src/cpl-graphql/cpl/graphql/typing.py
@@ -0,0 +1,5 @@
+from typing import Type
+
+from cpl.graphql.schema.query import Query
+
+TQuery = Type[Query]
\ No newline at end of file
diff --git a/src/cpl-graphql/pyproject.toml b/src/cpl-graphql/pyproject.toml
new file mode 100644
index 00000000..cecb85d2
--- /dev/null
+++ b/src/cpl-graphql/pyproject.toml
@@ -0,0 +1,30 @@
+[build-system]
+requires = ["setuptools>=70.1.0", "wheel>=0.43.0"]
+build-backend = "setuptools.build_meta"
+
+[project]
+name = "cpl-database"
+version = "2024.7.0"
+description = "CPL database"
+readme ="CPL database package"
+requires-python = ">=3.12"
+license = { text = "MIT" }
+authors = [
+ { name = "Sven Heidemann", email = "sven.heidemann@sh-edraft.de" }
+]
+keywords = ["cpl", "database", "backend", "shared", "library"]
+
+dynamic = ["dependencies", "optional-dependencies"]
+
+[project.urls]
+Homepage = "https://www.sh-edraft.de"
+
+[tool.setuptools.packages.find]
+where = ["."]
+include = ["cpl*"]
+
+[tool.setuptools.dynamic]
+dependencies = { file = ["requirements.txt"] }
+optional-dependencies.dev = { file = ["requirements.dev.txt"] }
+
+
diff --git a/src/cpl-graphql/requirements.dev.txt b/src/cpl-graphql/requirements.dev.txt
new file mode 100644
index 00000000..e7664b42
--- /dev/null
+++ b/src/cpl-graphql/requirements.dev.txt
@@ -0,0 +1 @@
+black==25.1.0
\ No newline at end of file
diff --git a/src/cpl-graphql/requirements.txt b/src/cpl-graphql/requirements.txt
new file mode 100644
index 00000000..abe92c36
--- /dev/null
+++ b/src/cpl-graphql/requirements.txt
@@ -0,0 +1,2 @@
+cpl-api
+graphene==3.4.3
\ No newline at end of file
diff --git a/src/cpl-query/cpl/query/ordered_enumerable.py b/src/cpl-query/cpl/query/ordered_enumerable.py
index 89edc3d7..03405057 100644
--- a/src/cpl-query/cpl/query/ordered_enumerable.py
+++ b/src/cpl-query/cpl/query/ordered_enumerable.py
@@ -6,7 +6,7 @@ from cpl.query.typing import K
class OrderedEnumerable(Enumerable[T]):
def __init__(self, source, key_selectors: List[tuple[Callable[[T], K], bool]]):
- super().__init__(source)
+ Enumerable.__init__(self, source)
self._key_selectors = key_selectors
def __iter__(self) -> Iterator[T]: