soia-client 1.0.20__py3-none-any.whl → 1.0.21__py3-none-any.whl
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/__init__.py +3 -3
- soia/_impl/service.py +254 -120
- {soia_client-1.0.20.dist-info → soia_client-1.0.21.dist-info}/METADATA +1 -1
- {soia_client-1.0.20.dist-info → soia_client-1.0.21.dist-info}/RECORD +7 -7
- {soia_client-1.0.20.dist-info → soia_client-1.0.21.dist-info}/WHEEL +1 -1
- {soia_client-1.0.20.dist-info → soia_client-1.0.21.dist-info}/licenses/LICENSE +0 -0
- {soia_client-1.0.20.dist-info → soia_client-1.0.21.dist-info}/top_level.txt +0 -0
soia/__init__.py
CHANGED
@@ -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",
|
soia/_impl/service.py
CHANGED
@@ -1,7 +1,8 @@
|
|
1
1
|
import inspect
|
2
2
|
import json
|
3
|
+
from collections.abc import Awaitable, Callable
|
3
4
|
from dataclasses import dataclass
|
4
|
-
from typing import Any,
|
5
|
+
from typing import Any, Generic, Literal, TypeVar, Union, cast
|
5
6
|
|
6
7
|
from soia._impl.method import Method, Request, Response
|
7
8
|
|
@@ -10,42 +11,157 @@ RequestHeaders = TypeVar("RequestHeaders")
|
|
10
11
|
ResponseHeaders = TypeVar("ResponseHeaders")
|
11
12
|
|
12
13
|
|
13
|
-
|
14
|
-
|
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
|
+
]
|
15
23
|
|
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
24
|
|
20
|
-
|
25
|
+
@dataclass(frozen=True)
|
26
|
+
class RawServiceResponse:
|
27
|
+
data: str
|
28
|
+
type: Literal["ok-json", "bad-request", "server-error"]
|
21
29
|
|
22
|
-
|
23
|
-
|
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}")
|
24
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}")
|
25
49
|
|
26
|
-
s = soia.Service[Headers, Headers]()
|
27
|
-
s.add_method(...)
|
28
|
-
s.add_method(...)
|
29
50
|
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
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,
|
45
111
|
)
|
46
|
-
|
112
|
+
return RawServiceResponse(json_code, "ok-json")
|
47
113
|
|
48
|
-
|
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
|
+
]
|
49
165
|
|
50
166
|
def __init__(self):
|
51
167
|
self._number_to_method_impl = {}
|
@@ -54,11 +170,16 @@ class Service(Generic[RequestHeaders, ResponseHeaders]):
|
|
54
170
|
self,
|
55
171
|
method: Method[Request, Response],
|
56
172
|
impl: Union[
|
173
|
+
# Sync
|
57
174
|
Callable[[Request], Response],
|
58
175
|
Callable[[Request, RequestHeaders], Response],
|
59
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]],
|
60
181
|
],
|
61
|
-
) ->
|
182
|
+
) -> None:
|
62
183
|
signature = inspect.Signature.from_callable(impl)
|
63
184
|
num_positional_params = 0
|
64
185
|
for param in signature.parameters.values():
|
@@ -97,109 +218,122 @@ class Service(Generic[RequestHeaders, ResponseHeaders]):
|
|
97
218
|
method=method,
|
98
219
|
impl=resolved_impl,
|
99
220
|
)
|
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
221
|
|
127
222
|
def handle_request(
|
128
223
|
self,
|
129
224
|
req_body: str,
|
130
225
|
req_headers: RequestHeaders,
|
131
226
|
res_headers: ResponseHeaders,
|
132
|
-
) ->
|
133
|
-
|
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()
|
134
235
|
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
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()
|
142
249
|
|
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
250
|
|
154
|
-
|
155
|
-
|
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
|
-
)
|
251
|
+
class Service(Generic[RequestHeaders, ResponseHeaders]):
|
252
|
+
"""Wraps around the implementation of a soia service on the server side.
|
175
253
|
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
)
|
180
|
-
except Exception as e:
|
181
|
-
return self.RawResponse(
|
182
|
-
f"bad request: can't parse JSON: {e}", "bad-request"
|
183
|
-
)
|
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.
|
184
257
|
|
185
|
-
|
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")
|
258
|
+
Example with Flask:
|
189
259
|
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
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,
|
197
283
|
)
|
284
|
+
"""
|
198
285
|
|
199
|
-
|
286
|
+
_impl: _ServiceImpl[RequestHeaders, ResponseHeaders]
|
200
287
|
|
288
|
+
def __init__(self):
|
289
|
+
self._impl = _ServiceImpl[RequestHeaders, ResponseHeaders]()
|
201
290
|
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
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)
|
@@ -1,4 +1,4 @@
|
|
1
|
-
soia/__init__.py,sha256=
|
1
|
+
soia/__init__.py,sha256=GFsFZjzbElOC3AJHoaiI_eb3SRXu0-ooE0QXusL-O1k,724
|
2
2
|
soia/_module_initializer.py,sha256=1TWnc0qUO7_iljQR2ywVERrrkY5jIkT3zucc-AYZyrQ,4221
|
3
3
|
soia/_spec.py,sha256=Y5EHHQa6qNeJc29aaqGrFPnPFXxlL7TED9_AXUGBjf0,3663
|
4
4
|
soia/reflection.py,sha256=U5knJGmawARCdcEhNxek4dvx48WLPETLqIqKBPWwT4Q,8771
|
@@ -14,13 +14,13 @@ soia/_impl/primitives.py,sha256=Xk26Fv4oQG2oXd3tS_2sAnJYQdXYX9nva09713AcJvs,8940
|
|
14
14
|
soia/_impl/repr.py,sha256=7WX0bEAVENTjlyZIcbT8TcJylS7IRIyafGCmqaIMxFM,1413
|
15
15
|
soia/_impl/serializer.py,sha256=28IwkjtUnLpbnPQfVNfJXkApCK4JhXHwLkC5MVhF8xo,3529
|
16
16
|
soia/_impl/serializers.py,sha256=IL9jHHMo11pgrL1-crarOEElvTyV5YM6FTcgumjW6IU,2564
|
17
|
-
soia/_impl/service.py,sha256=
|
17
|
+
soia/_impl/service.py,sha256=PsV286BYMoJpXIjeBc__MHHakcqof0Pbb3B_Zha1PZI,11928
|
18
18
|
soia/_impl/service_client.py,sha256=qDntwRyXfLsmXl4ELfOkh-fgv553nrGy72K0JghLM80,2734
|
19
19
|
soia/_impl/structs.py,sha256=YTc3Ykj2TxPquar2XsP2DhFfkfIoELXOveyd8yTqN90,26545
|
20
20
|
soia/_impl/timestamp.py,sha256=lXBNH8mPmzflkNjSKZSBl2XS-ot9N8N92B_zGO2SMtU,4078
|
21
21
|
soia/_impl/type_adapter.py,sha256=RyIyh4Fnt9rMy0HRzC-a2v2JAdZsV9FBzoGEUVygVRE,2101
|
22
|
-
soia_client-1.0.
|
23
|
-
soia_client-1.0.
|
24
|
-
soia_client-1.0.
|
25
|
-
soia_client-1.0.
|
26
|
-
soia_client-1.0.
|
22
|
+
soia_client-1.0.21.dist-info/licenses/LICENSE,sha256=SaAftKkX6hfSOiPdENQPS70tifH3PDHgazq8eK2Pwfw,1064
|
23
|
+
soia_client-1.0.21.dist-info/METADATA,sha256=Ru0EEbE4aLijjzYSjeaCuC3WImJuTYL0HfxpqaL8KFc,2122
|
24
|
+
soia_client-1.0.21.dist-info/WHEEL,sha256=0CuiUZ_p9E4cD6NyLD6UG80LBXYyiSYZOKDm5lp32xk,91
|
25
|
+
soia_client-1.0.21.dist-info/top_level.txt,sha256=lsYG9JrvauFe1oIV5zvnwsS9hsx3ztwfK_937op9mxc,5
|
26
|
+
soia_client-1.0.21.dist-info/RECORD,,
|
File without changes
|
File without changes
|