soia-client 1.0.20__tar.gz → 1.0.22__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {soia_client-1.0.20 → soia_client-1.0.22}/PKG-INFO +1 -1
- {soia_client-1.0.20 → soia_client-1.0.22}/pyproject.toml +1 -1
- {soia_client-1.0.20 → soia_client-1.0.22}/soia/__init__.py +3 -3
- soia_client-1.0.22/soia/_impl/service.py +339 -0
- {soia_client-1.0.20 → soia_client-1.0.22}/soia/_impl/structs.py +10 -3
- {soia_client-1.0.20 → soia_client-1.0.22}/soia_client.egg-info/PKG-INFO +1 -1
- {soia_client-1.0.20 → soia_client-1.0.22}/tests/test_module_initializer.py +6 -0
- soia_client-1.0.20/soia/_impl/service.py +0 -205
- {soia_client-1.0.20 → soia_client-1.0.22}/LICENSE +0 -0
- {soia_client-1.0.20 → soia_client-1.0.22}/README.md +0 -0
- {soia_client-1.0.20 → soia_client-1.0.22}/setup.cfg +0 -0
- {soia_client-1.0.20 → soia_client-1.0.22}/soia/_impl/__init__.py +0 -0
- {soia_client-1.0.20 → soia_client-1.0.22}/soia/_impl/arrays.py +0 -0
- {soia_client-1.0.20 → soia_client-1.0.22}/soia/_impl/enums.py +0 -0
- {soia_client-1.0.20 → soia_client-1.0.22}/soia/_impl/function_maker.py +0 -0
- {soia_client-1.0.20 → soia_client-1.0.22}/soia/_impl/keyed_items.py +0 -0
- {soia_client-1.0.20 → soia_client-1.0.22}/soia/_impl/method.py +0 -0
- {soia_client-1.0.20 → soia_client-1.0.22}/soia/_impl/never.py +0 -0
- {soia_client-1.0.20 → soia_client-1.0.22}/soia/_impl/optionals.py +0 -0
- {soia_client-1.0.20 → soia_client-1.0.22}/soia/_impl/primitives.py +0 -0
- {soia_client-1.0.20 → soia_client-1.0.22}/soia/_impl/repr.py +0 -0
- {soia_client-1.0.20 → soia_client-1.0.22}/soia/_impl/serializer.py +0 -0
- {soia_client-1.0.20 → soia_client-1.0.22}/soia/_impl/serializers.py +0 -0
- {soia_client-1.0.20 → soia_client-1.0.22}/soia/_impl/service_client.py +0 -0
- {soia_client-1.0.20 → soia_client-1.0.22}/soia/_impl/timestamp.py +0 -0
- {soia_client-1.0.20 → soia_client-1.0.22}/soia/_impl/type_adapter.py +0 -0
- {soia_client-1.0.20 → soia_client-1.0.22}/soia/_module_initializer.py +0 -0
- {soia_client-1.0.20 → soia_client-1.0.22}/soia/_spec.py +0 -0
- {soia_client-1.0.20 → soia_client-1.0.22}/soia/reflection.py +0 -0
- {soia_client-1.0.20 → soia_client-1.0.22}/soia_client.egg-info/SOURCES.txt +0 -0
- {soia_client-1.0.20 → soia_client-1.0.22}/soia_client.egg-info/dependency_links.txt +0 -0
- {soia_client-1.0.20 → soia_client-1.0.22}/soia_client.egg-info/top_level.txt +0 -0
- {soia_client-1.0.20 → soia_client-1.0.22}/tests/test_serializers.py +0 -0
- {soia_client-1.0.20 → soia_client-1.0.22}/tests/test_timestamp.py +0 -0
@@ -8,7 +8,7 @@ from soia._impl.serializers import (
|
|
8
8
|
optional_serializer,
|
9
9
|
primitive_serializer,
|
10
10
|
)
|
11
|
-
from soia._impl.service import
|
11
|
+
from soia._impl.service import RawServiceResponse, Service, ServiceAsync
|
12
12
|
from soia._impl.service_client import ServiceClient
|
13
13
|
from soia._impl.timestamp import Timestamp
|
14
14
|
|
@@ -18,10 +18,10 @@ __all__ = [
|
|
18
18
|
"_",
|
19
19
|
"KeyedItems",
|
20
20
|
"Method",
|
21
|
-
"
|
22
|
-
"ResponseHeaders",
|
21
|
+
"RawServiceResponse",
|
23
22
|
"Serializer",
|
24
23
|
"Service",
|
24
|
+
"ServiceAsync",
|
25
25
|
"ServiceClient",
|
26
26
|
"Timestamp",
|
27
27
|
"array_serializer",
|
@@ -0,0 +1,339 @@
|
|
1
|
+
import inspect
|
2
|
+
import json
|
3
|
+
from collections.abc import Awaitable, Callable
|
4
|
+
from dataclasses import dataclass
|
5
|
+
from typing import Any, Generic, Literal, TypeVar, Union, cast
|
6
|
+
|
7
|
+
from soia._impl.method import Method, Request, Response
|
8
|
+
|
9
|
+
RequestHeaders = TypeVar("RequestHeaders")
|
10
|
+
|
11
|
+
ResponseHeaders = TypeVar("ResponseHeaders")
|
12
|
+
|
13
|
+
|
14
|
+
@dataclass(frozen=True)
|
15
|
+
class _MethodImpl(Generic[Request, Response, RequestHeaders, ResponseHeaders]):
|
16
|
+
method: Method[Request, Response]
|
17
|
+
impl: Callable[
|
18
|
+
# Parameters
|
19
|
+
[Request, RequestHeaders, ResponseHeaders],
|
20
|
+
# Return type
|
21
|
+
Union[Response, Awaitable[Response]],
|
22
|
+
]
|
23
|
+
|
24
|
+
|
25
|
+
@dataclass(frozen=True)
|
26
|
+
class RawServiceResponse:
|
27
|
+
data: str
|
28
|
+
type: Literal["ok-json", "bad-request", "server-error"]
|
29
|
+
|
30
|
+
@property
|
31
|
+
def status_code(self):
|
32
|
+
if self.type == "ok-json":
|
33
|
+
return 200
|
34
|
+
elif self.type == "bad-request":
|
35
|
+
return 400
|
36
|
+
elif self.type == "server-error":
|
37
|
+
return 500
|
38
|
+
else:
|
39
|
+
raise TypeError(f"Unknown response type: {self.type}")
|
40
|
+
|
41
|
+
@property
|
42
|
+
def content_type(self):
|
43
|
+
if self.type == "ok-json":
|
44
|
+
return "application/json"
|
45
|
+
elif self.type == "bad-request" or self.type == "server-error":
|
46
|
+
return "text/plain; charset=utf-8"
|
47
|
+
else:
|
48
|
+
raise TypeError(f"Unknown response type: {self.type}")
|
49
|
+
|
50
|
+
|
51
|
+
@dataclass()
|
52
|
+
class _HandleRequestFlow(Generic[Request, Response, RequestHeaders, ResponseHeaders]):
|
53
|
+
req_body: str
|
54
|
+
req_headers: RequestHeaders
|
55
|
+
res_headers: ResponseHeaders
|
56
|
+
number_to_method_impl: dict[
|
57
|
+
int, _MethodImpl[Any, Any, RequestHeaders, ResponseHeaders]
|
58
|
+
]
|
59
|
+
_format: str = ""
|
60
|
+
|
61
|
+
def run(self) -> RawServiceResponse:
|
62
|
+
req_impl_pair_or_raw_response = self._parse_request()
|
63
|
+
if isinstance(req_impl_pair_or_raw_response, RawServiceResponse):
|
64
|
+
return req_impl_pair_or_raw_response
|
65
|
+
req, method_impl = req_impl_pair_or_raw_response
|
66
|
+
try:
|
67
|
+
res = method_impl.impl(req, self.req_headers, self.res_headers)
|
68
|
+
except Exception as e:
|
69
|
+
return RawServiceResponse(f"server error: {e}", "server-error")
|
70
|
+
if inspect.isawaitable(res):
|
71
|
+
raise TypeError("Method implementation must be synchronous")
|
72
|
+
return self._response_to_json(res, method_impl)
|
73
|
+
|
74
|
+
async def run_async(self) -> RawServiceResponse:
|
75
|
+
req_impl_pair_or_raw_response = self._parse_request()
|
76
|
+
if isinstance(req_impl_pair_or_raw_response, RawServiceResponse):
|
77
|
+
return req_impl_pair_or_raw_response
|
78
|
+
req, method_impl = req_impl_pair_or_raw_response
|
79
|
+
try:
|
80
|
+
res: Any = method_impl.impl(req, self.req_headers, self.res_headers)
|
81
|
+
if inspect.isawaitable(res):
|
82
|
+
res = await res
|
83
|
+
except Exception as e:
|
84
|
+
return RawServiceResponse(f"server error: {e}", "server-error")
|
85
|
+
return self._response_to_json(res, method_impl)
|
86
|
+
|
87
|
+
def _parse_request(
|
88
|
+
self,
|
89
|
+
) -> Union[
|
90
|
+
tuple[Any, _MethodImpl[Request, Response, RequestHeaders, ResponseHeaders]],
|
91
|
+
RawServiceResponse,
|
92
|
+
]:
|
93
|
+
if self.req_body == "list":
|
94
|
+
|
95
|
+
def method_to_json(method: Method) -> Any:
|
96
|
+
return {
|
97
|
+
"method": method.name,
|
98
|
+
"number": method.number,
|
99
|
+
"request": method.request_serializer.type_descriptor.as_json(),
|
100
|
+
"response": method.response_serializer.type_descriptor.as_json(),
|
101
|
+
}
|
102
|
+
|
103
|
+
json_code = json.dumps(
|
104
|
+
{
|
105
|
+
"methods": [
|
106
|
+
method_to_json(method_impl.method)
|
107
|
+
for method_impl in self.number_to_method_impl.values()
|
108
|
+
]
|
109
|
+
},
|
110
|
+
indent=2,
|
111
|
+
)
|
112
|
+
return RawServiceResponse(json_code, "ok-json")
|
113
|
+
|
114
|
+
parts = self.req_body.split(":", 3)
|
115
|
+
if len(parts) != 4:
|
116
|
+
return RawServiceResponse(
|
117
|
+
"bad request: invalid request format", "bad-request"
|
118
|
+
)
|
119
|
+
method_name = parts[0]
|
120
|
+
method_number_str = parts[1]
|
121
|
+
self.format = parts[2]
|
122
|
+
request_data = parts[3]
|
123
|
+
try:
|
124
|
+
method_number = int(method_number_str)
|
125
|
+
except Exception:
|
126
|
+
return RawServiceResponse(
|
127
|
+
"bad request: can't parse method number", "bad-request"
|
128
|
+
)
|
129
|
+
method_impl = self.number_to_method_impl.get(method_number)
|
130
|
+
if not method_impl:
|
131
|
+
return RawServiceResponse(
|
132
|
+
f"bad request: method not found: {method_name}; number: {method_number}",
|
133
|
+
"bad-request",
|
134
|
+
)
|
135
|
+
try:
|
136
|
+
req: Any = method_impl.method.request_serializer.from_json_code(
|
137
|
+
request_data
|
138
|
+
)
|
139
|
+
except Exception as e:
|
140
|
+
return RawServiceResponse(
|
141
|
+
f"bad request: can't parse JSON: {e}", "bad-request"
|
142
|
+
)
|
143
|
+
return (req, method_impl)
|
144
|
+
|
145
|
+
def _response_to_json(
|
146
|
+
self,
|
147
|
+
res: Response,
|
148
|
+
method_impl: _MethodImpl[Request, Response, RequestHeaders, ResponseHeaders],
|
149
|
+
) -> RawServiceResponse:
|
150
|
+
try:
|
151
|
+
res_json = method_impl.method.response_serializer.to_json_code(
|
152
|
+
res, readable=(self.format == "readable")
|
153
|
+
)
|
154
|
+
except Exception as e:
|
155
|
+
return RawServiceResponse(
|
156
|
+
f"server error: can't serialize response to JSON: {e}", "server-error"
|
157
|
+
)
|
158
|
+
return RawServiceResponse(res_json, "ok-json")
|
159
|
+
|
160
|
+
|
161
|
+
class _ServiceImpl(Generic[RequestHeaders, ResponseHeaders]):
|
162
|
+
_number_to_method_impl: dict[
|
163
|
+
int, _MethodImpl[Any, Any, RequestHeaders, ResponseHeaders]
|
164
|
+
]
|
165
|
+
|
166
|
+
def __init__(self):
|
167
|
+
self._number_to_method_impl = {}
|
168
|
+
|
169
|
+
def add_method(
|
170
|
+
self,
|
171
|
+
method: Method[Request, Response],
|
172
|
+
impl: Union[
|
173
|
+
# Sync
|
174
|
+
Callable[[Request], Response],
|
175
|
+
Callable[[Request, RequestHeaders], Response],
|
176
|
+
Callable[[Request, RequestHeaders, ResponseHeaders], Response],
|
177
|
+
# Async
|
178
|
+
Callable[[Request], Awaitable[Response]],
|
179
|
+
Callable[[Request, RequestHeaders], Awaitable[Response]],
|
180
|
+
Callable[[Request, RequestHeaders, ResponseHeaders], Awaitable[Response]],
|
181
|
+
],
|
182
|
+
) -> None:
|
183
|
+
signature = inspect.Signature.from_callable(impl)
|
184
|
+
num_positional_params = 0
|
185
|
+
for param in signature.parameters.values():
|
186
|
+
if param.kind in (
|
187
|
+
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
188
|
+
inspect.Parameter.POSITIONAL_ONLY,
|
189
|
+
):
|
190
|
+
num_positional_params += 1
|
191
|
+
if param.kind == inspect.Parameter.VAR_POSITIONAL:
|
192
|
+
raise ValueError("Method implementation cannot accept *args")
|
193
|
+
if num_positional_params not in range(1, 4):
|
194
|
+
raise ValueError(
|
195
|
+
"Method implementation must accept 1 to 3 positional parameters"
|
196
|
+
)
|
197
|
+
|
198
|
+
def resolved_impl(
|
199
|
+
req: Request, req_headers: RequestHeaders, res_headers: ResponseHeaders
|
200
|
+
) -> Response:
|
201
|
+
if num_positional_params == 1:
|
202
|
+
return cast(Callable[[Request], Response], impl)(req)
|
203
|
+
elif num_positional_params == 2:
|
204
|
+
return cast(Callable[[Request, RequestHeaders], Response], impl)(
|
205
|
+
req, req_headers
|
206
|
+
)
|
207
|
+
else:
|
208
|
+
return cast(
|
209
|
+
Callable[[Request, RequestHeaders, ResponseHeaders], Response], impl
|
210
|
+
)(req, req_headers, res_headers)
|
211
|
+
|
212
|
+
number = method.number
|
213
|
+
if number in self._number_to_method_impl:
|
214
|
+
raise ValueError(
|
215
|
+
f"Method with the same number already registered ({number})"
|
216
|
+
)
|
217
|
+
self._number_to_method_impl[number] = _MethodImpl(
|
218
|
+
method=method,
|
219
|
+
impl=resolved_impl,
|
220
|
+
)
|
221
|
+
|
222
|
+
def handle_request(
|
223
|
+
self,
|
224
|
+
req_body: str,
|
225
|
+
req_headers: RequestHeaders,
|
226
|
+
res_headers: ResponseHeaders,
|
227
|
+
) -> RawServiceResponse:
|
228
|
+
flow = _HandleRequestFlow(
|
229
|
+
req_body=req_body,
|
230
|
+
req_headers=req_headers,
|
231
|
+
res_headers=res_headers,
|
232
|
+
number_to_method_impl=self._number_to_method_impl,
|
233
|
+
)
|
234
|
+
return flow.run()
|
235
|
+
|
236
|
+
async def handle_request_async(
|
237
|
+
self,
|
238
|
+
req_body: str,
|
239
|
+
req_headers: RequestHeaders,
|
240
|
+
res_headers: ResponseHeaders,
|
241
|
+
) -> RawServiceResponse:
|
242
|
+
flow = _HandleRequestFlow(
|
243
|
+
req_body=req_body,
|
244
|
+
req_headers=req_headers,
|
245
|
+
res_headers=res_headers,
|
246
|
+
number_to_method_impl=self._number_to_method_impl,
|
247
|
+
)
|
248
|
+
return await flow.run_async()
|
249
|
+
|
250
|
+
|
251
|
+
class Service(Generic[RequestHeaders, ResponseHeaders]):
|
252
|
+
"""Wraps around the implementation of a soia service on the server side.
|
253
|
+
|
254
|
+
Usage: call '.add_method()' to register method implementations, then call
|
255
|
+
'.handle_request()' from the function called by your web framework when an
|
256
|
+
HTTP request is received at your service's endpoint.
|
257
|
+
|
258
|
+
Example with Flask:
|
259
|
+
|
260
|
+
from flask import Response, request
|
261
|
+
from werkzeug.datastructures import Headers
|
262
|
+
|
263
|
+
|
264
|
+
s = soia.Service[Headers, Headers]()
|
265
|
+
s.add_method(...)
|
266
|
+
s.add_method(...)
|
267
|
+
|
268
|
+
@app.route("/myapi", methods=["GET", "POST"])
|
269
|
+
def myapi():
|
270
|
+
if request.method == "POST":
|
271
|
+
req_body = request.get_data(as_text=True)
|
272
|
+
else:
|
273
|
+
query_string = request.query_string.decode("utf-8")
|
274
|
+
req_body = urllib.parse.unquote(query_string)
|
275
|
+
req_headers = request.headers
|
276
|
+
res_headers = Headers()
|
277
|
+
raw_response = s.handle_request(req_body, req_headers, res_headers)
|
278
|
+
return Response(
|
279
|
+
raw_response.data,
|
280
|
+
status=raw_response.status_code,
|
281
|
+
content_type=raw_response.content_type,
|
282
|
+
headers=res_headers,
|
283
|
+
)
|
284
|
+
"""
|
285
|
+
|
286
|
+
_impl: _ServiceImpl[RequestHeaders, ResponseHeaders]
|
287
|
+
|
288
|
+
def __init__(self):
|
289
|
+
self._impl = _ServiceImpl[RequestHeaders, ResponseHeaders]()
|
290
|
+
|
291
|
+
def add_method(
|
292
|
+
self,
|
293
|
+
method: Method[Request, Response],
|
294
|
+
impl: Union[
|
295
|
+
Callable[[Request], Response],
|
296
|
+
Callable[[Request, RequestHeaders], Response],
|
297
|
+
Callable[[Request, RequestHeaders, ResponseHeaders], Response],
|
298
|
+
],
|
299
|
+
) -> None:
|
300
|
+
self._impl.add_method(method, impl)
|
301
|
+
|
302
|
+
def handle_request(
|
303
|
+
self,
|
304
|
+
req_body: str,
|
305
|
+
req_headers: RequestHeaders,
|
306
|
+
res_headers: ResponseHeaders,
|
307
|
+
) -> RawServiceResponse:
|
308
|
+
return self._impl.handle_request(req_body, req_headers, res_headers)
|
309
|
+
|
310
|
+
|
311
|
+
class ServiceAsync(Generic[RequestHeaders, ResponseHeaders]):
|
312
|
+
_impl: _ServiceImpl[RequestHeaders, ResponseHeaders]
|
313
|
+
|
314
|
+
def __init__(self):
|
315
|
+
self._impl = _ServiceImpl[RequestHeaders, ResponseHeaders]()
|
316
|
+
|
317
|
+
def add_method(
|
318
|
+
self,
|
319
|
+
method: Method[Request, Response],
|
320
|
+
impl: Union[
|
321
|
+
# Sync
|
322
|
+
Callable[[Request], Response],
|
323
|
+
Callable[[Request, RequestHeaders], Response],
|
324
|
+
Callable[[Request, RequestHeaders, ResponseHeaders], Response],
|
325
|
+
# Async
|
326
|
+
Callable[[Request], Awaitable[Response]],
|
327
|
+
Callable[[Request, RequestHeaders], Awaitable[Response]],
|
328
|
+
Callable[[Request, RequestHeaders, ResponseHeaders], Awaitable[Response]],
|
329
|
+
],
|
330
|
+
) -> None:
|
331
|
+
self._impl.add_method(method, impl)
|
332
|
+
|
333
|
+
async def handle_request(
|
334
|
+
self,
|
335
|
+
req_body: str,
|
336
|
+
req_headers: RequestHeaders,
|
337
|
+
res_headers: ResponseHeaders,
|
338
|
+
) -> RawServiceResponse:
|
339
|
+
return await self._impl.handle_request_async(req_body, req_headers, res_headers)
|
@@ -135,6 +135,7 @@ class StructAdapter(TypeAdapter):
|
|
135
135
|
),
|
136
136
|
)
|
137
137
|
mutable_class.__init__ = cast(Any, _make_mutable_class_init_fn(fields))
|
138
|
+
frozen_class.whole = _make_whole_static_factory_method(frozen_class)
|
138
139
|
|
139
140
|
frozen_class.__eq__ = _make_eq_fn(fields)
|
140
141
|
frozen_class.__hash__ = cast(Any, _make_hash_fn(fields, self.record_hash))
|
@@ -260,7 +261,7 @@ def _make_frozen_class_init_fn(
|
|
260
261
|
fields: Sequence[_Field],
|
261
262
|
frozen_class: type,
|
262
263
|
simple_class: type,
|
263
|
-
) -> Callable[
|
264
|
+
) -> Callable[..., None]:
|
264
265
|
"""
|
265
266
|
Returns the implementation of the __init__() method of the frozen class.
|
266
267
|
"""
|
@@ -354,7 +355,7 @@ def _make_frozen_class_init_fn(
|
|
354
355
|
)
|
355
356
|
|
356
357
|
|
357
|
-
def _make_mutable_class_init_fn(fields: Sequence[_Field]) -> Callable[
|
358
|
+
def _make_mutable_class_init_fn(fields: Sequence[_Field]) -> Callable[..., None]:
|
358
359
|
"""
|
359
360
|
Returns the implementation of the __init__() method of the mutable class.
|
360
361
|
"""
|
@@ -383,6 +384,12 @@ def _make_mutable_class_init_fn(fields: Sequence[_Field]) -> Callable[[Any], Non
|
|
383
384
|
)
|
384
385
|
|
385
386
|
|
387
|
+
def _make_whole_static_factory_method(frozen_class: type) -> Callable[..., Any]:
|
388
|
+
def whole(**kwargs):
|
389
|
+
return frozen_class(**kwargs)
|
390
|
+
return whole
|
391
|
+
|
392
|
+
|
386
393
|
def _make_to_mutable_fn(
|
387
394
|
mutable_class: type,
|
388
395
|
simple_class: type,
|
@@ -526,7 +533,7 @@ def _make_hash_fn(
|
|
526
533
|
)
|
527
534
|
|
528
535
|
|
529
|
-
def _make_repr_fn(fields: Sequence[_Field]) -> Callable[[Any],
|
536
|
+
def _make_repr_fn(fields: Sequence[_Field]) -> Callable[[Any], str]:
|
530
537
|
"""
|
531
538
|
Returns the implementation of the __repr__() method of both the frozen class and the
|
532
539
|
mutable class.
|
@@ -363,6 +363,12 @@ class ModuleInitializerTestCase(unittest.TestCase):
|
|
363
363
|
self.assertEqual(point.x, 1.5)
|
364
364
|
self.assertEqual(point.y, 2.5)
|
365
365
|
|
366
|
+
def test_whole_static_factory_method(self):
|
367
|
+
point_cls = self.init_test_module()["Point"]
|
368
|
+
point = point_cls.whole(x=1.5, y=2.5)
|
369
|
+
self.assertEqual(point.x, 1.5)
|
370
|
+
self.assertEqual(point.y, 2.5)
|
371
|
+
|
366
372
|
def test_to_mutable(self):
|
367
373
|
point_cls = self.init_test_module()["Point"]
|
368
374
|
point = point_cls(x=1.5, y=2.5)
|
@@ -1,205 +0,0 @@
|
|
1
|
-
import inspect
|
2
|
-
import json
|
3
|
-
from dataclasses import dataclass
|
4
|
-
from typing import Any, Callable, Generic, Literal, TypeVar, Union, cast
|
5
|
-
|
6
|
-
from soia._impl.method import Method, Request, Response
|
7
|
-
|
8
|
-
RequestHeaders = TypeVar("RequestHeaders")
|
9
|
-
|
10
|
-
ResponseHeaders = TypeVar("ResponseHeaders")
|
11
|
-
|
12
|
-
|
13
|
-
class Service(Generic[RequestHeaders, ResponseHeaders]):
|
14
|
-
"""Wraps around the implementation of a soia service on the server side.
|
15
|
-
|
16
|
-
Usage: call '.add_method()' to register method implementations, then call
|
17
|
-
'.handle_request()' from the function called by your web framework when an
|
18
|
-
HTTP request is received at your service's endpoint.
|
19
|
-
|
20
|
-
Example with Flask:
|
21
|
-
|
22
|
-
from flask import Response, request
|
23
|
-
from werkzeug.datastructures import Headers
|
24
|
-
|
25
|
-
|
26
|
-
s = soia.Service[Headers, Headers]()
|
27
|
-
s.add_method(...)
|
28
|
-
s.add_method(...)
|
29
|
-
|
30
|
-
@app.route("/myapi", methods=["GET", "POST"])
|
31
|
-
def myapi():
|
32
|
-
if request.method == "POST":
|
33
|
-
req_body = request.get_data(as_text=True)
|
34
|
-
else:
|
35
|
-
query_string = request.query_string.decode("utf-8")
|
36
|
-
req_body = urllib.parse.unquote(query_string)
|
37
|
-
req_headers = request.headers
|
38
|
-
res_headers = Headers()
|
39
|
-
raw_response = s.handle_request(req_body, req_headers, res_headers)
|
40
|
-
return Response(
|
41
|
-
raw_response.data,
|
42
|
-
status=raw_response.status_code,
|
43
|
-
content_type=raw_response.content_type,
|
44
|
-
headers=res_headers,
|
45
|
-
)
|
46
|
-
"""
|
47
|
-
|
48
|
-
_number_to_method_impl: dict[int, "_MethodImpl"]
|
49
|
-
|
50
|
-
def __init__(self):
|
51
|
-
self._number_to_method_impl = {}
|
52
|
-
|
53
|
-
def add_method(
|
54
|
-
self,
|
55
|
-
method: Method[Request, Response],
|
56
|
-
impl: Union[
|
57
|
-
Callable[[Request], Response],
|
58
|
-
Callable[[Request, RequestHeaders], Response],
|
59
|
-
Callable[[Request, RequestHeaders, ResponseHeaders], Response],
|
60
|
-
],
|
61
|
-
) -> "Service":
|
62
|
-
signature = inspect.Signature.from_callable(impl)
|
63
|
-
num_positional_params = 0
|
64
|
-
for param in signature.parameters.values():
|
65
|
-
if param.kind in (
|
66
|
-
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
67
|
-
inspect.Parameter.POSITIONAL_ONLY,
|
68
|
-
):
|
69
|
-
num_positional_params += 1
|
70
|
-
if param.kind == inspect.Parameter.VAR_POSITIONAL:
|
71
|
-
raise ValueError("Method implementation cannot accept *args")
|
72
|
-
if num_positional_params not in range(1, 4):
|
73
|
-
raise ValueError(
|
74
|
-
"Method implementation must accept 1 to 3 positional parameters"
|
75
|
-
)
|
76
|
-
|
77
|
-
def resolved_impl(
|
78
|
-
req: Request, req_headers: RequestHeaders, res_headers: ResponseHeaders
|
79
|
-
) -> Response:
|
80
|
-
if num_positional_params == 1:
|
81
|
-
return cast(Callable[[Request], Response], impl)(req)
|
82
|
-
elif num_positional_params == 2:
|
83
|
-
return cast(Callable[[Request, RequestHeaders], Response], impl)(
|
84
|
-
req, req_headers
|
85
|
-
)
|
86
|
-
else:
|
87
|
-
return cast(
|
88
|
-
Callable[[Request, RequestHeaders, ResponseHeaders], Response], impl
|
89
|
-
)(req, req_headers, res_headers)
|
90
|
-
|
91
|
-
number = method.number
|
92
|
-
if number in self._number_to_method_impl:
|
93
|
-
raise ValueError(
|
94
|
-
f"Method with the same number already registered ({number})"
|
95
|
-
)
|
96
|
-
self._number_to_method_impl[number] = _MethodImpl(
|
97
|
-
method=method,
|
98
|
-
impl=resolved_impl,
|
99
|
-
)
|
100
|
-
return self
|
101
|
-
|
102
|
-
@dataclass(frozen=True)
|
103
|
-
class RawResponse:
|
104
|
-
data: str
|
105
|
-
type: Literal["ok-json", "bad-request", "server-error"]
|
106
|
-
|
107
|
-
@property
|
108
|
-
def status_code(self):
|
109
|
-
if self.type == "ok-json":
|
110
|
-
return 200
|
111
|
-
elif self.type == "bad-request":
|
112
|
-
return 400
|
113
|
-
elif self.type == "server-error":
|
114
|
-
return 500
|
115
|
-
else:
|
116
|
-
raise TypeError(f"Unknown response type: {self.type}")
|
117
|
-
|
118
|
-
@property
|
119
|
-
def content_type(self):
|
120
|
-
if self.type == "ok-json":
|
121
|
-
return "application/json"
|
122
|
-
elif self.type == "bad-request" or self.type == "server-error":
|
123
|
-
return "text/plain; charset=utf-8"
|
124
|
-
else:
|
125
|
-
raise TypeError(f"Unknown response type: {self.type}")
|
126
|
-
|
127
|
-
def handle_request(
|
128
|
-
self,
|
129
|
-
req_body: str,
|
130
|
-
req_headers: RequestHeaders,
|
131
|
-
res_headers: ResponseHeaders,
|
132
|
-
) -> RawResponse:
|
133
|
-
if req_body == "list":
|
134
|
-
|
135
|
-
def method_to_json(method: Method) -> Any:
|
136
|
-
return {
|
137
|
-
"method": method.name,
|
138
|
-
"number": method.number,
|
139
|
-
"request": method.request_serializer.type_descriptor.as_json(),
|
140
|
-
"response": method.response_serializer.type_descriptor.as_json(),
|
141
|
-
}
|
142
|
-
|
143
|
-
json_code = json.dumps(
|
144
|
-
{
|
145
|
-
"methods": [
|
146
|
-
method_to_json(method_impl.method)
|
147
|
-
for method_impl in self._number_to_method_impl.values()
|
148
|
-
]
|
149
|
-
},
|
150
|
-
indent=2,
|
151
|
-
)
|
152
|
-
return self.RawResponse(json_code, "ok-json")
|
153
|
-
|
154
|
-
parts = req_body.split(":", 3)
|
155
|
-
if len(parts) != 4:
|
156
|
-
return self.RawResponse(
|
157
|
-
"bad request: invalid request format", "bad-request"
|
158
|
-
)
|
159
|
-
method_name = parts[0]
|
160
|
-
method_number_str = parts[1]
|
161
|
-
format = parts[2]
|
162
|
-
request_data = parts[3]
|
163
|
-
try:
|
164
|
-
method_number = int(method_number_str)
|
165
|
-
except Exception:
|
166
|
-
return self.RawResponse(
|
167
|
-
"bad request: can't parse method number", "bad-request"
|
168
|
-
)
|
169
|
-
method_impl = self._number_to_method_impl.get(method_number)
|
170
|
-
if not method_impl:
|
171
|
-
return self.RawResponse(
|
172
|
-
f"bad request: method not found: {method_name}; number: {method_number}",
|
173
|
-
"bad-request",
|
174
|
-
)
|
175
|
-
|
176
|
-
try:
|
177
|
-
req: Any = method_impl.method.request_serializer.from_json_code(
|
178
|
-
request_data
|
179
|
-
)
|
180
|
-
except Exception as e:
|
181
|
-
return self.RawResponse(
|
182
|
-
f"bad request: can't parse JSON: {e}", "bad-request"
|
183
|
-
)
|
184
|
-
|
185
|
-
try:
|
186
|
-
res: Any = method_impl.impl(req, req_headers, res_headers)
|
187
|
-
except Exception as e:
|
188
|
-
return self.RawResponse(f"server error: {e}", "server-error")
|
189
|
-
|
190
|
-
try:
|
191
|
-
res_json = method_impl.method.response_serializer.to_json_code(
|
192
|
-
res, readable=(format == "readable")
|
193
|
-
)
|
194
|
-
except Exception as e:
|
195
|
-
return self.RawResponse(
|
196
|
-
f"server error: can't serialize response to JSON: {e}", "server-error"
|
197
|
-
)
|
198
|
-
|
199
|
-
return self.RawResponse(res_json, "ok-json")
|
200
|
-
|
201
|
-
|
202
|
-
@dataclass(frozen=True)
|
203
|
-
class _MethodImpl(Generic[Request, Response, RequestHeaders, ResponseHeaders]):
|
204
|
-
method: Method[Request, Response]
|
205
|
-
impl: Callable[[Request, RequestHeaders, ResponseHeaders], Response]
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|