aspyx-service 0.10.7__py3-none-any.whl → 0.11.1__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 +24 -60
- aspyx_service/healthcheck.py +2 -2
- aspyx_service/protobuf.py +1093 -0
- aspyx_service/restchannel.py +23 -3
- aspyx_service/server.py +97 -54
- aspyx_service/service.py +37 -14
- {aspyx_service-0.10.7.dist-info → aspyx_service-0.11.1.dist-info}/METADATA +21 -31
- aspyx_service-0.11.1.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.1.dist-info}/WHEEL +0 -0
- {aspyx_service-0.10.7.dist-info → aspyx_service-0.11.1.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
|
@@ -14,16 +14,17 @@ import msgpack
|
|
|
14
14
|
import uvicorn
|
|
15
15
|
|
|
16
16
|
from fastapi import FastAPI, APIRouter, Request as HttpRequest, Response as HttpResponse, HTTPException
|
|
17
|
-
|
|
17
|
+
from fastapi.datastructures import DefaultPlaceholder, Default
|
|
18
18
|
|
|
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
|
-
from aspyx.util import get_deserializer, get_serializer
|
|
24
|
+
from aspyx.util import get_deserializer, get_serializer, CopyOnWriteCache
|
|
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
|
|
|
@@ -190,7 +192,7 @@ class FastAPIServer(Server):
|
|
|
190
192
|
|
|
191
193
|
# cache
|
|
192
194
|
|
|
193
|
-
self.deserializers
|
|
195
|
+
self.deserializers = CopyOnWriteCache[str, list[Callable]]()
|
|
194
196
|
|
|
195
197
|
# that's the overall dispatcher
|
|
196
198
|
|
|
@@ -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
|
|
|
@@ -325,18 +328,16 @@ class FastAPIServer(Server):
|
|
|
325
328
|
self.thread.start()
|
|
326
329
|
|
|
327
330
|
def get_deserializers(self, service: Type, method):
|
|
328
|
-
deserializers = self.deserializers.get(method
|
|
331
|
+
deserializers = self.deserializers.get(method)
|
|
329
332
|
if deserializers is None:
|
|
330
333
|
descriptor = TypeDescriptor.for_type(service).get_method(method.__name__)
|
|
331
334
|
|
|
332
335
|
deserializers = [get_deserializer(type) for type in descriptor.param_types]
|
|
333
|
-
self.deserializers
|
|
336
|
+
self.deserializers.put(method, deserializers)
|
|
334
337
|
|
|
335
338
|
return deserializers
|
|
336
339
|
|
|
337
340
|
def deserialize_args(self, args: list[Any], type: Type, method: Callable) -> list:
|
|
338
|
-
#args = list(request.args)
|
|
339
|
-
|
|
340
341
|
deserializers = self.get_deserializers(type, method)
|
|
341
342
|
|
|
342
343
|
for i, arg in enumerate(args):
|
|
@@ -344,68 +345,110 @@ class FastAPIServer(Server):
|
|
|
344
345
|
|
|
345
346
|
return args
|
|
346
347
|
|
|
347
|
-
|
|
348
|
-
|
|
348
|
+
def get_descriptor_and_method(self, method_name: str) -> typing.Tuple[ServiceDescriptor, Callable]:
|
|
349
|
+
parts = method_name.split(":")
|
|
349
350
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
raw_data = await http_request.body()
|
|
354
|
-
data = msgpack.unpackb(raw_data, raw=False)
|
|
355
|
-
elif "application/json" in content_type:
|
|
356
|
-
data = await http_request.json()
|
|
357
|
-
else:
|
|
358
|
-
return HttpResponse(
|
|
359
|
-
content="Unsupported Content-Type",
|
|
360
|
-
status_code=415,
|
|
361
|
-
media_type="text/plain"
|
|
362
|
-
)
|
|
351
|
+
# component = parts[0]
|
|
352
|
+
service_name = parts[1]
|
|
353
|
+
method_name = parts[2]
|
|
363
354
|
|
|
364
|
-
|
|
355
|
+
service_descriptor = typing.cast(ServiceDescriptor, ServiceManager.descriptors_by_name[service_name])
|
|
356
|
+
service = self.service_manager.get_service(service_descriptor.type, preferred_channel="local")
|
|
365
357
|
|
|
366
|
-
|
|
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
|
-
)
|
|
358
|
+
return service_descriptor, getattr(service, method_name)
|
|
373
359
|
|
|
374
|
-
async def
|
|
375
|
-
|
|
360
|
+
async def invoke_json(self, http_request: HttpRequest):
|
|
361
|
+
data = await http_request.json()
|
|
362
|
+
service_descriptor, method = self.get_descriptor_and_method(data["method"])
|
|
363
|
+
args = self.deserialize_args(data["args"], service_descriptor.type, method)
|
|
376
364
|
|
|
377
|
-
|
|
365
|
+
try:
|
|
366
|
+
result = await self.dispatch(service_descriptor, method, args)
|
|
378
367
|
|
|
379
|
-
|
|
368
|
+
return Response(result=result, exception=None).model_dump()
|
|
369
|
+
except Exception as e:
|
|
370
|
+
return Response(result=None, exception=str(e)).model_dump()
|
|
380
371
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
372
|
+
async def invoke_msgpack(self, http_request: HttpRequest):
|
|
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)
|
|
384
376
|
|
|
385
|
-
|
|
386
|
-
|
|
377
|
+
try:
|
|
378
|
+
response = Response(result=await self.dispatch(service_descriptor, method, args), exception=None).model_dump()
|
|
379
|
+
except Exception as e:
|
|
380
|
+
response = Response(result=None, exception=str(e)).model_dump()
|
|
381
|
+
|
|
382
|
+
return HttpResponse(
|
|
383
|
+
content=msgpack.packb(response, use_bin_type=True),
|
|
384
|
+
media_type="application/msgpack"
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
async def invoke_protobuf(self, http_request: HttpRequest):
|
|
388
|
+
service_descriptor, method = self.get_descriptor_and_method(http_request.headers.get("x-rpc-method"))
|
|
387
389
|
|
|
388
|
-
|
|
390
|
+
data = await http_request.body()
|
|
389
391
|
|
|
390
|
-
|
|
392
|
+
# create message
|
|
393
|
+
|
|
394
|
+
request = self.protobuf_manager.get_request_message(service_descriptor.type, method)()
|
|
395
|
+
request.ParseFromString(data)
|
|
396
|
+
|
|
397
|
+
# and parse
|
|
398
|
+
|
|
399
|
+
args = self.protobuf_manager.create_deserializer(request.DESCRIPTOR, method).deserialize(request)
|
|
400
|
+
|
|
401
|
+
response_type = self.protobuf_manager.get_response_message(service_descriptor.type,method)
|
|
402
|
+
result_serializer = self.protobuf_manager.create_result_serializer(response_type, method)
|
|
391
403
|
try:
|
|
392
|
-
|
|
393
|
-
result = await method(*args)
|
|
394
|
-
else:
|
|
395
|
-
result = method(*args)
|
|
404
|
+
result = await self.dispatch(service_descriptor, method, args)
|
|
396
405
|
|
|
397
|
-
|
|
406
|
+
result_message = result_serializer.serialize_result(result, None)
|
|
398
407
|
|
|
399
|
-
|
|
400
|
-
|
|
408
|
+
return HttpResponse(
|
|
409
|
+
content=result_message.SerializeToString(),
|
|
410
|
+
media_type="application/x-protobuf"
|
|
411
|
+
)
|
|
401
412
|
|
|
402
413
|
except Exception as e:
|
|
403
|
-
|
|
414
|
+
result_message = result_serializer.serialize_result(None, str(e))
|
|
415
|
+
|
|
416
|
+
return HttpResponse(
|
|
417
|
+
content=result_message.SerializeToString(),
|
|
418
|
+
media_type="application/x-protobuf"
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
async def invoke(self, http_request: HttpRequest):
|
|
422
|
+
content_type = http_request.headers.get("content-type", "")
|
|
423
|
+
|
|
424
|
+
if content_type == "application/x-protobuf":
|
|
425
|
+
return await self.invoke_protobuf(http_request)
|
|
426
|
+
|
|
427
|
+
elif content_type == "application/msgpack":
|
|
428
|
+
return await self.invoke_msgpack(http_request)
|
|
429
|
+
|
|
430
|
+
elif content_type == "application/json":
|
|
431
|
+
return await self.invoke_json(http_request)
|
|
432
|
+
|
|
433
|
+
else:
|
|
434
|
+
return HttpResponse(
|
|
435
|
+
content="Unsupported Content-Type",
|
|
436
|
+
status_code=415,
|
|
437
|
+
media_type="text/plain"
|
|
438
|
+
)
|
|
439
|
+
|
|
440
|
+
async def dispatch(self, service_descriptor: ServiceDescriptor, method: Callable, args: list[Any]) :
|
|
441
|
+
#ServiceManager.logger.debug("dispatch request %s.%s", service_descriptor, method.__name__)
|
|
442
|
+
|
|
443
|
+
if inspect.iscoroutinefunction(method):
|
|
444
|
+
return await method(*args)
|
|
445
|
+
else:
|
|
446
|
+
return method(*args)
|
|
404
447
|
|
|
405
448
|
# override
|
|
406
449
|
|
|
407
|
-
def
|
|
408
|
-
self.router.
|
|
450
|
+
def add_route(self, path: str, endpoint: Callable, methods: list[str], response_class: typing.Union[Type[Response], DefaultPlaceholder] = Default(JSONResponse)):
|
|
451
|
+
self.router.add_api_route(path=path, endpoint=endpoint, methods=methods, response_class=response_class)
|
|
409
452
|
|
|
410
453
|
def route_health(self, url: str, callable: Callable):
|
|
411
454
|
async def get_health_response():
|
aspyx_service/service.py
CHANGED
|
@@ -7,17 +7,23 @@ import re
|
|
|
7
7
|
import socket
|
|
8
8
|
import logging
|
|
9
9
|
import threading
|
|
10
|
+
import typing
|
|
10
11
|
from abc import abstractmethod, ABC
|
|
11
12
|
from dataclasses import dataclass
|
|
12
13
|
from enum import Enum, auto
|
|
13
14
|
|
|
14
15
|
from typing import Type, TypeVar, Generic, Callable, Optional, cast
|
|
15
16
|
|
|
17
|
+
from fastapi.datastructures import DefaultPlaceholder, Default
|
|
18
|
+
from httpx import Response
|
|
19
|
+
from starlette.responses import JSONResponse, PlainTextResponse
|
|
20
|
+
|
|
16
21
|
from aspyx.di import injectable, Environment, Providers, ClassInstanceProvider, inject_environment, order, \
|
|
17
22
|
Lifecycle, LifecycleCallable, InstanceProvider
|
|
18
23
|
from aspyx.di.aop.aop import ClassAspectTarget
|
|
19
24
|
from aspyx.reflection import Decorators, DynamicProxy, DecoratorDescriptor, TypeDescriptor
|
|
20
25
|
from aspyx.util import StringBuilder
|
|
26
|
+
|
|
21
27
|
from .healthcheck import HealthCheckManager, HealthStatus
|
|
22
28
|
|
|
23
29
|
T = TypeVar("T")
|
|
@@ -41,7 +47,7 @@ class ComponentStatus(Enum):
|
|
|
41
47
|
|
|
42
48
|
class Server(ABC):
|
|
43
49
|
"""
|
|
44
|
-
A server is a central
|
|
50
|
+
A server is a central class that boots a main module and initializes the ServiceManager.
|
|
45
51
|
It also is the place where http servers get initialized.
|
|
46
52
|
"""
|
|
47
53
|
port = 0
|
|
@@ -50,6 +56,7 @@ class Server(ABC):
|
|
|
50
56
|
|
|
51
57
|
def __init__(self):
|
|
52
58
|
self.environment : Optional[Environment] = None
|
|
59
|
+
self.instance = self
|
|
53
60
|
|
|
54
61
|
# public
|
|
55
62
|
|
|
@@ -57,7 +64,7 @@ class Server(ABC):
|
|
|
57
64
|
return self.environment.get(type)
|
|
58
65
|
|
|
59
66
|
@abstractmethod
|
|
60
|
-
def
|
|
67
|
+
def add_route(self, path : str, endpoint : Callable, methods : list[str], response_class : typing.Union[Type[Response], DefaultPlaceholder] = Default(JSONResponse)):
|
|
61
68
|
pass
|
|
62
69
|
|
|
63
70
|
@abstractmethod
|
|
@@ -557,7 +564,7 @@ class ComponentRegistry:
|
|
|
557
564
|
|
|
558
565
|
|
|
559
566
|
@injectable()
|
|
560
|
-
class
|
|
567
|
+
class ChannelFactory:
|
|
561
568
|
"""
|
|
562
569
|
Internal factory for channels.
|
|
563
570
|
"""
|
|
@@ -567,12 +574,12 @@ class ChannelManager:
|
|
|
567
574
|
def register_channel(cls, channel: str, type: Type):
|
|
568
575
|
ServiceManager.logger.info("register channel %s", channel)
|
|
569
576
|
|
|
570
|
-
|
|
577
|
+
ChannelFactory.factories[channel] = type
|
|
571
578
|
|
|
572
579
|
# constructor
|
|
573
580
|
|
|
574
581
|
def __init__(self):
|
|
575
|
-
self.environment = None
|
|
582
|
+
self.environment : Optional[Environment] = None
|
|
576
583
|
|
|
577
584
|
# lifecycle hooks
|
|
578
585
|
|
|
@@ -582,6 +589,12 @@ class ChannelManager:
|
|
|
582
589
|
|
|
583
590
|
# public
|
|
584
591
|
|
|
592
|
+
def prepare_channel(self, server: Server, channel: str, component_descriptor: ComponentDescriptor):
|
|
593
|
+
type = self.factories[channel]
|
|
594
|
+
|
|
595
|
+
if getattr(type, "prepare", None) is not None:
|
|
596
|
+
getattr(type, "prepare", None)(server, component_descriptor)
|
|
597
|
+
|
|
585
598
|
def make(self, name: str, descriptor: ComponentDescriptor, address: ChannelInstances) -> Channel:
|
|
586
599
|
ServiceManager.logger.info("create channel %s: %s", name, self.factories.get(name).__name__)
|
|
587
600
|
|
|
@@ -603,7 +616,7 @@ def channel(name: str):
|
|
|
603
616
|
|
|
604
617
|
Providers.register(ClassInstanceProvider(cls, False, "request"))
|
|
605
618
|
|
|
606
|
-
|
|
619
|
+
ChannelFactory.register_channel(name, cls)
|
|
607
620
|
|
|
608
621
|
return cls
|
|
609
622
|
|
|
@@ -649,20 +662,24 @@ class ServiceManager:
|
|
|
649
662
|
def register_component(cls, component_type: Type, services: list[Type]):
|
|
650
663
|
component_descriptor = ComponentDescriptor(component_type, services)
|
|
651
664
|
|
|
665
|
+
setattr(component_type, "__descriptor__", component_descriptor)
|
|
666
|
+
|
|
652
667
|
cls.logger.info("register component %s", component_descriptor.name)
|
|
653
668
|
|
|
654
669
|
ServiceManager.descriptors[component_type] = component_descriptor
|
|
655
670
|
ServiceManager.descriptors_by_name[component_descriptor.name] = component_descriptor
|
|
656
671
|
|
|
657
672
|
for component_service in component_descriptor.services:
|
|
673
|
+
setattr(component_service.type, "__descriptor__", component_service)
|
|
674
|
+
|
|
658
675
|
ServiceManager.descriptors[component_service.type] = component_service
|
|
659
676
|
ServiceManager.descriptors_by_name[component_service.name] = component_service
|
|
660
677
|
|
|
661
678
|
# constructor
|
|
662
679
|
|
|
663
|
-
def __init__(self, component_registry: ComponentRegistry,
|
|
680
|
+
def __init__(self, component_registry: ComponentRegistry, channel_factory: ChannelFactory):
|
|
664
681
|
self.component_registry = component_registry
|
|
665
|
-
self.
|
|
682
|
+
self.channel_factory = channel_factory
|
|
666
683
|
self.environment : Optional[Environment] = None
|
|
667
684
|
self.preferred_channel = ""
|
|
668
685
|
|
|
@@ -686,7 +703,7 @@ class ServiceManager:
|
|
|
686
703
|
def get_instance(self, type: Type[T]) -> T:
|
|
687
704
|
instance = self.instances.get(type)
|
|
688
705
|
if instance is None:
|
|
689
|
-
ServiceManager.logger.
|
|
706
|
+
ServiceManager.logger.debug("create implementation %s", type.__name__)
|
|
690
707
|
|
|
691
708
|
instance = self.environment.get(type)
|
|
692
709
|
self.instances[type] = instance
|
|
@@ -695,9 +712,16 @@ class ServiceManager:
|
|
|
695
712
|
|
|
696
713
|
# lifecycle
|
|
697
714
|
|
|
715
|
+
|
|
698
716
|
def startup(self, server: Server) -> None:
|
|
699
717
|
self.logger.info("startup on port %s", server.port)
|
|
700
718
|
|
|
719
|
+
# add some introspection endpoints
|
|
720
|
+
|
|
721
|
+
server.add_route(path="/report", endpoint=lambda: self.report(), methods=["GET"], response_class=PlainTextResponse)
|
|
722
|
+
|
|
723
|
+
# boot components
|
|
724
|
+
|
|
701
725
|
for descriptor in self.descriptors.values():
|
|
702
726
|
if descriptor.is_component():
|
|
703
727
|
# register local address
|
|
@@ -720,8 +744,6 @@ class ServiceManager:
|
|
|
720
744
|
|
|
721
745
|
self.component_registry.register(descriptor.get_component_descriptor(), [ChannelAddress("local", "")])
|
|
722
746
|
|
|
723
|
-
#health_name = next((decorator.args[0] for decorator in Decorators.get(descriptor.type) if decorator.decorator is health), None)
|
|
724
|
-
|
|
725
747
|
# startup
|
|
726
748
|
|
|
727
749
|
instance.startup()
|
|
@@ -733,9 +755,10 @@ class ServiceManager:
|
|
|
733
755
|
|
|
734
756
|
# register addresses
|
|
735
757
|
|
|
736
|
-
|
|
758
|
+
for address in descriptor.addresses:
|
|
759
|
+
self.channel_factory.prepare_channel(server, address.channel, descriptor.get_component_descriptor())
|
|
737
760
|
|
|
738
|
-
|
|
761
|
+
self.component_registry.register(descriptor.get_component_descriptor(), descriptor.addresses)
|
|
739
762
|
|
|
740
763
|
def shutdown(self):
|
|
741
764
|
self.logger.info("shutdown")
|
|
@@ -818,7 +841,7 @@ class ServiceManager:
|
|
|
818
841
|
if channel_instance is None:
|
|
819
842
|
# create channel
|
|
820
843
|
|
|
821
|
-
channel_instance = self.
|
|
844
|
+
channel_instance = self.channel_factory.make(address.channel, component_descriptor, address)
|
|
822
845
|
|
|
823
846
|
# cache
|
|
824
847
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: aspyx_service
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.11.1
|
|
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
|
|
@@ -113,9 +114,6 @@ After booting the DI infrastructure with a main module we could already call a s
|
|
|
113
114
|
```python
|
|
114
115
|
@module(imports=[ServiceModule])
|
|
115
116
|
class Module:
|
|
116
|
-
def __init__(self):
|
|
117
|
-
pass
|
|
118
|
-
|
|
119
117
|
@create()
|
|
120
118
|
def create_registry(self) -> ConsulComponentRegistry:
|
|
121
119
|
return ConsulComponentRegistry(Server.port, Consul(host="localhost", port=8500)) # a consul based registry!
|
|
@@ -136,11 +134,6 @@ As we can also host implementations, lets look at this side as well:
|
|
|
136
134
|
```python
|
|
137
135
|
@implementation()
|
|
138
136
|
class TestComponentImpl(AbstractComponent, TestComponent):
|
|
139
|
-
# constructor
|
|
140
|
-
|
|
141
|
-
def __init__(self):
|
|
142
|
-
super().__init__()
|
|
143
|
-
|
|
144
137
|
# implement Component
|
|
145
138
|
|
|
146
139
|
def get_addresses(self, port: int) -> list[ChannelAddress]:
|
|
@@ -148,9 +141,6 @@ class TestComponentImpl(AbstractComponent, TestComponent):
|
|
|
148
141
|
|
|
149
142
|
@implementation()
|
|
150
143
|
class TestServiceImpl(TestService):
|
|
151
|
-
def __init__(self):
|
|
152
|
-
pass
|
|
153
|
-
|
|
154
144
|
def hello(self, message: str) -> str:
|
|
155
145
|
return f"hello {message}"
|
|
156
146
|
```
|
|
@@ -262,8 +252,7 @@ Service implementations implement the corresponding interface and are decorated
|
|
|
262
252
|
```python
|
|
263
253
|
@implementation()
|
|
264
254
|
class TestServiceImpl(TestService):
|
|
265
|
-
|
|
266
|
-
pass
|
|
255
|
+
pass
|
|
267
256
|
```
|
|
268
257
|
|
|
269
258
|
The constructor is required since the instances are managed by the DI framework.
|
|
@@ -273,11 +262,6 @@ Component implementations derive from the interface and the abstract base class
|
|
|
273
262
|
```python
|
|
274
263
|
@implementation()
|
|
275
264
|
class TestComponentImpl(AbstractComponent, TestComponent):
|
|
276
|
-
# constructor
|
|
277
|
-
|
|
278
|
-
def __init__(self):
|
|
279
|
-
super().__init__()
|
|
280
|
-
|
|
281
265
|
# implement Component
|
|
282
266
|
|
|
283
267
|
def get_addresses(self, port: int) -> list[ChannelAddress]:
|
|
@@ -312,7 +296,9 @@ For this purpose injectable classes can be decorated with `@health_checks()` tha
|
|
|
312
296
|
@injectable()
|
|
313
297
|
class Checks:
|
|
314
298
|
def __init__(self):
|
|
315
|
-
pass
|
|
299
|
+
pass # normally, we would inject stuff here
|
|
300
|
+
|
|
301
|
+
# checks
|
|
316
302
|
|
|
317
303
|
@health_check(fail_if_slower_than=1)
|
|
318
304
|
def check_performance(self, result: HealthCheckManager.Result):
|
|
@@ -381,6 +367,8 @@ Several channels are implemented:
|
|
|
381
367
|
channel that posts generic `Request` objects via a `invoke` POST-call
|
|
382
368
|
- `dispatch-msgpack`
|
|
383
369
|
channel that posts generic `Request` objects via a `invoke` POST-call after packing the json with msgpack
|
|
370
|
+
- `dispatch-protobuf`
|
|
371
|
+
channel that posts parameters via a `invoke` POST-call after packing the arguments with protobuf
|
|
384
372
|
- `rest`
|
|
385
373
|
channel that executes regular rest-calls as defined by a couple of decorators.
|
|
386
374
|
|
|
@@ -400,14 +388,6 @@ To customize the behavior, an `around` advice can be implemented easily:
|
|
|
400
388
|
```python
|
|
401
389
|
@advice
|
|
402
390
|
class ChannelAdvice:
|
|
403
|
-
def __init__(self):
|
|
404
|
-
pass
|
|
405
|
-
|
|
406
|
-
@advice
|
|
407
|
-
class ChannelAdvice:
|
|
408
|
-
def __init__(self):
|
|
409
|
-
pass
|
|
410
|
-
|
|
411
391
|
@around(methods().named("customize").of_type(Channel))
|
|
412
392
|
def customize_channel(self, invocation: Invocation):
|
|
413
393
|
channel = cast(Channel, invocation.args[0])
|
|
@@ -425,6 +405,7 @@ The avg response times - on a local server - where all below 1ms per call.
|
|
|
425
405
|
- rest calls are the slowest ( about 0.7ms )
|
|
426
406
|
- dispatching-json 20% faster
|
|
427
407
|
- dispatching-msgpack 30% faster
|
|
408
|
+
- dispatching protobuf
|
|
428
409
|
|
|
429
410
|
The biggest advantage of the dispatching flavors is, that you don't have to worry about the additional decorators!
|
|
430
411
|
|
|
@@ -455,6 +436,11 @@ Additional annotations are
|
|
|
455
436
|
- `Body` the post body
|
|
456
437
|
- `QueryParam`marked for query params
|
|
457
438
|
|
|
439
|
+
You can skip the annotations, assuming the following heuristic:
|
|
440
|
+
|
|
441
|
+
- if no body is marked it will pick the first parameter which is a dataclass or a pydantic model
|
|
442
|
+
- all parameters which are not in the path or equal to the body are assumed to be query params.
|
|
443
|
+
|
|
458
444
|
### Intercepting calls
|
|
459
445
|
|
|
460
446
|
The client side HTTP calling is done with `httpx` instances of type `Httpx.Client` or `Httpx.AsyncClient`.
|
|
@@ -500,7 +486,7 @@ The required - `FastAPI` - infrastructure to expose those services requires:
|
|
|
500
486
|
- and a final `boot` call with the root module, which will return an `Environment`
|
|
501
487
|
|
|
502
488
|
```python
|
|
503
|
-
fast_api = FastAPI() # so you can run it with
|
|
489
|
+
fast_api = FastAPI() # so you can run it with uvicorn from command-line
|
|
504
490
|
|
|
505
491
|
@module(imports=[ServiceModule])
|
|
506
492
|
class Module:
|
|
@@ -512,7 +498,7 @@ class Module:
|
|
|
512
498
|
return FastAPIServer(fastapi, service_manager, component_registry)
|
|
513
499
|
|
|
514
500
|
|
|
515
|
-
environment = FastAPIServer.boot(
|
|
501
|
+
environment = FastAPIServer.boot(Module, host="0.0.0.0", port=8000)
|
|
516
502
|
```
|
|
517
503
|
|
|
518
504
|
This setup will also expose all service interfaces decorated with the corresponding http decorators!
|
|
@@ -559,6 +545,10 @@ class FancyChannel(Channel):
|
|
|
559
545
|
|
|
560
546
|
- first release version
|
|
561
547
|
|
|
548
|
+
**0.11.0**
|
|
549
|
+
|
|
550
|
+
- added protobuf support
|
|
551
|
+
|
|
562
552
|
|
|
563
553
|
|
|
564
554
|
|
|
@@ -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=ujJnyWijNNNsl_kJu39bFV2MDq8OjBGOs7XXDWdpx9w,15314
|
|
4
|
+
aspyx_service/healthcheck.py,sha256=XiQx1T0DP0kcCyK_sYBuE-JHs5N285HotLVycFCgzBU,5612
|
|
5
|
+
aspyx_service/protobuf.py,sha256=w2wZymTSObGhgevIRZJ9kRxiEel8f6BycMPQiTYNMzI,39595
|
|
6
|
+
aspyx_service/registries.py,sha256=bnTjKb40fbZXA52E2lDSEzCWI5_NBKZzQjc8ffufB5g,8039
|
|
7
|
+
aspyx_service/restchannel.py,sha256=aCpNCvv1eIRa9IJW8Os4bQQErZKrEeyID65QhnD9QJw,9134
|
|
8
|
+
aspyx_service/server.py,sha256=yI_croAT77PUh2_z1-EGklgzxjMvuUtHomckN3piaBo,15794
|
|
9
|
+
aspyx_service/service.py,sha256=gOX5rbOOLJ-cSIvxa_5lqo1DzcPmDoJCBbMP-ZYKYxs,27972
|
|
10
|
+
aspyx_service/session.py,sha256=HjGpnmwdislc8Ur6pQbSMi2K-lvTsb9_XyO80zupiF8,3713
|
|
11
|
+
aspyx_service-0.11.1.dist-info/METADATA,sha256=nYCeGXZYBRnaEIFYNJYgIY_b5p0qEIOHZI30hwEUwr4,18127
|
|
12
|
+
aspyx_service-0.11.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
13
|
+
aspyx_service-0.11.1.dist-info/licenses/LICENSE,sha256=n4jfx_MNj7cBtPhhI7MCoB_K35cj1icP9yJ4Rh4vlvY,1070
|
|
14
|
+
aspyx_service-0.11.1.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
|