Skip to content

Commit dcf7ea4

Browse files
committed
Checkpoint
1 parent fb49b6f commit dcf7ea4

File tree

3 files changed

+134
-88
lines changed

3 files changed

+134
-88
lines changed

src/fastcs/backends/graphQL/graphQL.py

Lines changed: 68 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@
66
import strawberry
77
import uvicorn
88
from fastapi import FastAPI
9-
from pydantic import create_model
109
from strawberry.asgi import GraphQL
1110
from strawberry.tools import create_type
11+
from strawberry.types.field import StrawberryField
1212

1313
from fastcs.attributes import AttrR, AttrRW, AttrW, T
1414
from fastcs.controller import BaseController
@@ -22,39 +22,30 @@ class GraphQLServerOptions:
2222
log_level: str = "info"
2323

2424

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-
4125
class GraphQLServer:
4226
def __init__(self, mapping: Mapping):
4327
self._mapping = mapping
28+
self._field_dict: dict[str, list[StrawberryField]] = {
29+
"Query": [],
30+
"Mutation": [],
31+
}
4432
self._app = self._create_app()
4533

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)
34+
def _create_app(self) -> FastAPI:
35+
_add_dev_attributes(self._field_dict, self._mapping)
36+
_add_dev_commands(self._field_dict, self._mapping)
37+
38+
schema_kwargs = {}
39+
for key, value in self._field_dict.items():
40+
if self._field_dict[key]:
41+
# Strawberry types map to graphql object
42+
schema_kwargs[key.lower()] = create_type(key, value)
43+
schema = strawberry.Schema(**schema_kwargs) # type: ignore
44+
graphql_app: GraphQL = GraphQL(schema)
5245

5346
app = FastAPI()
5447
app.add_route("/graphql", graphql_app)
5548
app.add_websocket_route("/graphql", graphql_app)
56-
# _add_dev_attributes(app, self._mapping)
57-
# _add_dev_commands(app, self._mapping)
5849

5950
return app
6051

@@ -70,106 +61,95 @@ def run(self, options: GraphQLServerOptions | None = None) -> None:
7061
)
7162

7263

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(
64+
def _wrap_attr_set(
65+
d_attr_name: str,
8166
attribute: AttrW[T],
8267
) -> Callable[[T], Coroutine[Any, Any, None]]:
83-
async def attr_set(request):
84-
await attribute.process_without_display_update(request.value)
68+
async def _dynamic_f(value):
69+
await attribute.process_without_display_update(value)
70+
return value
8571

86-
# Fast api uses type annotations for validation, schema, conversions
87-
attr_set.__annotations__["request"] = _put_request_body(attribute)
72+
# Add type annotations for validation, schema, conversions
73+
_dynamic_f.__name__ = d_attr_name
74+
_dynamic_f.__annotations__["value"] = attribute.datatype.dtype
75+
_dynamic_f.__annotations__["return"] = attribute.datatype.dtype
8876

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-
)
77+
return _dynamic_f
9778

9879

9980
def _wrap_attr_get(
81+
d_attr_name: str,
10082
attribute: AttrR[T],
10183
) -> 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}
84+
async def _dynamic_f() -> Any:
85+
return attribute.get()
10586

106-
return attr_get
87+
_dynamic_f.__name__ = d_attr_name
88+
_dynamic_f.__annotations__["return"] = attribute.datatype.dtype
10789

90+
return _dynamic_f
10891

109-
def _add_dev_attributes(app: FastAPI, mapping: Mapping) -> None:
92+
93+
def _add_dev_attributes(
94+
field_dict: dict[str, list[StrawberryField]], mapping: Mapping
95+
) -> None:
96+
pass
11097
for single_mapping in mapping.get_controller_mappings():
11198
path = single_mapping.controller.path
99+
# nest for each controller
100+
# if path:
112101

113102
for attr_name, attribute in single_mapping.attributes.items():
114103
attr_name = attr_name.title().replace("_", "")
115104
d_attr_name = f"{'/'.join(path)}/{attr_name}" if path else attr_name
116105

117106
match attribute:
118-
# https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods
119107
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
108+
field_dict["Query"].append(
109+
strawberry.field(_wrap_attr_get(d_attr_name, attribute))
133110
)
111+
field_dict["Mutation"].append(
112+
strawberry.mutation(_wrap_attr_set(d_attr_name, attribute))
113+
) # mutation for server changes https://graphql.org/learn/queries/
114+
134115
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),
116+
field_dict["Query"].append(
117+
strawberry.field(_wrap_attr_get(d_attr_name, attribute))
141118
)
119+
142120
case AttrW():
143-
app.add_api_route(
144-
f"/{d_attr_name}",
145-
_wrap_attr_put(attribute),
146-
methods=["PUT"],
147-
status_code=204,
121+
field_dict["Mutation"].append(
122+
strawberry.mutation(_wrap_attr_set(d_attr_name, attribute))
148123
)
149124

150125

151126
def _wrap_command(
152-
method: Callable, controller: BaseController
153-
) -> Callable[..., Awaitable[None]]:
154-
async def command() -> None:
127+
method_name: str, method: Callable, controller: BaseController
128+
) -> Callable[..., Awaitable[bool]]:
129+
async def _dynamic_f() -> bool:
155130
await MethodType(method, controller)()
131+
return True
156132

157-
return command
133+
_dynamic_f.__name__ = method_name
158134

135+
return _dynamic_f
159136

160-
def _add_dev_commands(app: FastAPI, mapping: Mapping) -> None:
137+
138+
def _add_dev_commands(
139+
field_dict: dict[str, list[StrawberryField]], mapping: Mapping
140+
) -> None:
161141
for single_mapping in mapping.get_controller_mappings():
162142
path = single_mapping.controller.path
163143

164144
for name, method in single_mapping.command_methods.items():
165145
cmd_name = name.title().replace("_", "")
166146
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,
147+
field_dict["Mutation"].append(
148+
strawberry.mutation(
149+
_wrap_command(
150+
d_cmd_name,
151+
method.fn,
152+
single_mapping.controller,
153+
)
154+
)
175155
)
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import json
2+
3+
import pytest
4+
5+
from fastcs.attributes import AttrR
6+
from fastcs.datatypes import Bool, Float, Int
7+
8+
9+
class TestGraphQLServer:
10+
@pytest.fixture(autouse=True)
11+
def _server_client(self, graphQL_client):
12+
self._client = graphQL_client
13+
14+
def client_read(self, field: str, value):
15+
query = f"query {{{field}}}"
16+
response = self._client.post("/graphql", json={"query": query})
17+
assert response.status_code == 200
18+
assert response.json()["data"] == {field: value}
19+
20+
def client_write(self, field: str, value):
21+
mutation = f"mutation {{{field}(value: {json.dumps(value)})}}"
22+
response = self._client.post("/graphql", json={"query": mutation})
23+
assert response.status_code == 200
24+
assert response.json()["data"] == {field: value}
25+
26+
def client_exec(self, field: str):
27+
mutation = f"mutation {{{field}}}"
28+
response = self._client.post("/graphql", json={"query": mutation})
29+
assert response.status_code == 200
30+
31+
def test_read_int(self):
32+
self.client_read("ReadInt", AttrR(Int())._value)
33+
34+
def test_read_write_int(self):
35+
self.client_read("ReadWriteInt", AttrR(Int())._value)
36+
self.client_write("ReadWriteInt", AttrR(Int())._value)
37+
38+
def test_read_write_float(self):
39+
self.client_read("ReadWriteFloat", AttrR(Float())._value)
40+
self.client_write("ReadWriteFloat", AttrR(Float())._value)
41+
42+
def test_read_bool(self):
43+
self.client_read("ReadBool", AttrR(Bool())._value)
44+
45+
def test_write_bool(self):
46+
self.client_write("WriteBool", AttrR(Bool())._value)
47+
48+
# # We need to discuss enums
49+
# def test_string_enum(self):
50+
51+
def test_big_enum(self):
52+
self.client_read(
53+
"BigEnum", AttrR(Int(), allowed_values=list(range(1, 18)))._value
54+
)
55+
56+
def test_go(self):
57+
self.client_exec("Go")

tests/conftest.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@
88

99
import pytest
1010
from aioca import purge_channel_caches
11+
from fastapi.testclient import TestClient
1112

1213
from fastcs.attributes import AttrR, AttrRW, AttrW, Handler, Sender, Updater
14+
from fastcs.backends.graphQL.backend import GraphQLBackend
1315
from fastcs.controller import Controller
1416
from fastcs.datatypes import Bool, Float, Int, String
1517
from fastcs.mapping import Mapping
@@ -120,3 +122,10 @@ def ioc():
120122
except ValueError:
121123
# Someone else already called communicate
122124
pass
125+
126+
127+
@pytest.fixture(scope="class")
128+
def graphQL_client():
129+
app = GraphQLBackend(TestController())._server._app
130+
client = TestClient(app)
131+
return client

0 commit comments

Comments
 (0)