aspyx-service 0.10.7__py3-none-any.whl → 0.11.0__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.
Potentially problematic release.
This version of aspyx-service might be problematic. Click here for more details.
- aspyx_service/__init__.py +5 -0
- aspyx_service/channels.py +30 -3
- aspyx_service/healthcheck.py +2 -2
- aspyx_service/protobuf.py +1083 -0
- aspyx_service/restchannel.py +23 -3
- aspyx_service/server.py +82 -32
- aspyx_service/service.py +9 -4
- {aspyx_service-0.10.7.dist-info → aspyx_service-0.11.0.dist-info}/METADATA +3 -2
- aspyx_service-0.11.0.dist-info/RECORD +14 -0
- aspyx_service-0.10.7.dist-info/RECORD +0 -13
- {aspyx_service-0.10.7.dist-info → aspyx_service-0.11.0.dist-info}/WHEEL +0 -0
- {aspyx_service-0.10.7.dist-info → aspyx_service-0.11.0.dist-info}/licenses/LICENSE +0 -0
aspyx_service/restchannel.py
CHANGED
|
@@ -9,6 +9,7 @@ from typing import get_type_hints, TypeVar, Annotated, Callable, get_origin, get
|
|
|
9
9
|
from pydantic import BaseModel
|
|
10
10
|
|
|
11
11
|
from aspyx.reflection import DynamicProxy, Decorators
|
|
12
|
+
from aspyx.util import get_serializer
|
|
12
13
|
|
|
13
14
|
from .channels import HTTPXChannel
|
|
14
15
|
from .service import channel, ServiceCommunicationException
|
|
@@ -130,9 +131,26 @@ class RestChannel(HTTPXChannel):
|
|
|
130
131
|
# local class
|
|
131
132
|
|
|
132
133
|
class Call:
|
|
134
|
+
# slots
|
|
135
|
+
|
|
136
|
+
__slots__ = [
|
|
137
|
+
"type",
|
|
138
|
+
"url_template",
|
|
139
|
+
"path_param_names",
|
|
140
|
+
"body_param_name",
|
|
141
|
+
"query_param_names",
|
|
142
|
+
"return_type",
|
|
143
|
+
"signature",
|
|
144
|
+
"body_serializer"
|
|
145
|
+
]
|
|
146
|
+
|
|
147
|
+
# constructor
|
|
148
|
+
|
|
133
149
|
def __init__(self, type: Type, method : Callable):
|
|
134
150
|
self.signature = inspect.signature(method)
|
|
135
151
|
|
|
152
|
+
type_hints = get_type_hints(method)
|
|
153
|
+
|
|
136
154
|
param_names = list(self.signature.parameters.keys())
|
|
137
155
|
param_names.remove("self")
|
|
138
156
|
|
|
@@ -171,6 +189,7 @@ class RestChannel(HTTPXChannel):
|
|
|
171
189
|
|
|
172
190
|
if BodyMarker in metadata:
|
|
173
191
|
self.body_param_name = param_name
|
|
192
|
+
self.body_serializer = get_serializer(type_hints[param_name])
|
|
174
193
|
param_names.remove(param_name)
|
|
175
194
|
elif QueryParamMarker in metadata:
|
|
176
195
|
self.query_param_names.add(param_name)
|
|
@@ -197,6 +216,7 @@ class RestChannel(HTTPXChannel):
|
|
|
197
216
|
or is_dataclass(typ)
|
|
198
217
|
):
|
|
199
218
|
self.body_param_name = name
|
|
219
|
+
self.body_serializer = get_serializer(type_hints[name])
|
|
200
220
|
param_names.remove(name)
|
|
201
221
|
break
|
|
202
222
|
|
|
@@ -207,7 +227,7 @@ class RestChannel(HTTPXChannel):
|
|
|
207
227
|
|
|
208
228
|
# return type
|
|
209
229
|
|
|
210
|
-
self.return_type =
|
|
230
|
+
self.return_type = type_hints['return']
|
|
211
231
|
|
|
212
232
|
# constructor
|
|
213
233
|
|
|
@@ -241,7 +261,7 @@ class RestChannel(HTTPXChannel):
|
|
|
241
261
|
query_params = {k: arguments[k] for k in call.query_param_names if k in arguments}
|
|
242
262
|
body = {}
|
|
243
263
|
if call.body_param_name is not None:
|
|
244
|
-
body = self.to_dict(arguments.get(call.body_param_name))
|
|
264
|
+
body = call.body_serializer(arguments.get(call.body_param_name))#self.to_dict(arguments.get(call.body_param_name))
|
|
245
265
|
|
|
246
266
|
# call
|
|
247
267
|
|
|
@@ -273,7 +293,7 @@ class RestChannel(HTTPXChannel):
|
|
|
273
293
|
query_params = {k: arguments[k] for k in call.query_param_names if k in arguments}
|
|
274
294
|
body = {}
|
|
275
295
|
if call.body_param_name is not None:
|
|
276
|
-
body = self.to_dict(arguments.get(call.body_param_name))
|
|
296
|
+
body = call.body_serializer(arguments.get(call.body_param_name))#self.to_dict(arguments.get(call.body_param_name))
|
|
277
297
|
|
|
278
298
|
# call
|
|
279
299
|
|
aspyx_service/server.py
CHANGED
|
@@ -19,11 +19,12 @@ from fastapi import FastAPI, APIRouter, Request as HttpRequest, Response as Http
|
|
|
19
19
|
from fastapi.responses import JSONResponse
|
|
20
20
|
from starlette.middleware.base import BaseHTTPMiddleware
|
|
21
21
|
|
|
22
|
-
from aspyx.di import Environment,
|
|
22
|
+
from aspyx.di import Environment, on_init, inject_environment, on_destroy
|
|
23
23
|
from aspyx.reflection import TypeDescriptor, Decorators
|
|
24
24
|
from aspyx.util import get_deserializer, get_serializer
|
|
25
25
|
|
|
26
|
-
from .
|
|
26
|
+
from .protobuf import ProtobufManager
|
|
27
|
+
from .service import ComponentRegistry, ServiceDescriptor
|
|
27
28
|
from .healthcheck import HealthCheckManager
|
|
28
29
|
|
|
29
30
|
from .service import Server, ServiceManager
|
|
@@ -172,10 +173,11 @@ class FastAPIServer(Server):
|
|
|
172
173
|
|
|
173
174
|
# constructor
|
|
174
175
|
|
|
175
|
-
def __init__(self, fast_api: FastAPI, service_manager: ServiceManager, component_registry: ComponentRegistry):
|
|
176
|
+
def __init__(self, fast_api: FastAPI, service_manager: ServiceManager, component_registry: ComponentRegistry, protobuf_manager: ProtobufManager):
|
|
176
177
|
super().__init__()
|
|
177
178
|
|
|
178
179
|
self.environment : Optional[Environment] = None
|
|
180
|
+
self.protobuf_manager = protobuf_manager
|
|
179
181
|
self.service_manager = service_manager
|
|
180
182
|
self.component_registry = component_registry
|
|
181
183
|
|
|
@@ -213,6 +215,7 @@ class FastAPIServer(Server):
|
|
|
213
215
|
self.add_routes()
|
|
214
216
|
self.fast_api.include_router(self.router)
|
|
215
217
|
|
|
218
|
+
# TODO: trace routes
|
|
216
219
|
#for route in self.fast_api.routes:
|
|
217
220
|
# print(f"{route.name}: {route.path} [{route.methods}]")
|
|
218
221
|
|
|
@@ -344,16 +347,51 @@ class FastAPIServer(Server):
|
|
|
344
347
|
|
|
345
348
|
return args
|
|
346
349
|
|
|
350
|
+
def get_descriptor_and_method(self, method_name: str) -> typing.Tuple[ServiceDescriptor, Callable]:
|
|
351
|
+
parts = method_name.split(":")
|
|
352
|
+
|
|
353
|
+
# component = parts[0]
|
|
354
|
+
service_name = parts[1]
|
|
355
|
+
method_name = parts[2]
|
|
356
|
+
|
|
357
|
+
service_descriptor : ServiceDescriptor = typing.cast(ServiceDescriptor, ServiceManager.descriptors_by_name[service_name])
|
|
358
|
+
service = self.service_manager.get_service(service_descriptor.type, preferred_channel="local")
|
|
359
|
+
|
|
360
|
+
return service_descriptor, getattr(service, method_name)
|
|
361
|
+
|
|
347
362
|
async def invoke(self, http_request: HttpRequest):
|
|
348
363
|
content_type = http_request.headers.get("content-type", "")
|
|
364
|
+
response_name = ""
|
|
365
|
+
|
|
366
|
+
service_descriptor : ServiceDescriptor
|
|
367
|
+
method : Callable
|
|
368
|
+
args : list[Any]
|
|
349
369
|
|
|
350
370
|
content = "json"
|
|
351
371
|
if "application/msgpack" in content_type:
|
|
352
372
|
content = "msgpack"
|
|
353
|
-
|
|
354
|
-
|
|
373
|
+
data = msgpack.unpackb(await http_request.body(), raw=False)
|
|
374
|
+
service_descriptor, method = self.get_descriptor_and_method(data["method"])
|
|
375
|
+
args = self.deserialize_args(data["args"], service_descriptor.type, method)
|
|
376
|
+
|
|
355
377
|
elif "application/json" in content_type:
|
|
356
378
|
data = await http_request.json()
|
|
379
|
+
service_descriptor, method = self.get_descriptor_and_method(data["method"])
|
|
380
|
+
args = self.deserialize_args(data["args"], service_descriptor.type, method)
|
|
381
|
+
|
|
382
|
+
elif "application/x-protobuf" in content_type:
|
|
383
|
+
content = "protobuf"
|
|
384
|
+
service_descriptor, method = self.get_descriptor_and_method(http_request.headers.get("x-rpc-method") )
|
|
385
|
+
self.protobuf_manager.check(service_descriptor.type)
|
|
386
|
+
data = await http_request.body()
|
|
387
|
+
|
|
388
|
+
request_name = ProtobufManager.get_message_name(service_descriptor.type, f"{method.__name__}Request")
|
|
389
|
+
|
|
390
|
+
request = self.protobuf_manager.get_message_type(request_name)()
|
|
391
|
+
request.ParseFromString(data)
|
|
392
|
+
|
|
393
|
+
args = self.protobuf_manager.create_deserializer(request.DESCRIPTOR, method).deserialize(request)
|
|
394
|
+
|
|
357
395
|
else:
|
|
358
396
|
return HttpResponse(
|
|
359
397
|
content="Unsupported Content-Type",
|
|
@@ -364,43 +402,55 @@ class FastAPIServer(Server):
|
|
|
364
402
|
request = data
|
|
365
403
|
|
|
366
404
|
if content == "json":
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
return HttpResponse(
|
|
370
|
-
content=msgpack.packb(await self.dispatch(http_request, request), use_bin_type=True),
|
|
371
|
-
media_type="application/msgpack"
|
|
372
|
-
)
|
|
405
|
+
try:
|
|
406
|
+
result = await self.dispatch(service_descriptor, method, args)
|
|
373
407
|
|
|
374
|
-
|
|
375
|
-
|
|
408
|
+
return Response(result=result, exception=None).model_dump()
|
|
409
|
+
except Exception as e:
|
|
410
|
+
return Response(result=None, exception=str(e)).model_dump()
|
|
376
411
|
|
|
377
|
-
|
|
412
|
+
elif content == "protobuf":
|
|
413
|
+
response_name = ProtobufManager.get_message_name(service_descriptor.type, f"{method.__name__}Response")
|
|
414
|
+
response_type = self.protobuf_manager.get_message_type(response_name)
|
|
378
415
|
|
|
379
|
-
|
|
416
|
+
result_serializer = self.protobuf_manager.create_result_serializer(response_type, method)
|
|
380
417
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
method_name = parts[2]
|
|
418
|
+
try:
|
|
419
|
+
result = await self.dispatch(service_descriptor, method, args)
|
|
384
420
|
|
|
385
|
-
|
|
386
|
-
service = self.service_manager.get_service(service_descriptor.type, preferred_channel="local")
|
|
421
|
+
result_message = result_serializer.serialize_result(result, None)
|
|
387
422
|
|
|
388
|
-
|
|
423
|
+
return HttpResponse(
|
|
424
|
+
content=result_message.SerializeToString(),
|
|
425
|
+
media_type="application/x-protobuf"
|
|
426
|
+
)
|
|
389
427
|
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
if inspect.iscoroutinefunction(method):
|
|
393
|
-
result = await method(*args)
|
|
394
|
-
else:
|
|
395
|
-
result = method(*args)
|
|
428
|
+
except Exception as e:
|
|
429
|
+
result_message = result_serializer.serialize_result(None, str(e))
|
|
396
430
|
|
|
397
|
-
|
|
431
|
+
return HttpResponse(
|
|
432
|
+
content=result_message.SerializeToString(),
|
|
433
|
+
media_type="application/x-protobuf"
|
|
434
|
+
)
|
|
398
435
|
|
|
399
|
-
|
|
400
|
-
|
|
436
|
+
else:
|
|
437
|
+
try:
|
|
438
|
+
response = Response(result=await self.dispatch(service_descriptor, method, args), exception=None).model_dump()
|
|
439
|
+
except Exception as e:
|
|
440
|
+
response = Response(result=None, exception=str(e)).model_dump()
|
|
401
441
|
|
|
402
|
-
|
|
403
|
-
|
|
442
|
+
return HttpResponse(
|
|
443
|
+
content=msgpack.packb(response, use_bin_type=True),
|
|
444
|
+
media_type="application/msgpack"
|
|
445
|
+
)
|
|
446
|
+
|
|
447
|
+
async def dispatch(self, service_descriptor: ServiceDescriptor, method: Callable, args: list[Any]) :
|
|
448
|
+
ServiceManager.logger.debug("dispatch request %s.%s", service_descriptor, method.__name__)
|
|
449
|
+
|
|
450
|
+
if inspect.iscoroutinefunction(method):
|
|
451
|
+
return await method(*args)
|
|
452
|
+
else:
|
|
453
|
+
return method(*args)
|
|
404
454
|
|
|
405
455
|
# override
|
|
406
456
|
|
aspyx_service/service.py
CHANGED
|
@@ -18,6 +18,7 @@ from aspyx.di import injectable, Environment, Providers, ClassInstanceProvider,
|
|
|
18
18
|
from aspyx.di.aop.aop import ClassAspectTarget
|
|
19
19
|
from aspyx.reflection import Decorators, DynamicProxy, DecoratorDescriptor, TypeDescriptor
|
|
20
20
|
from aspyx.util import StringBuilder
|
|
21
|
+
|
|
21
22
|
from .healthcheck import HealthCheckManager, HealthStatus
|
|
22
23
|
|
|
23
24
|
T = TypeVar("T")
|
|
@@ -557,7 +558,7 @@ class ComponentRegistry:
|
|
|
557
558
|
|
|
558
559
|
|
|
559
560
|
@injectable()
|
|
560
|
-
class
|
|
561
|
+
class ChannelFactory:
|
|
561
562
|
"""
|
|
562
563
|
Internal factory for channels.
|
|
563
564
|
"""
|
|
@@ -567,7 +568,7 @@ class ChannelManager:
|
|
|
567
568
|
def register_channel(cls, channel: str, type: Type):
|
|
568
569
|
ServiceManager.logger.info("register channel %s", channel)
|
|
569
570
|
|
|
570
|
-
|
|
571
|
+
ChannelFactory.factories[channel] = type
|
|
571
572
|
|
|
572
573
|
# constructor
|
|
573
574
|
|
|
@@ -603,7 +604,7 @@ def channel(name: str):
|
|
|
603
604
|
|
|
604
605
|
Providers.register(ClassInstanceProvider(cls, False, "request"))
|
|
605
606
|
|
|
606
|
-
|
|
607
|
+
ChannelFactory.register_channel(name, cls)
|
|
607
608
|
|
|
608
609
|
return cls
|
|
609
610
|
|
|
@@ -649,18 +650,22 @@ class ServiceManager:
|
|
|
649
650
|
def register_component(cls, component_type: Type, services: list[Type]):
|
|
650
651
|
component_descriptor = ComponentDescriptor(component_type, services)
|
|
651
652
|
|
|
653
|
+
setattr(component_type, "__descriptor__", component_descriptor)
|
|
654
|
+
|
|
652
655
|
cls.logger.info("register component %s", component_descriptor.name)
|
|
653
656
|
|
|
654
657
|
ServiceManager.descriptors[component_type] = component_descriptor
|
|
655
658
|
ServiceManager.descriptors_by_name[component_descriptor.name] = component_descriptor
|
|
656
659
|
|
|
657
660
|
for component_service in component_descriptor.services:
|
|
661
|
+
setattr(component_service.type, "__descriptor__", component_service)
|
|
662
|
+
|
|
658
663
|
ServiceManager.descriptors[component_service.type] = component_service
|
|
659
664
|
ServiceManager.descriptors_by_name[component_service.name] = component_service
|
|
660
665
|
|
|
661
666
|
# constructor
|
|
662
667
|
|
|
663
|
-
def __init__(self, component_registry: ComponentRegistry, channel_manager:
|
|
668
|
+
def __init__(self, component_registry: ComponentRegistry, channel_manager: ChannelFactory):
|
|
664
669
|
self.component_registry = component_registry
|
|
665
670
|
self.channel_manager = channel_manager
|
|
666
671
|
self.environment : Optional[Environment] = None
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: aspyx_service
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.11.0
|
|
4
4
|
Summary: Aspyx Service framework
|
|
5
5
|
Author-email: Andreas Ernst <andreas.ernst7@gmail.com>
|
|
6
6
|
License: MIT License
|
|
@@ -26,10 +26,11 @@ License: MIT License
|
|
|
26
26
|
SOFTWARE.
|
|
27
27
|
License-File: LICENSE
|
|
28
28
|
Requires-Python: >=3.9
|
|
29
|
-
Requires-Dist: aspyx>=1.
|
|
29
|
+
Requires-Dist: aspyx>=1.7.0
|
|
30
30
|
Requires-Dist: fastapi~=0.115.13
|
|
31
31
|
Requires-Dist: httpx~=0.28.1
|
|
32
32
|
Requires-Dist: msgpack~=1.1.1
|
|
33
|
+
Requires-Dist: protobuf~=5.29.4
|
|
33
34
|
Requires-Dist: python-consul2~=0.1.5
|
|
34
35
|
Requires-Dist: uvicorn[standard]
|
|
35
36
|
Description-Content-Type: text/markdown
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
aspyx_service/__init__.py,sha256=Mzt6pBhME_qDij2timEZT0emTTXRues_xXu3muhk3Jc,2642
|
|
2
|
+
aspyx_service/authorization.py,sha256=0B1xb0WrRaj2rcGTHVUhh6i8aA0sy7BmpYA18xI9LQA,3833
|
|
3
|
+
aspyx_service/channels.py,sha256=6R9WeC8JO-TI2FPtWBm7ad-jivn-gmkHoB7z6xosw6A,17423
|
|
4
|
+
aspyx_service/healthcheck.py,sha256=XiQx1T0DP0kcCyK_sYBuE-JHs5N285HotLVycFCgzBU,5612
|
|
5
|
+
aspyx_service/protobuf.py,sha256=OTOPRxpzPZEK0lX2BIH3R-FNnxiBtsWvypuyzMla5bU,39010
|
|
6
|
+
aspyx_service/registries.py,sha256=bnTjKb40fbZXA52E2lDSEzCWI5_NBKZzQjc8ffufB5g,8039
|
|
7
|
+
aspyx_service/restchannel.py,sha256=aCpNCvv1eIRa9IJW8Os4bQQErZKrEeyID65QhnD9QJw,9134
|
|
8
|
+
aspyx_service/server.py,sha256=dl3UWawQRPNy7h9WzHQge3dF8nG_3oYWBVgjfN-iqQg,15891
|
|
9
|
+
aspyx_service/service.py,sha256=0sFBpjRxEtMjRhwI1f51qsUCcTzbqoMx7gA3gZNge7A,27159
|
|
10
|
+
aspyx_service/session.py,sha256=HjGpnmwdislc8Ur6pQbSMi2K-lvTsb9_XyO80zupiF8,3713
|
|
11
|
+
aspyx_service-0.11.0.dist-info/METADATA,sha256=ff-Pa-1neQhtVGBX4ByuVLQ8ujH54AjND7unm1H8F2U,17978
|
|
12
|
+
aspyx_service-0.11.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
13
|
+
aspyx_service-0.11.0.dist-info/licenses/LICENSE,sha256=n4jfx_MNj7cBtPhhI7MCoB_K35cj1icP9yJ4Rh4vlvY,1070
|
|
14
|
+
aspyx_service-0.11.0.dist-info/RECORD,,
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
aspyx_service/__init__.py,sha256=OWJoScdDVK1NTs9cIgImShgEdJb8TZHLBQjS11rA0Yo,2564
|
|
2
|
-
aspyx_service/authorization.py,sha256=0B1xb0WrRaj2rcGTHVUhh6i8aA0sy7BmpYA18xI9LQA,3833
|
|
3
|
-
aspyx_service/channels.py,sha256=u1afqUfcmVgLxXDXC2BYHH-dMozLcmVROuPRpypSwr8,16397
|
|
4
|
-
aspyx_service/healthcheck.py,sha256=vjfY7s5kd5mRJynVpvAJ4BvVF7QY1xrvj94Y-m041LQ,5615
|
|
5
|
-
aspyx_service/registries.py,sha256=bnTjKb40fbZXA52E2lDSEzCWI5_NBKZzQjc8ffufB5g,8039
|
|
6
|
-
aspyx_service/restchannel.py,sha256=0Xb8grEE8Dyx3g3ENl78DDMKa2WGjIKIPgOrpw5p9ak,8470
|
|
7
|
-
aspyx_service/server.py,sha256=75B1l__GknjwKX2jGIoR4YUTaaFBEVQ_qfHW2a3cy4U,13354
|
|
8
|
-
aspyx_service/service.py,sha256=drETAZasbYJZisnmbhAqW0-mHghJ3IWyPaU-7etxvBI,27003
|
|
9
|
-
aspyx_service/session.py,sha256=HjGpnmwdislc8Ur6pQbSMi2K-lvTsb9_XyO80zupiF8,3713
|
|
10
|
-
aspyx_service-0.10.7.dist-info/METADATA,sha256=mZ2ZsGGqncY4y4pGpeaOy-cDIQu7OHwRWjW09T1jzwM,17946
|
|
11
|
-
aspyx_service-0.10.7.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
12
|
-
aspyx_service-0.10.7.dist-info/licenses/LICENSE,sha256=n4jfx_MNj7cBtPhhI7MCoB_K35cj1icP9yJ4Rh4vlvY,1070
|
|
13
|
-
aspyx_service-0.10.7.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|