Skip to content

Commit fb49b6f

Browse files
committed
Checkpoint
1 parent bdab4fa commit fb49b6f

File tree

3 files changed

+191
-1
lines changed

3 files changed

+191
-1
lines changed

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ dev = [
4343
"types-mock",
4444
"aioca",
4545
"p4p",
46+
"strawberry-graphql[fastapi]",
4647
]
4748

4849
[project.scripts]
@@ -61,7 +62,7 @@ version_file = "src/fastcs/_version.py"
6162

6263
[tool.pyright]
6364
typeCheckingMode = "standard"
64-
reportMissingImports = false # Ignore missing stubs in imported modules
65+
reportMissingImports = false # Ignore missing stubs in imported modules
6566

6667
[tool.pytest.ini_options]
6768
# Run pytest with all our checkers, and don't spam us with massive tracebacks on error
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
from fastcs.backend import Backend
2+
from fastcs.controller import Controller
3+
4+
from .graphQL import GraphQLServer
5+
6+
7+
class GraphQLBackend(Backend):
8+
def __init__(self, controller: Controller):
9+
super().__init__(controller)
10+
11+
self._server = GraphQLServer(self._mapping)
12+
13+
def _run(self):
14+
self._server.run()
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
from collections.abc import Awaitable, Callable, Coroutine
2+
from dataclasses import dataclass
3+
from types import MethodType
4+
from typing import Any
5+
6+
import strawberry
7+
import uvicorn
8+
from fastapi import FastAPI
9+
from pydantic import create_model
10+
from strawberry.asgi import GraphQL
11+
from strawberry.tools import create_type
12+
13+
from fastcs.attributes import AttrR, AttrRW, AttrW, T
14+
from fastcs.controller import BaseController
15+
from fastcs.mapping import Mapping
16+
17+
18+
@dataclass
19+
class GraphQLServerOptions:
20+
host: str = "localhost"
21+
port: int = 8080
22+
log_level: str = "info"
23+
24+
25+
@strawberry.type
26+
class User:
27+
name: str
28+
age: int
29+
30+
31+
@strawberry.type
32+
class Query:
33+
@strawberry.field
34+
def user(self) -> User:
35+
return User(name="Patrick", age=100)
36+
37+
38+
schema = strawberry.Schema(query=Query)
39+
40+
41+
class GraphQLServer:
42+
def __init__(self, mapping: Mapping):
43+
self._mapping = mapping
44+
self._app = self._create_app()
45+
46+
def _create_app(self):
47+
Query = self._create_query(self._mapping)
48+
Mutation = self._create_mutation()
49+
# https://docs.github.com/en/graphql/guides/forming-calls-with-graphql
50+
schema = strawberry.Schema(query=Query, mutation=Mutation)
51+
graphql_app = GraphQL(schema)
52+
53+
app = FastAPI()
54+
app.add_route("/graphql", graphql_app)
55+
app.add_websocket_route("/graphql", graphql_app)
56+
# _add_dev_attributes(app, self._mapping)
57+
# _add_dev_commands(app, self._mapping)
58+
59+
return app
60+
61+
def run(self, options: GraphQLServerOptions | None = None) -> None:
62+
if options is None:
63+
options = GraphQLServerOptions()
64+
65+
uvicorn.run(
66+
self._app,
67+
host=options.host,
68+
port=options.port,
69+
log_level=options.log_level,
70+
)
71+
72+
73+
def _put_request_body(attribute: AttrW[T]):
74+
return create_model(
75+
f"Put{str(attribute.datatype.dtype)}Value",
76+
**{"value": (attribute.datatype.dtype, ...)}, # type: ignore
77+
)
78+
79+
80+
def _wrap_attr_put(
81+
attribute: AttrW[T],
82+
) -> Callable[[T], Coroutine[Any, Any, None]]:
83+
async def attr_set(request):
84+
await attribute.process_without_display_update(request.value)
85+
86+
# Fast api uses type annotations for validation, schema, conversions
87+
attr_set.__annotations__["request"] = _put_request_body(attribute)
88+
89+
return attr_set
90+
91+
92+
def _get_response_body(attribute: AttrR[T]):
93+
return create_model(
94+
f"Get{str(attribute.datatype.dtype)}Value",
95+
**{"value": (attribute.datatype.dtype, ...)}, # type: ignore
96+
)
97+
98+
99+
def _wrap_attr_get(
100+
attribute: AttrR[T],
101+
) -> Callable[[], Coroutine[Any, Any, Any]]:
102+
async def attr_get() -> Any: # Must be any as response_model is set
103+
value = attribute.get() # type: ignore
104+
return {"value": value}
105+
106+
return attr_get
107+
108+
109+
def _add_dev_attributes(app: FastAPI, mapping: Mapping) -> None:
110+
for single_mapping in mapping.get_controller_mappings():
111+
path = single_mapping.controller.path
112+
113+
for attr_name, attribute in single_mapping.attributes.items():
114+
attr_name = attr_name.title().replace("_", "")
115+
d_attr_name = f"{'/'.join(path)}/{attr_name}" if path else attr_name
116+
117+
match attribute:
118+
# https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods
119+
case AttrRW():
120+
strawberry.type
121+
app.add_api_route(
122+
f"/{d_attr_name}",
123+
_wrap_attr_get(attribute),
124+
methods=["GET"], # Idemponent and safe data retrieval,
125+
status_code=200, # https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/GET
126+
response_model=_get_response_body(attribute),
127+
)
128+
app.add_api_route(
129+
f"/{d_attr_name}",
130+
_wrap_attr_put(attribute),
131+
methods=["PUT"], # Idempotent state change
132+
status_code=204, # https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/PUT
133+
)
134+
case AttrR():
135+
app.add_api_route(
136+
f"/{d_attr_name}",
137+
_wrap_attr_get(attribute),
138+
methods=["GET"],
139+
status_code=200,
140+
response_model=_get_response_body(attribute),
141+
)
142+
case AttrW():
143+
app.add_api_route(
144+
f"/{d_attr_name}",
145+
_wrap_attr_put(attribute),
146+
methods=["PUT"],
147+
status_code=204,
148+
)
149+
150+
151+
def _wrap_command(
152+
method: Callable, controller: BaseController
153+
) -> Callable[..., Awaitable[None]]:
154+
async def command() -> None:
155+
await MethodType(method, controller)()
156+
157+
return command
158+
159+
160+
def _add_dev_commands(app: FastAPI, mapping: Mapping) -> None:
161+
for single_mapping in mapping.get_controller_mappings():
162+
path = single_mapping.controller.path
163+
164+
for name, method in single_mapping.command_methods.items():
165+
cmd_name = name.title().replace("_", "")
166+
d_cmd_name = f"{'/'.join(path)}/{cmd_name}" if path else cmd_name
167+
app.add_api_route(
168+
f"/{d_cmd_name}",
169+
_wrap_command(
170+
method.fn,
171+
single_mapping.controller,
172+
),
173+
methods=["PUT"],
174+
status_code=204,
175+
)

0 commit comments

Comments
 (0)