Source code for fastcs.transport.graphQL.graphQL
from collections.abc import Awaitable, Callable, Coroutine
from typing import Any
import strawberry
import uvicorn
from strawberry.asgi import GraphQL
from strawberry.tools import create_type
from strawberry.types.field import StrawberryField
from fastcs.attributes import AttrR, AttrRW, AttrW, T
from fastcs.controller_api import ControllerAPI
from fastcs.exceptions import FastCSException
from .options import GraphQLServerOptions
[docs]
class GraphQLServer:
"""A GraphQL server which handles a controller.
Avoid running directly, instead use `fastcs.launch.FastCS`.
"""
def __init__(self, controller_api: ControllerAPI):
self._controller_api = controller_api
self._app = self._create_app()
def _create_app(self) -> GraphQL:
api = GraphQLAPI(self._controller_api)
schema = api.create_schema()
app = GraphQL(schema)
return app
async def serve(self, options: GraphQLServerOptions | None = None) -> None:
options = options or GraphQLServerOptions()
self._server = uvicorn.Server(
uvicorn.Config(
app=self._app,
host=options.host,
port=options.port,
log_level=options.log_level,
)
)
await self._server.serve()
[docs]
class GraphQLAPI:
"""A Strawberry API built dynamically from a `ControllerAPI`"""
def __init__(self, controller_api: ControllerAPI):
self.queries: list[StrawberryField] = []
self.mutations: list[StrawberryField] = []
self._process_attributes(controller_api)
self._process_commands(controller_api)
self._process_sub_apis(controller_api)
def _process_attributes(self, api: ControllerAPI):
"""Create queries and mutations from api attributes."""
for attr_name, attribute in api.attributes.items():
match attribute:
# mutation for server changes https://graphql.org/learn/queries/
case AttrRW():
self.queries.append(
strawberry.field(_wrap_attr_get(attr_name, attribute))
)
self.mutations.append(
strawberry.mutation(_wrap_attr_set(attr_name, attribute))
)
case AttrR():
self.queries.append(
strawberry.field(_wrap_attr_get(attr_name, attribute))
)
case AttrW():
self.mutations.append(
strawberry.mutation(_wrap_attr_set(attr_name, attribute))
)
def _process_commands(self, controller_api: ControllerAPI):
"""Create mutations from api commands"""
for name, method in controller_api.command_methods.items():
self.mutations.append(strawberry.mutation(_wrap_command(name, method.fn)))
def _process_sub_apis(self, root_controller_api: ControllerAPI):
"""Recursively add fields from the queries and mutations of sub apis"""
for controller_api in root_controller_api.sub_apis.values():
name = "".join(controller_api.path)
child_tree = GraphQLAPI(controller_api)
if child_tree.queries:
self.queries.append(
_wrap_as_field(
name, create_type(f"{name}Query", child_tree.queries)
)
)
if child_tree.mutations:
self.mutations.append(
_wrap_as_field(
name, create_type(f"{name}Mutation", child_tree.mutations)
)
)
[docs]
def create_schema(self) -> strawberry.Schema:
"""Create a Strawberry Schema to load into a GraphQL application."""
if not self.queries:
raise FastCSException(
"Can't create GraphQL transport from ControllerAPI with no read "
"attributes"
)
query = create_type("Query", self.queries)
mutation = create_type("Mutation", self.mutations) if self.mutations else None
return strawberry.Schema(query=query, mutation=mutation)
def _wrap_attr_set(
attr_name: str, attribute: AttrW[T]
) -> Callable[[T], Coroutine[Any, Any, None]]:
"""Wrap an attribute in a function with annotations for strawberry"""
async def _dynamic_f(value):
await attribute.process(value)
return value
# Add type annotations for validation, schema, conversions
_dynamic_f.__name__ = attr_name
_dynamic_f.__annotations__["value"] = attribute.datatype.dtype
_dynamic_f.__annotations__["return"] = attribute.datatype.dtype
return _dynamic_f
def _wrap_attr_get(
attr_name: str, attribute: AttrR[T]
) -> Callable[[], Coroutine[Any, Any, Any]]:
"""Wrap an attribute in a function with annotations for strawberry"""
async def _dynamic_f() -> Any:
return attribute.get()
_dynamic_f.__name__ = attr_name
_dynamic_f.__annotations__["return"] = attribute.datatype.dtype
return _dynamic_f
def _wrap_as_field(field_name: str, operation: type) -> StrawberryField:
"""Wrap a strawberry type as a field of a parent type"""
def _dynamic_field():
return operation()
_dynamic_field.__name__ = field_name
_dynamic_field.__annotations__["return"] = operation
return strawberry.field(_dynamic_field)
def _wrap_command(method_name: str, method: Callable) -> Callable[..., Awaitable[bool]]:
"""Wrap a command in a function with annotations for strawberry"""
async def _dynamic_f() -> bool:
await method()
return True
_dynamic_f.__name__ = method_name
return _dynamic_f