Added gql base #181
This commit is contained in:
@@ -3,7 +3,7 @@ from starlette.responses import JSONResponse
|
|||||||
from cpl.api.api_module import ApiModule
|
from cpl.api.api_module import ApiModule
|
||||||
from cpl.api.application.web_app import WebApp
|
from cpl.api.application.web_app import WebApp
|
||||||
from cpl.application.application_builder import ApplicationBuilder
|
from cpl.application.application_builder import ApplicationBuilder
|
||||||
from cpl.auth import AuthModule
|
from cpl.graphql.application.graphql_app import GraphQLApp
|
||||||
from cpl.auth.permission.permissions import Permissions
|
from cpl.auth.permission.permissions import Permissions
|
||||||
from cpl.auth.schema import AuthUser, Role
|
from cpl.auth.schema import AuthUser, Role
|
||||||
from cpl.core.configuration import Configuration
|
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.environment import Environment
|
||||||
from cpl.core.utils.cache import Cache
|
from cpl.core.utils.cache import Cache
|
||||||
from cpl.database.mysql.mysql_module import MySQLModule
|
from cpl.database.mysql.mysql_module import MySQLModule
|
||||||
|
from cpl.graphql.graphql_module import GraphQLModule
|
||||||
|
from cpl.graphql.schema.root_query import RootQuery
|
||||||
|
from queries.hello import HelloQuery
|
||||||
from scoped_service import ScopedService
|
from scoped_service import ScopedService
|
||||||
from service import PingService
|
from service import PingService
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
builder = ApplicationBuilder[WebApp](WebApp)
|
builder = ApplicationBuilder[GraphQLApp](GraphQLApp)
|
||||||
|
|
||||||
Configuration.add_json_file(f"appsettings.json")
|
Configuration.add_json_file(f"appsettings.json")
|
||||||
Configuration.add_json_file(f"appsettings.{Environment.get_environment()}.json")
|
Configuration.add_json_file(f"appsettings.{Environment.get_environment()}.json")
|
||||||
@@ -27,12 +30,15 @@ def main():
|
|||||||
builder.services.add_transient(PingService)
|
builder.services.add_transient(PingService)
|
||||||
builder.services.add_module(MySQLModule)
|
builder.services.add_module(MySQLModule)
|
||||||
builder.services.add_module(ApiModule)
|
builder.services.add_module(ApiModule)
|
||||||
|
builder.services.add_module(GraphQLModule)
|
||||||
|
|
||||||
builder.services.add_scoped(ScopedService)
|
builder.services.add_scoped(ScopedService)
|
||||||
|
|
||||||
builder.services.add_cache(AuthUser)
|
builder.services.add_cache(AuthUser)
|
||||||
builder.services.add_cache(Role)
|
builder.services.add_cache(Role)
|
||||||
|
|
||||||
|
builder.services.add_transient(HelloQuery)
|
||||||
|
|
||||||
app = builder.build()
|
app = builder.build()
|
||||||
app.with_logging()
|
app.with_logging()
|
||||||
|
|
||||||
@@ -48,6 +54,13 @@ def main():
|
|||||||
)
|
)
|
||||||
app.with_routes_directory("routes")
|
app.with_routes_directory("routes")
|
||||||
|
|
||||||
|
schema = app.with_graphql()
|
||||||
|
schema.query.string_field("ping", resolver=lambda *_: "pong")
|
||||||
|
schema.query.with_query("hello", HelloQuery)
|
||||||
|
|
||||||
|
app.with_playground()
|
||||||
|
app.with_graphiql()
|
||||||
|
|
||||||
provider = builder.service_provider
|
provider = builder.service_provider
|
||||||
user_cache = provider.get_service(Cache[AuthUser])
|
user_cache = provider.get_service(Cache[AuthUser])
|
||||||
role_cache = provider.get_service(Cache[Role])
|
role_cache = provider.get_service(Cache[Role])
|
||||||
|
|||||||
0
example/api/src/queries/__init__.py
Normal file
0
example/api/src/queries/__init__.py
Normal file
13
example/api/src/queries/hello.py
Normal file
13
example/api/src/queries/hello.py
Normal file
@@ -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}",
|
||||||
|
)
|
||||||
45
src/cpl-api/cpl/api/abc/web_app_abc.py
Normal file
45
src/cpl-api/cpl/api/abc/web_app_abc.py
Normal 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: ...
|
||||||
@@ -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,6 +10,7 @@ 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.api.abc.web_app_abc import WebAppABC
|
||||||
from cpl.api.api_module import ApiModule
|
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
|
||||||
@@ -24,8 +25,7 @@ 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.auth.auth_module import AuthModule
|
||||||
from cpl.auth.permission.permission_module import PermissionsModule
|
from cpl.auth.permission.permission_module import PermissionsModule
|
||||||
from cpl.core.configuration.configuration import Configuration
|
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.service_provider import ServiceProvider
|
||||||
from cpl.dependency.typing import Modules
|
from cpl.dependency.typing import Modules
|
||||||
|
|
||||||
PolicyInput = Union[dict[str, PolicyResolver], Policy]
|
|
||||||
|
|
||||||
|
class WebApp(WebAppABC):
|
||||||
class WebApp(ApplicationABC):
|
def __init__(self, services: ServiceProvider, modules: Modules, required_modules: list[str | object] = None):
|
||||||
def __init__(self, services: ServiceProvider, modules: Modules):
|
WebAppABC.__init__(self, services, modules, [AuthModule, PermissionsModule, ApiModule] + (required_modules or []))
|
||||||
super().__init__(services, modules, [AuthModule, PermissionsModule, ApiModule])
|
|
||||||
self._app: Starlette | None = None
|
self._app: Starlette | None = None
|
||||||
|
|
||||||
self._logger = services.get_service(APILogger)
|
self._logger = services.get_service(APILogger)
|
||||||
@@ -78,16 +76,17 @@ class WebApp(ApplicationABC):
|
|||||||
self._logger.debug(f"Allowed origins: {origins}")
|
self._logger.debug(f"Allowed origins: {origins}")
|
||||||
return origins.split(",")
|
return origins.split(",")
|
||||||
|
|
||||||
def with_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"
|
||||||
@@ -102,6 +101,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],
|
||||||
@@ -131,7 +136,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,
|
||||||
@@ -179,6 +184,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 = []
|
||||||
|
|
||||||
@@ -206,13 +212,6 @@ class WebApp(ApplicationABC):
|
|||||||
self.with_middleware(AuthorizationMiddleware)
|
self.with_middleware(AuthorizationMiddleware)
|
||||||
return self
|
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):
|
async def main(self):
|
||||||
self._logger.debug(f"Preparing API")
|
self._logger.debug(f"Preparing API")
|
||||||
self._validate_policies()
|
self._validate_policies()
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from cpl.core.configuration import ConfigurationModelABC
|
|||||||
class ApiSettings(ConfigurationModelABC):
|
class ApiSettings(ConfigurationModelABC):
|
||||||
|
|
||||||
def __init__(self, src: Optional[dict] = None):
|
def __init__(self, src: Optional[dict] = None):
|
||||||
super().__init__(src)
|
ConfigurationModelABC.__init__(self, src)
|
||||||
|
|
||||||
self.option("host", str, "0.0.0.0")
|
self.option("host", str, "0.0.0.0")
|
||||||
self.option("port", int, 5000)
|
self.option("port", int, 5000)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ from typing import Union, Literal, Callable, Type, Awaitable
|
|||||||
from urllib.request import Request
|
from urllib.request import Request
|
||||||
|
|
||||||
from starlette.middleware import Middleware
|
from starlette.middleware import Middleware
|
||||||
|
from starlette.responses import Response
|
||||||
from starlette.types import ASGIApp
|
from starlette.types import ASGIApp
|
||||||
from starlette.websockets import WebSocket
|
from starlette.websockets import WebSocket
|
||||||
|
|
||||||
@@ -9,6 +10,7 @@ from cpl.api.abc.asgi_middleware_abc import ASGIMiddleware
|
|||||||
from cpl.auth.schema import AuthUser
|
from cpl.auth.schema import AuthUser
|
||||||
|
|
||||||
TRequest = Union[Request, WebSocket]
|
TRequest = Union[Request, WebSocket]
|
||||||
|
TEndpoint = Callable[[TRequest, ...], Awaitable[Response]] | Callable[[TRequest, ...], Response]
|
||||||
HTTPMethods = Literal["GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"]
|
HTTPMethods = Literal["GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"]
|
||||||
PartialMiddleware = Union[
|
PartialMiddleware = Union[
|
||||||
ASGIMiddleware,
|
ASGIMiddleware,
|
||||||
@@ -17,3 +19,4 @@ PartialMiddleware = Union[
|
|||||||
Callable[[ASGIApp], ASGIApp],
|
Callable[[ASGIApp], ASGIApp],
|
||||||
]
|
]
|
||||||
PolicyResolver = Callable[[AuthUser], bool | Awaitable[bool]]
|
PolicyResolver = Callable[[AuthUser], bool | Awaitable[bool]]
|
||||||
|
PolicyInput = Union[dict[str, PolicyResolver], "Policy"]
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ class ApplicationABC(ABC):
|
|||||||
|
|
||||||
module_dependency_error(
|
module_dependency_error(
|
||||||
type(self).__name__,
|
type(self).__name__,
|
||||||
module.__name__,
|
module.__name__ if not isinstance(module, str) else module,
|
||||||
ImportError(
|
ImportError(
|
||||||
f"Required module '{module}' for application '{self.__class__.__name__}' is not loaded. Load using 'add_module({module})' method."
|
f"Required module '{module}' for application '{self.__class__.__name__}' is not loaded. Load using 'add_module({module})' method."
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ from cpl.auth.permission.permissions import Permissions
|
|||||||
from cpl.core.typing import SerialId
|
from cpl.core.typing import SerialId
|
||||||
from cpl.database.abc import DbModelABC
|
from cpl.database.abc import DbModelABC
|
||||||
from cpl.database.logger import DBLogger
|
from cpl.database.logger import DBLogger
|
||||||
from cpl.dependency import ServiceProvider
|
from cpl.dependency import get_provider
|
||||||
|
|
||||||
|
|
||||||
class AuthUser(DbModelABC):
|
class AuthUser(DbModelABC):
|
||||||
@@ -87,3 +87,4 @@ class AuthUser(DbModelABC):
|
|||||||
|
|
||||||
self._keycloak_id = str(uuid.UUID(int=0))
|
self._keycloak_id = str(uuid.UUID(int=0))
|
||||||
await auth_user_dao.update(self)
|
await auth_user_dao.update(self)
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ class ModuleABC(ABC):
|
|||||||
__OPTIONAL_VARS = ["dependencies", "configuration", "singleton", "scoped", "transient", "hosted"]
|
__OPTIONAL_VARS = ["dependencies", "configuration", "singleton", "scoped", "transient", "hosted"]
|
||||||
|
|
||||||
def __init_subclass__(cls):
|
def __init_subclass__(cls):
|
||||||
super().__init_subclass__()
|
ABC.__init_subclass__()
|
||||||
|
|
||||||
if f"{cls.__module__}.{cls.__name__}" == "cpl.dependency.module.module.Module":
|
if f"{cls.__module__}.{cls.__name__}" == "cpl.dependency.module.module.Module":
|
||||||
return
|
return
|
||||||
|
|||||||
0
src/cpl-graphql/cpl/graphql/__init__.py
Normal file
0
src/cpl-graphql/cpl/graphql/__init__.py
Normal file
0
src/cpl-graphql/cpl/graphql/_endpoints/__init__.py
Normal file
0
src/cpl-graphql/cpl/graphql/_endpoints/__init__.py
Normal file
37
src/cpl-graphql/cpl/graphql/_endpoints/graphiql.py
Normal file
37
src/cpl-graphql/cpl/graphql/_endpoints/graphiql.py
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
from starlette.responses import HTMLResponse
|
||||||
|
|
||||||
|
async def graphiql_endpoint(request):
|
||||||
|
return HTMLResponse("""
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<title>GraphiQL</title>
|
||||||
|
<link href="https://unpkg.com/graphiql@2.4.0/graphiql.min.css" rel="stylesheet" />
|
||||||
|
</head>
|
||||||
|
<body style="margin:0;overflow:hidden;">
|
||||||
|
<div id="graphiql" style="height:100vh;"></div>
|
||||||
|
|
||||||
|
<!-- React + ReactDOM -->
|
||||||
|
<script src="https://unpkg.com/react@18.2.0/umd/react.production.min.js"></script>
|
||||||
|
<script src="https://unpkg.com/react-dom@18.2.0/umd/react-dom.production.min.js"></script>
|
||||||
|
|
||||||
|
<!-- GraphiQL -->
|
||||||
|
<script src="https://unpkg.com/graphiql@2.4.0/graphiql.min.js"></script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const graphQLFetcher = graphQLParams =>
|
||||||
|
fetch('/api/graphql', {
|
||||||
|
method: 'post',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(graphQLParams),
|
||||||
|
}).then(response => response.json()).catch(() => response.text());
|
||||||
|
|
||||||
|
ReactDOM.render(
|
||||||
|
React.createElement(GraphiQL, { fetcher: graphQLFetcher }),
|
||||||
|
document.getElementById('graphiql'),
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
""")
|
||||||
13
src/cpl-graphql/cpl/graphql/_endpoints/graphql.py
Normal file
13
src/cpl-graphql/cpl/graphql/_endpoints/graphql.py
Normal file
@@ -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)
|
||||||
27
src/cpl-graphql/cpl/graphql/_endpoints/playground.py
Normal file
27
src/cpl-graphql/cpl/graphql/_endpoints/playground.py
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
from starlette.requests import Request
|
||||||
|
from starlette.responses import Response, HTMLResponse
|
||||||
|
|
||||||
|
|
||||||
|
async def playground_endpoint(request: Request) -> Response:
|
||||||
|
return HTMLResponse("""
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset=utf-8/>
|
||||||
|
<title>GraphQL Playground</title>
|
||||||
|
<link rel="stylesheet" href="https://unpkg.com/graphql-playground-react/build/static/css/index.css" />
|
||||||
|
<link rel="shortcut icon" href="https://raw.githubusercontent.com/graphql/graphql-playground/master/packages/graphql-playground-react/public/favicon.png" />
|
||||||
|
<script src="https://unpkg.com/graphql-playground-react/build/static/js/middleware.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"/>
|
||||||
|
<script>
|
||||||
|
window.addEventListener('load', function () {
|
||||||
|
GraphQLPlayground.init(document.getElementById('root'), {
|
||||||
|
endpoint: '/api/graphql'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
""")
|
||||||
0
src/cpl-graphql/cpl/graphql/abc/__init__.py
Normal file
0
src/cpl-graphql/cpl/graphql/abc/__init__.py
Normal file
54
src/cpl-graphql/cpl/graphql/abc/query_base.py
Normal file
54
src/cpl-graphql/cpl/graphql/abc/query_base.py
Normal file
@@ -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)
|
||||||
1
src/cpl-graphql/cpl/graphql/application/__init__.py
Normal file
1
src/cpl-graphql/cpl/graphql/application/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from .graphql_app import WebApp
|
||||||
80
src/cpl-graphql/cpl/graphql/application/graphql_app.py
Normal file
80
src/cpl-graphql/cpl/graphql/application/graphql_app.py
Normal file
@@ -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
|
||||||
17
src/cpl-graphql/cpl/graphql/graphql_module.py
Normal file
17
src/cpl-graphql/cpl/graphql/graphql_module.py
Normal file
@@ -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()
|
||||||
0
src/cpl-graphql/cpl/graphql/schema/__init__.py
Normal file
0
src/cpl-graphql/cpl/graphql/schema/__init__.py
Normal file
30
src/cpl-graphql/cpl/graphql/schema/field.py
Normal file
30
src/cpl-graphql/cpl/graphql/schema/field.py
Normal file
@@ -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
|
||||||
6
src/cpl-graphql/cpl/graphql/schema/query.py
Normal file
6
src/cpl-graphql/cpl/graphql/schema/query.py
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
from cpl.graphql.abc.query_base import QueryBase
|
||||||
|
|
||||||
|
|
||||||
|
class Query(QueryBase):
|
||||||
|
def __init__(self):
|
||||||
|
QueryBase.__init__(self)
|
||||||
6
src/cpl-graphql/cpl/graphql/schema/root_query.py
Normal file
6
src/cpl-graphql/cpl/graphql/schema/root_query.py
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
from cpl.graphql.schema.query import Query
|
||||||
|
|
||||||
|
|
||||||
|
class RootQuery(Query):
|
||||||
|
def __init__(self):
|
||||||
|
Query.__init__(self)
|
||||||
0
src/cpl-graphql/cpl/graphql/service/__init__.py
Normal file
0
src/cpl-graphql/cpl/graphql/service/__init__.py
Normal file
56
src/cpl-graphql/cpl/graphql/service/schema.py
Normal file
56
src/cpl-graphql/cpl/graphql/service/schema.py
Normal file
@@ -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)
|
||||||
31
src/cpl-graphql/cpl/graphql/service/service.py
Normal file
31
src/cpl-graphql/cpl/graphql/service/service.py
Normal file
@@ -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
|
||||||
5
src/cpl-graphql/cpl/graphql/typing.py
Normal file
5
src/cpl-graphql/cpl/graphql/typing.py
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
from typing import Type
|
||||||
|
|
||||||
|
from cpl.graphql.schema.query import Query
|
||||||
|
|
||||||
|
TQuery = Type[Query]
|
||||||
30
src/cpl-graphql/pyproject.toml
Normal file
30
src/cpl-graphql/pyproject.toml
Normal file
@@ -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"] }
|
||||||
|
|
||||||
|
|
||||||
1
src/cpl-graphql/requirements.dev.txt
Normal file
1
src/cpl-graphql/requirements.dev.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
black==25.1.0
|
||||||
2
src/cpl-graphql/requirements.txt
Normal file
2
src/cpl-graphql/requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
cpl-api
|
||||||
|
graphene==3.4.3
|
||||||
@@ -6,7 +6,7 @@ from cpl.query.typing import K
|
|||||||
|
|
||||||
class OrderedEnumerable(Enumerable[T]):
|
class OrderedEnumerable(Enumerable[T]):
|
||||||
def __init__(self, source, key_selectors: List[tuple[Callable[[T], K], bool]]):
|
def __init__(self, source, key_selectors: List[tuple[Callable[[T], K], bool]]):
|
||||||
super().__init__(source)
|
Enumerable.__init__(self, source)
|
||||||
self._key_selectors = key_selectors
|
self._key_selectors = key_selectors
|
||||||
|
|
||||||
def __iter__(self) -> Iterator[T]:
|
def __iter__(self) -> Iterator[T]:
|
||||||
|
|||||||
Reference in New Issue
Block a user