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 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 RequestHeaders, ResponseHeaders, Service
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
- "RequestHeaders",
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, Callable, Generic, Literal, TypeVar, Union, cast
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
- class Service(Generic[RequestHeaders, ResponseHeaders]):
14
- """Wraps around the implementation of a soia service on the server side.
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
- Example with Flask:
25
+ @dataclass(frozen=True)
26
+ class RawServiceResponse:
27
+ data: str
28
+ type: Literal["ok-json", "bad-request", "server-error"]
21
29
 
22
- from flask import Response, request
23
- from werkzeug.datastructures import Headers
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
- @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,
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
- _number_to_method_impl: dict[int, "_MethodImpl"]
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
- ) -> "Service":
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
- ) -> RawResponse:
133
- if req_body == "list":
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
- 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
- }
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
- 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
- )
251
+ class Service(Generic[RequestHeaders, ResponseHeaders]):
252
+ """Wraps around the implementation of a soia service on the server side.
175
253
 
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
- )
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
- 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")
258
+ Example with Flask:
189
259
 
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"
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
- return self.RawResponse(res_json, "ok-json")
286
+ _impl: _ServiceImpl[RequestHeaders, ResponseHeaders]
200
287
 
288
+ def __init__(self):
289
+ self._impl = _ServiceImpl[RequestHeaders, ResponseHeaders]()
201
290
 
202
- @dataclass(frozen=True)
203
- class _MethodImpl(Generic[Request, Response, RequestHeaders, ResponseHeaders]):
204
- method: Method[Request, Response]
205
- impl: Callable[[Request, RequestHeaders, ResponseHeaders], Response]
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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: soia-client
3
- Version: 1.0.20
3
+ Version: 1.0.21
4
4
  Author-email: Tyler Fibonacci <gepheum@gmail.com>
5
5
  License: MIT License
6
6
 
@@ -1,4 +1,4 @@
1
- soia/__init__.py,sha256=mI-PNb0RHa0mq4m0e6NKIcQsR8EFkTeKHzh6bVbnRYg,722
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=GH0vujMDaqRpX8bNQATUPdzJ1V_fjt4eHGCBAjLjlaY,7229
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.20.dist-info/licenses/LICENSE,sha256=SaAftKkX6hfSOiPdENQPS70tifH3PDHgazq8eK2Pwfw,1064
23
- soia_client-1.0.20.dist-info/METADATA,sha256=7HocgiYValsPhLrZnucq5U1m-AIF3aRBQn5RZjXLl0o,2122
24
- soia_client-1.0.20.dist-info/WHEEL,sha256=GHB6lJx2juba1wDgXDNlMTyM13ckjBMKf-OnwgKOCtA,91
25
- soia_client-1.0.20.dist-info/top_level.txt,sha256=lsYG9JrvauFe1oIV5zvnwsS9hsx3ztwfK_937op9mxc,5
26
- soia_client-1.0.20.dist-info/RECORD,,
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,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.3.0)
2
+ Generator: setuptools (80.3.1)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5