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]: