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.

@@ -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 = get_type_hints(method)['return']
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, injectable, on_init, inject_environment, on_destroy
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 .service import ComponentRegistry
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
- raw_data = await http_request.body()
354
- data = msgpack.unpackb(raw_data, raw=False)
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
- return await self.dispatch(http_request, request)
368
- else:
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
- async def dispatch(self, http_request: HttpRequest, request: dict) :
375
- ServiceManager.logger.debug("dispatch request %s", request["method"])
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
- # <comp>:<service>:<method>
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
- parts = request["method"].split(":")
416
+ result_serializer = self.protobuf_manager.create_result_serializer(response_type, method)
380
417
 
381
- #component = parts[0]
382
- service_name = parts[1]
383
- method_name = parts[2]
418
+ try:
419
+ result = await self.dispatch(service_descriptor, method, args)
384
420
 
385
- service_descriptor = ServiceManager.descriptors_by_name[service_name]
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
- method = getattr(service, method_name)
423
+ return HttpResponse(
424
+ content=result_message.SerializeToString(),
425
+ media_type="application/x-protobuf"
426
+ )
389
427
 
390
- args = self.deserialize_args(request["args"], service_descriptor.type, method)
391
- try:
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
- return Response(result=result, exception=None).model_dump()
431
+ return HttpResponse(
432
+ content=result_message.SerializeToString(),
433
+ media_type="application/x-protobuf"
434
+ )
398
435
 
399
- except HTTPException as e:
400
- raise
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
- except Exception as e:
403
- return Response(result=None, exception=str(e)).model_dump()
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 ChannelManager:
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
- ChannelManager.factories[channel] = type
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
- ChannelManager.register_channel(name, cls)
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: ChannelManager):
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.10.7
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.6.0
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,,