aspyx-service 0.11.0__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/channels.py +20 -83
- aspyx_service/protobuf.py +73 -63
- aspyx_service/server.py +66 -73
- aspyx_service/service.py +29 -11
- {aspyx_service-0.11.0.dist-info → aspyx_service-0.11.1.dist-info}/METADATA +19 -30
- aspyx_service-0.11.1.dist-info/RECORD +14 -0
- aspyx_service-0.11.0.dist-info/RECORD +0 -14
- {aspyx_service-0.11.0.dist-info → aspyx_service-0.11.1.dist-info}/WHEEL +0 -0
- {aspyx_service-0.11.0.dist-info → aspyx_service-0.11.1.dist-info}/licenses/LICENSE +0 -0
aspyx_service/channels.py
CHANGED
|
@@ -16,7 +16,7 @@ from pydantic import BaseModel
|
|
|
16
16
|
from aspyx.di.configuration import inject_value
|
|
17
17
|
from aspyx.reflection import DynamicProxy, TypeDescriptor
|
|
18
18
|
from aspyx.threading import ThreadLocal, ContextLocal
|
|
19
|
-
from aspyx.util import get_deserializer, TypeDeserializer, TypeSerializer, get_serializer
|
|
19
|
+
from aspyx.util import get_deserializer, TypeDeserializer, TypeSerializer, get_serializer, CopyOnWriteCache
|
|
20
20
|
from .service import ServiceManager, ServiceCommunicationException, TokenExpiredException, InvalidTokenException, \
|
|
21
21
|
AuthorizationException, MissingTokenException
|
|
22
22
|
|
|
@@ -61,6 +61,9 @@ class TokenContext:
|
|
|
61
61
|
cls.refresh_token.reset(refresh_token)
|
|
62
62
|
|
|
63
63
|
class HTTPXChannel(Channel):
|
|
64
|
+
"""
|
|
65
|
+
A channel using the httpx clients.
|
|
66
|
+
"""
|
|
64
67
|
__slots__ = [
|
|
65
68
|
"client",
|
|
66
69
|
"async_client",
|
|
@@ -75,28 +78,6 @@ class HTTPXChannel(Channel):
|
|
|
75
78
|
client_local = ThreadLocal[Client]()
|
|
76
79
|
async_client_local = ThreadLocal[AsyncClient]()
|
|
77
80
|
|
|
78
|
-
# class methods
|
|
79
|
-
|
|
80
|
-
@classmethod
|
|
81
|
-
def to_dict(cls, obj: Any) -> Any:
|
|
82
|
-
if isinstance(obj, BaseModel):
|
|
83
|
-
return obj.model_dump()
|
|
84
|
-
|
|
85
|
-
elif is_dataclass(obj):
|
|
86
|
-
return {
|
|
87
|
-
f.name: cls.to_dict(getattr(obj, f.name))
|
|
88
|
-
|
|
89
|
-
for f in fields(obj)
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
elif isinstance(obj, (list, tuple)):
|
|
93
|
-
return [cls.to_dict(item) for item in obj]
|
|
94
|
-
|
|
95
|
-
elif isinstance(obj, dict):
|
|
96
|
-
return {key: cls.to_dict(value) for key, value in obj.items()}
|
|
97
|
-
|
|
98
|
-
return obj
|
|
99
|
-
|
|
100
81
|
# constructor
|
|
101
82
|
|
|
102
83
|
def __init__(self):
|
|
@@ -104,9 +85,8 @@ class HTTPXChannel(Channel):
|
|
|
104
85
|
|
|
105
86
|
self.timeout = 1000.0
|
|
106
87
|
self.service_names: dict[Type, str] = {}
|
|
107
|
-
self.serializers
|
|
108
|
-
self.deserializers
|
|
109
|
-
self.optimize_serialization = True
|
|
88
|
+
self.serializers = CopyOnWriteCache[Callable, list[Callable]]()
|
|
89
|
+
self.deserializers = CopyOnWriteCache[Callable, Callable]()
|
|
110
90
|
|
|
111
91
|
# inject
|
|
112
92
|
|
|
@@ -132,7 +112,7 @@ class HTTPXChannel(Channel):
|
|
|
132
112
|
|
|
133
113
|
serializers = [get_serializer(type) for type in param_types]
|
|
134
114
|
|
|
135
|
-
self.serializers
|
|
115
|
+
self.serializers.put(method, serializers)
|
|
136
116
|
|
|
137
117
|
return serializers
|
|
138
118
|
|
|
@@ -143,7 +123,7 @@ class HTTPXChannel(Channel):
|
|
|
143
123
|
|
|
144
124
|
deserializer = get_deserializer(return_type)
|
|
145
125
|
|
|
146
|
-
self.deserializers
|
|
126
|
+
self.deserializers.put(method, deserializer)
|
|
147
127
|
|
|
148
128
|
return deserializer
|
|
149
129
|
|
|
@@ -196,32 +176,6 @@ class HTTPXChannel(Channel):
|
|
|
196
176
|
|
|
197
177
|
headers["Authorization"] = f"Bearer {token}"
|
|
198
178
|
|
|
199
|
-
### TEST
|
|
200
|
-
|
|
201
|
-
print_size = False
|
|
202
|
-
if print_size:
|
|
203
|
-
request = self.get_client().build_request(http_method, url, params=params, json=json, headers=headers, timeout=timeout, content=content)
|
|
204
|
-
# Measure body
|
|
205
|
-
body_size = len(request.content or b"")
|
|
206
|
-
|
|
207
|
-
# Measure headers (as raw bytes)
|
|
208
|
-
headers_size = sum(
|
|
209
|
-
len(k.encode()) + len(v.encode()) + 4 # ": " + "\r\n"
|
|
210
|
-
for k, v in request.headers.items()
|
|
211
|
-
) + 2 # final \r\n
|
|
212
|
-
|
|
213
|
-
# Optional: estimate request line
|
|
214
|
-
request_line = f"{request.method} {request.url.raw_path.decode()} HTTP/1.1\r\n".encode()
|
|
215
|
-
request_line_size = len(request_line)
|
|
216
|
-
|
|
217
|
-
# Total estimated size
|
|
218
|
-
total_size = request_line_size + headers_size + body_size
|
|
219
|
-
|
|
220
|
-
print(f"Request line: {request_line_size} bytes")
|
|
221
|
-
print(f"Headers: {headers_size} bytes")
|
|
222
|
-
print(f"Body: {body_size} bytes")
|
|
223
|
-
print(f"Total request size: {total_size} bytes")
|
|
224
|
-
|
|
225
179
|
try:
|
|
226
180
|
response = self.get_client().request(http_method, url, params=params, json=json, headers=headers, timeout=timeout, content=content)
|
|
227
181
|
response.raise_for_status()
|
|
@@ -317,16 +271,11 @@ class DispatchJSONChannel(HTTPXChannel):
|
|
|
317
271
|
def invoke(self, invocation: DynamicProxy.Invocation):
|
|
318
272
|
service_name = self.service_names[invocation.type] # map type to registered service name
|
|
319
273
|
|
|
320
|
-
request
|
|
321
|
-
"method": f"{self.component_descriptor.name}:{service_name}:{invocation.method.__name__}"
|
|
322
|
-
|
|
274
|
+
request = {
|
|
275
|
+
"method": f"{self.component_descriptor.name}:{service_name}:{invocation.method.__name__}",
|
|
276
|
+
"args": self.serialize_args(invocation)
|
|
323
277
|
}
|
|
324
278
|
|
|
325
|
-
if self.optimize_serialization:
|
|
326
|
-
request["args"] = self.serialize_args(invocation)
|
|
327
|
-
else:
|
|
328
|
-
request["args"] = self.to_dict(invocation.args)
|
|
329
|
-
|
|
330
279
|
try:
|
|
331
280
|
http_result = self.request( "post", f"{self.get_url()}/invoke", json=request, timeout=self.timeout)
|
|
332
281
|
result = http_result.json()
|
|
@@ -343,15 +292,11 @@ class DispatchJSONChannel(HTTPXChannel):
|
|
|
343
292
|
|
|
344
293
|
async def invoke_async(self, invocation: DynamicProxy.Invocation):
|
|
345
294
|
service_name = self.service_names[invocation.type] # map type to registered service name
|
|
346
|
-
request
|
|
347
|
-
"method": f"{self.component_descriptor.name}:{service_name}:{invocation.method.__name__}"
|
|
295
|
+
request = {
|
|
296
|
+
"method": f"{self.component_descriptor.name}:{service_name}:{invocation.method.__name__}",
|
|
297
|
+
"args": self.serialize_args(invocation)
|
|
348
298
|
}
|
|
349
299
|
|
|
350
|
-
if self.optimize_serialization:
|
|
351
|
-
request["args"] = self.serialize_args(invocation)
|
|
352
|
-
else:
|
|
353
|
-
request["args"] = self.to_dict(invocation.args)
|
|
354
|
-
|
|
355
300
|
try:
|
|
356
301
|
data = await self.request_async("post", f"{self.get_url()}/invoke", json=request, timeout=self.timeout)
|
|
357
302
|
result = data.json()
|
|
@@ -387,15 +332,11 @@ class DispatchMSPackChannel(HTTPXChannel):
|
|
|
387
332
|
|
|
388
333
|
def invoke(self, invocation: DynamicProxy.Invocation):
|
|
389
334
|
service_name = self.service_names[invocation.type] # map type to registered service name
|
|
390
|
-
request
|
|
391
|
-
"method": f"{self.component_descriptor.name}:{service_name}:{invocation.method.__name__}"
|
|
335
|
+
request = {
|
|
336
|
+
"method": f"{self.component_descriptor.name}:{service_name}:{invocation.method.__name__}",
|
|
337
|
+
"args": self.serialize_args(invocation)
|
|
392
338
|
}
|
|
393
339
|
|
|
394
|
-
if self.optimize_serialization:
|
|
395
|
-
request["args"] = self.serialize_args(invocation)
|
|
396
|
-
else:
|
|
397
|
-
request["args"] = self.to_dict(invocation.args)
|
|
398
|
-
|
|
399
340
|
try:
|
|
400
341
|
packed = msgpack.packb(request, use_bin_type=True)
|
|
401
342
|
|
|
@@ -444,15 +385,11 @@ class DispatchMSPackChannel(HTTPXChannel):
|
|
|
444
385
|
|
|
445
386
|
async def invoke_async(self, invocation: DynamicProxy.Invocation):
|
|
446
387
|
service_name = self.service_names[invocation.type] # map type to registered service name
|
|
447
|
-
request
|
|
448
|
-
"method": f"{self.component_descriptor.name}:{service_name}:{invocation.method.__name__}"
|
|
388
|
+
request = {
|
|
389
|
+
"method": f"{self.component_descriptor.name}:{service_name}:{invocation.method.__name__}",
|
|
390
|
+
"args": self.serialize_args(invocation)
|
|
449
391
|
}
|
|
450
392
|
|
|
451
|
-
if self.optimize_serialization:
|
|
452
|
-
request["args"] = self.serialize_args(invocation)
|
|
453
|
-
else:
|
|
454
|
-
request["args"] = self.to_dict(invocation.args)
|
|
455
|
-
|
|
456
393
|
try:
|
|
457
394
|
packed = msgpack.packb(request, use_bin_type=True)
|
|
458
395
|
|
aspyx_service/protobuf.py
CHANGED
|
@@ -4,10 +4,11 @@ Protobuf channel and utilities
|
|
|
4
4
|
from __future__ import annotations
|
|
5
5
|
|
|
6
6
|
import inspect
|
|
7
|
+
import logging
|
|
7
8
|
import threading
|
|
8
9
|
from dataclasses import is_dataclass, fields as dc_fields
|
|
9
10
|
from typing import Type, get_type_hints, Callable, Tuple, get_origin, get_args, List, Dict, Any, Union, Sequence, \
|
|
10
|
-
Optional
|
|
11
|
+
Optional, cast
|
|
11
12
|
|
|
12
13
|
import httpx
|
|
13
14
|
from google.protobuf.message_factory import GetMessageClass
|
|
@@ -17,12 +18,13 @@ from google.protobuf import descriptor_pb2, descriptor_pool, message_factory
|
|
|
17
18
|
from google.protobuf.descriptor_pool import DescriptorPool
|
|
18
19
|
from google.protobuf.message import Message
|
|
19
20
|
from google.protobuf.descriptor import FieldDescriptor, Descriptor
|
|
21
|
+
from starlette.responses import PlainTextResponse
|
|
20
22
|
|
|
21
|
-
from aspyx.di import injectable
|
|
23
|
+
from aspyx.di import injectable, Environment
|
|
22
24
|
from aspyx.reflection import DynamicProxy, TypeDescriptor
|
|
23
|
-
from aspyx.util import CopyOnWriteCache
|
|
25
|
+
from aspyx.util import CopyOnWriteCache, StringBuilder
|
|
24
26
|
|
|
25
|
-
from .service import channel, ServiceException
|
|
27
|
+
from .service import channel, ServiceException, Server, ComponentDescriptor
|
|
26
28
|
from .channels import HTTPXChannel
|
|
27
29
|
from .service import ServiceManager, ServiceCommunicationException, AuthorizationException, RemoteServiceException
|
|
28
30
|
|
|
@@ -53,6 +55,11 @@ def defaults_dict(model_cls: Type[BaseModel]) -> dict[str, Any]:
|
|
|
53
55
|
return result
|
|
54
56
|
|
|
55
57
|
class ProtobufBuilder:
|
|
58
|
+
"""
|
|
59
|
+
used to infer protobuf services and messages given component and service structures.
|
|
60
|
+
"""
|
|
61
|
+
logger = logging.getLogger("aspyx.service.protobuf") #
|
|
62
|
+
|
|
56
63
|
# slots
|
|
57
64
|
|
|
58
65
|
__slots__ = [
|
|
@@ -72,11 +79,11 @@ class ProtobufBuilder:
|
|
|
72
79
|
|
|
73
80
|
@classmethod
|
|
74
81
|
def get_request_message_name(cls, type: Type, method: Callable) -> str:
|
|
75
|
-
return cls.get_message_name(type, f"{method.__name__}
|
|
82
|
+
return cls.get_message_name(type, f"_{method.__name__}_Request")
|
|
76
83
|
|
|
77
84
|
@classmethod
|
|
78
85
|
def get_response_message_name(cls, type: Type, method: Callable) -> str:
|
|
79
|
-
return cls.get_message_name(type, f"{method.__name__}
|
|
86
|
+
return cls.get_message_name(type, f"_{method.__name__}_Response")
|
|
80
87
|
|
|
81
88
|
# local classes
|
|
82
89
|
|
|
@@ -90,7 +97,7 @@ class ProtobufBuilder:
|
|
|
90
97
|
self.file_desc_proto.name = f"{self.name}.proto"
|
|
91
98
|
self.file_desc_proto.package = self.name
|
|
92
99
|
self.types : dict[Type, Any] = {}
|
|
93
|
-
self.
|
|
100
|
+
self.sealed = False
|
|
94
101
|
self.lock = threading.RLock()
|
|
95
102
|
|
|
96
103
|
# public
|
|
@@ -107,12 +114,16 @@ class ProtobufBuilder:
|
|
|
107
114
|
raise TypeError("Expected a dataclass or Pydantic model class.")
|
|
108
115
|
|
|
109
116
|
def add_message(self, cls: Type) -> str:
|
|
110
|
-
if self.
|
|
117
|
+
if self.sealed:
|
|
111
118
|
raise ServiceException(f"module {self.name} is already sealed")
|
|
112
119
|
|
|
120
|
+
|
|
121
|
+
|
|
113
122
|
name = cls.__name__
|
|
114
123
|
full_name = f"{self.name}.{name}"
|
|
115
124
|
|
|
125
|
+
ProtobufBuilder.logger.debug(f"adding message %s", full_name)
|
|
126
|
+
|
|
116
127
|
# Check if a message type is already defined
|
|
117
128
|
|
|
118
129
|
if any(m.name == name for m in self.file_desc_proto.message_type):
|
|
@@ -154,12 +165,14 @@ class ProtobufBuilder:
|
|
|
154
165
|
return self.types[type]
|
|
155
166
|
|
|
156
167
|
def build_request_message(self, method: TypeDescriptor.MethodDescriptor, request_name: str):
|
|
157
|
-
if self.
|
|
168
|
+
if self.sealed:
|
|
158
169
|
raise ServiceException(f"module {self.name} is already sealed")
|
|
159
170
|
|
|
160
171
|
request_msg = descriptor_pb2.DescriptorProto() # type: ignore
|
|
161
172
|
request_msg.name = request_name.split(".")[-1]
|
|
162
173
|
|
|
174
|
+
ProtobufBuilder.logger.debug(f"adding request message %s", request_msg.name)
|
|
175
|
+
|
|
163
176
|
# loop over parameters
|
|
164
177
|
|
|
165
178
|
field_index = 1
|
|
@@ -182,12 +195,14 @@ class ProtobufBuilder:
|
|
|
182
195
|
self.file_desc_proto.message_type.add().CopyFrom(request_msg)
|
|
183
196
|
|
|
184
197
|
def build_response_message(self, method: TypeDescriptor.MethodDescriptor, response_name: str):
|
|
185
|
-
if self.
|
|
198
|
+
if self.sealed:
|
|
186
199
|
raise ServiceException(f"module {self.name} is already sealed")
|
|
187
200
|
|
|
188
201
|
response_msg = descriptor_pb2.DescriptorProto() # type: ignore
|
|
189
202
|
response_msg.name = response_name.split(".")[-1]
|
|
190
203
|
|
|
204
|
+
ProtobufBuilder.logger.debug(f"adding response message %s", response_msg.name)
|
|
205
|
+
|
|
191
206
|
# return
|
|
192
207
|
|
|
193
208
|
return_type = method.return_type
|
|
@@ -218,13 +233,13 @@ class ProtobufBuilder:
|
|
|
218
233
|
self.file_desc_proto.message_type.add().CopyFrom(response_msg)
|
|
219
234
|
|
|
220
235
|
def build_service_method(self, service_desc: descriptor_pb2.ServiceDescriptorProto, service_type: TypeDescriptor, method: TypeDescriptor.MethodDescriptor):
|
|
221
|
-
name = f"{service_type.cls.__name__}{method.get_name()}"
|
|
236
|
+
name = f"{service_type.cls.__name__}_{method.get_name()}"
|
|
222
237
|
package = self.name
|
|
223
238
|
|
|
224
239
|
method_desc = descriptor_pb2.MethodDescriptorProto()
|
|
225
240
|
|
|
226
|
-
request_name = f".{package}.{name}
|
|
227
|
-
response_name = f".{package}.{name}
|
|
241
|
+
request_name = f".{package}.{name}_Request"
|
|
242
|
+
response_name = f".{package}.{name}_Response"
|
|
228
243
|
|
|
229
244
|
method_desc.name = method.get_name()
|
|
230
245
|
method_desc.input_type = request_name
|
|
@@ -240,12 +255,14 @@ class ProtobufBuilder:
|
|
|
240
255
|
service_desc.method.add().CopyFrom(method_desc)
|
|
241
256
|
|
|
242
257
|
def add_service(self, service_type: TypeDescriptor):
|
|
243
|
-
if self.
|
|
258
|
+
if self.sealed:
|
|
244
259
|
raise ServiceException(f"module {self.name} is already sealed")
|
|
245
260
|
|
|
246
261
|
service_desc = descriptor_pb2.ServiceDescriptorProto() # type: ignore
|
|
247
262
|
service_desc.name = service_type.cls.__name__
|
|
248
263
|
|
|
264
|
+
ProtobufBuilder.logger.debug(f"add service %s", service_desc.name)
|
|
265
|
+
|
|
249
266
|
# check methods
|
|
250
267
|
|
|
251
268
|
for method in service_type.get_methods():
|
|
@@ -255,17 +272,16 @@ class ProtobufBuilder:
|
|
|
255
272
|
|
|
256
273
|
self.file_desc_proto.service.add().CopyFrom(service_desc)
|
|
257
274
|
|
|
258
|
-
def
|
|
259
|
-
if not self.
|
|
260
|
-
|
|
275
|
+
def seal(self, builder: ProtobufBuilder):
|
|
276
|
+
if not self.sealed:
|
|
277
|
+
ProtobufBuilder.logger.debug(f"create protobuf {self.file_desc_proto.name}")
|
|
261
278
|
|
|
262
|
-
|
|
263
|
-
# print(m)
|
|
279
|
+
self.sealed = True
|
|
264
280
|
|
|
265
281
|
# add dependency first
|
|
266
282
|
|
|
267
283
|
for dependency in self.file_desc_proto.dependency:
|
|
268
|
-
builder.modules[dependency].
|
|
284
|
+
builder.modules[dependency].seal(builder)
|
|
269
285
|
|
|
270
286
|
builder.pool.Add(self.file_desc_proto)
|
|
271
287
|
|
|
@@ -340,7 +356,6 @@ class ProtobufBuilder:
|
|
|
340
356
|
|
|
341
357
|
def get_message_type(self, full_name: str):
|
|
342
358
|
return GetMessageClass(self.pool.FindMessageTypeByName(full_name))
|
|
343
|
-
#return self.factory.GetPrototype(self.pool.FindMessageTypeByName(full_name))
|
|
344
359
|
|
|
345
360
|
def get_request_message(self, type: Type, method: Callable):
|
|
346
361
|
return self.get_message_type(self.get_request_message_name(type, method))
|
|
@@ -364,11 +379,7 @@ class ProtobufBuilder:
|
|
|
364
379
|
# public
|
|
365
380
|
|
|
366
381
|
#@synchronized()
|
|
367
|
-
def
|
|
368
|
-
descriptor = getattr(service_type, "__descriptor__")
|
|
369
|
-
|
|
370
|
-
component_descriptor = descriptor.component_descriptor
|
|
371
|
-
|
|
382
|
+
def prepare_component(self, component_descriptor: ComponentDescriptor):
|
|
372
383
|
with self.lock:
|
|
373
384
|
if component_descriptor not in self.components:
|
|
374
385
|
for service in component_descriptor.services:
|
|
@@ -377,7 +388,7 @@ class ProtobufBuilder:
|
|
|
377
388
|
# finalize
|
|
378
389
|
|
|
379
390
|
for module in self.modules.values():
|
|
380
|
-
module.
|
|
391
|
+
module.seal(self)
|
|
381
392
|
|
|
382
393
|
# done
|
|
383
394
|
|
|
@@ -799,54 +810,41 @@ class ProtobufManager(ProtobufBuilder):
|
|
|
799
810
|
# public
|
|
800
811
|
|
|
801
812
|
def create_serializer(self, type: Type, method: Callable) -> ProtobufManager.MethodSerializer:
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
serializer = self.serializer_cache.get(method)
|
|
805
|
-
if serializer is None:
|
|
806
|
-
self.check(type) # make sure all messages are created
|
|
807
|
-
|
|
808
|
-
serializer = ProtobufManager.MethodSerializer(self, self.get_request_message(type, method)).args(method)
|
|
809
|
-
|
|
810
|
-
self.serializer_cache.put(method, serializer)
|
|
811
|
-
|
|
812
|
-
return serializer
|
|
813
|
+
return self.serializer_cache.get(method, lambda m: ProtobufManager.MethodSerializer(self, self.get_request_message(type, m)).args(m) )
|
|
813
814
|
|
|
814
815
|
def create_deserializer(self, descriptor: Descriptor, method: Callable) -> ProtobufManager.MethodDeserializer:
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
deserializer = self.deserializer_cache.get(descriptor)
|
|
818
|
-
if deserializer is None:
|
|
819
|
-
deserializer = ProtobufManager.MethodDeserializer(self, descriptor).args(method)
|
|
820
|
-
|
|
821
|
-
self.deserializer_cache.put(descriptor, deserializer)
|
|
822
|
-
|
|
823
|
-
return deserializer
|
|
816
|
+
return self.deserializer_cache.get(descriptor, lambda d: ProtobufManager.MethodDeserializer(self, d).args(method))
|
|
824
817
|
|
|
825
818
|
def create_result_serializer(self, descriptor: Descriptor, method: Callable) -> ProtobufManager.MethodSerializer:
|
|
826
|
-
|
|
819
|
+
return self.result_serializer_cache.get(descriptor, lambda d: ProtobufManager.MethodSerializer(self, d).result(method))
|
|
827
820
|
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
serializer = ProtobufManager.MethodSerializer(self, descriptor).result(method)
|
|
821
|
+
def create_result_deserializer(self, descriptor: Descriptor, method: Callable) -> ProtobufManager.MethodDeserializer:
|
|
822
|
+
return self.result_deserializer_cache.get(descriptor, lambda d: ProtobufManager.MethodDeserializer(self, d).result(method))
|
|
831
823
|
|
|
832
|
-
|
|
824
|
+
def report(self) -> str:
|
|
825
|
+
builder = StringBuilder()
|
|
826
|
+
for module in self.modules.values():
|
|
827
|
+
builder.append(ProtobufDumper.dump_proto(module.file_desc_proto))
|
|
833
828
|
|
|
834
|
-
return
|
|
829
|
+
return str(builder)
|
|
835
830
|
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
831
|
+
@channel("dispatch-protobuf")
|
|
832
|
+
class ProtobufChannel(HTTPXChannel):
|
|
833
|
+
"""
|
|
834
|
+
channel, encoding requests and responses with protobuf
|
|
835
|
+
"""
|
|
836
|
+
# class methods
|
|
839
837
|
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
838
|
+
@classmethod
|
|
839
|
+
def prepare(cls, server: Server, component_descriptor: ComponentDescriptor):
|
|
840
|
+
protobuf_manager = server.get(ProtobufManager)
|
|
841
|
+
protobuf_manager.prepare_component(component_descriptor)
|
|
843
842
|
|
|
844
|
-
|
|
843
|
+
def report_protobuf():
|
|
844
|
+
return protobuf_manager.report()
|
|
845
845
|
|
|
846
|
-
|
|
846
|
+
server.add_route(path="/report-protobuf", endpoint=report_protobuf, methods=["GET"], response_class=PlainTextResponse)
|
|
847
847
|
|
|
848
|
-
@channel("dispatch-protobuf")
|
|
849
|
-
class ProtobufChannel(HTTPXChannel):
|
|
850
848
|
# local classes
|
|
851
849
|
|
|
852
850
|
class Call:
|
|
@@ -896,6 +894,12 @@ class ProtobufChannel(HTTPXChannel):
|
|
|
896
894
|
self.protobuf_manager = protobuf_manager
|
|
897
895
|
self.cache = CopyOnWriteCache[Callable, ProtobufChannel.Call]()
|
|
898
896
|
|
|
897
|
+
# make sure, all protobuf messages are created
|
|
898
|
+
|
|
899
|
+
for descriptor in manager.descriptors.values():
|
|
900
|
+
if descriptor.is_component():
|
|
901
|
+
protobuf_manager.prepare_component(cast(ComponentDescriptor, descriptor))
|
|
902
|
+
|
|
899
903
|
# internal
|
|
900
904
|
|
|
901
905
|
def get_call(self, type: Type, method: Callable) -> ProtobufChannel.Call:
|
|
@@ -973,6 +977,7 @@ class ProtobufDumper:
|
|
|
973
977
|
lines.append('') # blank line
|
|
974
978
|
|
|
975
979
|
# Options (basic)
|
|
980
|
+
|
|
976
981
|
for opt in fd.options.ListFields() if fd.HasField('options') else []:
|
|
977
982
|
# Just a simple string option dump; for complex options you'd need more logic
|
|
978
983
|
name = opt[0].name
|
|
@@ -982,6 +987,7 @@ class ProtobufDumper:
|
|
|
982
987
|
lines.append('')
|
|
983
988
|
|
|
984
989
|
# Enums
|
|
990
|
+
|
|
985
991
|
def dump_enum(enum: descriptor_pb2.EnumDescriptorProto, indent=''):
|
|
986
992
|
enum_lines = [f"{indent}enum {enum.name} {{"]
|
|
987
993
|
for value in enum.value:
|
|
@@ -990,6 +996,7 @@ class ProtobufDumper:
|
|
|
990
996
|
return enum_lines
|
|
991
997
|
|
|
992
998
|
# Messages (recursive)
|
|
999
|
+
|
|
993
1000
|
def dump_message(msg: descriptor_pb2.DescriptorProto, indent=''):
|
|
994
1001
|
msg_lines = [f"{indent}message {msg.name} {{"]
|
|
995
1002
|
# Nested enums
|
|
@@ -997,6 +1004,7 @@ class ProtobufDumper:
|
|
|
997
1004
|
msg_lines.extend(dump_enum(enum, indent + ' '))
|
|
998
1005
|
|
|
999
1006
|
# Nested messages
|
|
1007
|
+
|
|
1000
1008
|
for nested in msg.nested_type:
|
|
1001
1009
|
# skip map entry messages (synthetic)
|
|
1002
1010
|
if nested.options.map_entry:
|
|
@@ -1004,6 +1012,7 @@ class ProtobufDumper:
|
|
|
1004
1012
|
msg_lines.extend(dump_message(nested, indent + ' '))
|
|
1005
1013
|
|
|
1006
1014
|
# Fields
|
|
1015
|
+
|
|
1007
1016
|
for field in msg.field:
|
|
1008
1017
|
label = {
|
|
1009
1018
|
1: 'optional',
|
|
@@ -1054,6 +1063,7 @@ class ProtobufDumper:
|
|
|
1054
1063
|
return msg_lines
|
|
1055
1064
|
|
|
1056
1065
|
# Services
|
|
1066
|
+
|
|
1057
1067
|
def dump_service(svc: descriptor_pb2.ServiceDescriptorProto, indent=''):
|
|
1058
1068
|
svc_lines = [f"{indent}service {svc.name} {{"]
|
|
1059
1069
|
for method in svc.method:
|
aspyx_service/server.py
CHANGED
|
@@ -14,14 +14,14 @@ 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
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
26
|
from .protobuf import ProtobufManager
|
|
27
27
|
from .service import ComponentRegistry, ServiceDescriptor
|
|
@@ -192,7 +192,7 @@ class FastAPIServer(Server):
|
|
|
192
192
|
|
|
193
193
|
# cache
|
|
194
194
|
|
|
195
|
-
self.deserializers
|
|
195
|
+
self.deserializers = CopyOnWriteCache[str, list[Callable]]()
|
|
196
196
|
|
|
197
197
|
# that's the overall dispatcher
|
|
198
198
|
|
|
@@ -328,18 +328,16 @@ class FastAPIServer(Server):
|
|
|
328
328
|
self.thread.start()
|
|
329
329
|
|
|
330
330
|
def get_deserializers(self, service: Type, method):
|
|
331
|
-
deserializers = self.deserializers.get(method
|
|
331
|
+
deserializers = self.deserializers.get(method)
|
|
332
332
|
if deserializers is None:
|
|
333
333
|
descriptor = TypeDescriptor.for_type(service).get_method(method.__name__)
|
|
334
334
|
|
|
335
335
|
deserializers = [get_deserializer(type) for type in descriptor.param_types]
|
|
336
|
-
self.deserializers
|
|
336
|
+
self.deserializers.put(method, deserializers)
|
|
337
337
|
|
|
338
338
|
return deserializers
|
|
339
339
|
|
|
340
340
|
def deserialize_args(self, args: list[Any], type: Type, method: Callable) -> list:
|
|
341
|
-
#args = list(request.args)
|
|
342
|
-
|
|
343
341
|
deserializers = self.get_deserializers(type, method)
|
|
344
342
|
|
|
345
343
|
for i, arg in enumerate(args):
|
|
@@ -354,98 +352,93 @@ class FastAPIServer(Server):
|
|
|
354
352
|
service_name = parts[1]
|
|
355
353
|
method_name = parts[2]
|
|
356
354
|
|
|
357
|
-
service_descriptor
|
|
355
|
+
service_descriptor = typing.cast(ServiceDescriptor, ServiceManager.descriptors_by_name[service_name])
|
|
358
356
|
service = self.service_manager.get_service(service_descriptor.type, preferred_channel="local")
|
|
359
357
|
|
|
360
358
|
return service_descriptor, getattr(service, method_name)
|
|
361
359
|
|
|
362
|
-
async def
|
|
363
|
-
|
|
364
|
-
|
|
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)
|
|
365
364
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
args : list[Any]
|
|
365
|
+
try:
|
|
366
|
+
result = await self.dispatch(service_descriptor, method, args)
|
|
369
367
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
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)
|
|
368
|
+
return Response(result=result, exception=None).model_dump()
|
|
369
|
+
except Exception as e:
|
|
370
|
+
return Response(result=None, exception=str(e)).model_dump()
|
|
376
371
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
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)
|
|
381
376
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
data = await http_request.body()
|
|
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()
|
|
387
381
|
|
|
388
|
-
|
|
382
|
+
return HttpResponse(
|
|
383
|
+
content=msgpack.packb(response, use_bin_type=True),
|
|
384
|
+
media_type="application/msgpack"
|
|
385
|
+
)
|
|
389
386
|
|
|
390
|
-
|
|
391
|
-
|
|
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"))
|
|
392
389
|
|
|
393
|
-
|
|
390
|
+
data = await http_request.body()
|
|
394
391
|
|
|
395
|
-
|
|
396
|
-
return HttpResponse(
|
|
397
|
-
content="Unsupported Content-Type",
|
|
398
|
-
status_code=415,
|
|
399
|
-
media_type="text/plain"
|
|
400
|
-
)
|
|
392
|
+
# create message
|
|
401
393
|
|
|
402
|
-
request =
|
|
394
|
+
request = self.protobuf_manager.get_request_message(service_descriptor.type, method)()
|
|
395
|
+
request.ParseFromString(data)
|
|
403
396
|
|
|
404
|
-
|
|
405
|
-
try:
|
|
406
|
-
result = await self.dispatch(service_descriptor, method, args)
|
|
397
|
+
# and parse
|
|
407
398
|
|
|
408
|
-
|
|
409
|
-
except Exception as e:
|
|
410
|
-
return Response(result=None, exception=str(e)).model_dump()
|
|
399
|
+
args = self.protobuf_manager.create_deserializer(request.DESCRIPTOR, method).deserialize(request)
|
|
411
400
|
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
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)
|
|
403
|
+
try:
|
|
404
|
+
result = await self.dispatch(service_descriptor, method, args)
|
|
415
405
|
|
|
416
|
-
|
|
406
|
+
result_message = result_serializer.serialize_result(result, None)
|
|
417
407
|
|
|
418
|
-
|
|
419
|
-
|
|
408
|
+
return HttpResponse(
|
|
409
|
+
content=result_message.SerializeToString(),
|
|
410
|
+
media_type="application/x-protobuf"
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
except Exception as e:
|
|
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
420
|
|
|
421
|
-
|
|
421
|
+
async def invoke(self, http_request: HttpRequest):
|
|
422
|
+
content_type = http_request.headers.get("content-type", "")
|
|
422
423
|
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
media_type="application/x-protobuf"
|
|
426
|
-
)
|
|
424
|
+
if content_type == "application/x-protobuf":
|
|
425
|
+
return await self.invoke_protobuf(http_request)
|
|
427
426
|
|
|
428
|
-
|
|
429
|
-
|
|
427
|
+
elif content_type == "application/msgpack":
|
|
428
|
+
return await self.invoke_msgpack(http_request)
|
|
430
429
|
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
media_type="application/x-protobuf"
|
|
434
|
-
)
|
|
430
|
+
elif content_type == "application/json":
|
|
431
|
+
return await self.invoke_json(http_request)
|
|
435
432
|
|
|
436
433
|
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()
|
|
441
|
-
|
|
442
434
|
return HttpResponse(
|
|
443
|
-
content=
|
|
444
|
-
|
|
435
|
+
content="Unsupported Content-Type",
|
|
436
|
+
status_code=415,
|
|
437
|
+
media_type="text/plain"
|
|
445
438
|
)
|
|
446
439
|
|
|
447
440
|
async def dispatch(self, service_descriptor: ServiceDescriptor, method: Callable, args: list[Any]) :
|
|
448
|
-
ServiceManager.logger.debug("dispatch request %s.%s", service_descriptor, method.__name__)
|
|
441
|
+
#ServiceManager.logger.debug("dispatch request %s.%s", service_descriptor, method.__name__)
|
|
449
442
|
|
|
450
443
|
if inspect.iscoroutinefunction(method):
|
|
451
444
|
return await method(*args)
|
|
@@ -454,8 +447,8 @@ class FastAPIServer(Server):
|
|
|
454
447
|
|
|
455
448
|
# override
|
|
456
449
|
|
|
457
|
-
def
|
|
458
|
-
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)
|
|
459
452
|
|
|
460
453
|
def route_health(self, url: str, callable: Callable):
|
|
461
454
|
async def get_health_response():
|
aspyx_service/service.py
CHANGED
|
@@ -7,12 +7,17 @@ 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
|
|
@@ -42,7 +47,7 @@ class ComponentStatus(Enum):
|
|
|
42
47
|
|
|
43
48
|
class Server(ABC):
|
|
44
49
|
"""
|
|
45
|
-
A server is a central
|
|
50
|
+
A server is a central class that boots a main module and initializes the ServiceManager.
|
|
46
51
|
It also is the place where http servers get initialized.
|
|
47
52
|
"""
|
|
48
53
|
port = 0
|
|
@@ -51,6 +56,7 @@ class Server(ABC):
|
|
|
51
56
|
|
|
52
57
|
def __init__(self):
|
|
53
58
|
self.environment : Optional[Environment] = None
|
|
59
|
+
self.instance = self
|
|
54
60
|
|
|
55
61
|
# public
|
|
56
62
|
|
|
@@ -58,7 +64,7 @@ class Server(ABC):
|
|
|
58
64
|
return self.environment.get(type)
|
|
59
65
|
|
|
60
66
|
@abstractmethod
|
|
61
|
-
def
|
|
67
|
+
def add_route(self, path : str, endpoint : Callable, methods : list[str], response_class : typing.Union[Type[Response], DefaultPlaceholder] = Default(JSONResponse)):
|
|
62
68
|
pass
|
|
63
69
|
|
|
64
70
|
@abstractmethod
|
|
@@ -573,7 +579,7 @@ class ChannelFactory:
|
|
|
573
579
|
# constructor
|
|
574
580
|
|
|
575
581
|
def __init__(self):
|
|
576
|
-
self.environment = None
|
|
582
|
+
self.environment : Optional[Environment] = None
|
|
577
583
|
|
|
578
584
|
# lifecycle hooks
|
|
579
585
|
|
|
@@ -583,6 +589,12 @@ class ChannelFactory:
|
|
|
583
589
|
|
|
584
590
|
# public
|
|
585
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
|
+
|
|
586
598
|
def make(self, name: str, descriptor: ComponentDescriptor, address: ChannelInstances) -> Channel:
|
|
587
599
|
ServiceManager.logger.info("create channel %s: %s", name, self.factories.get(name).__name__)
|
|
588
600
|
|
|
@@ -665,9 +677,9 @@ class ServiceManager:
|
|
|
665
677
|
|
|
666
678
|
# constructor
|
|
667
679
|
|
|
668
|
-
def __init__(self, component_registry: ComponentRegistry,
|
|
680
|
+
def __init__(self, component_registry: ComponentRegistry, channel_factory: ChannelFactory):
|
|
669
681
|
self.component_registry = component_registry
|
|
670
|
-
self.
|
|
682
|
+
self.channel_factory = channel_factory
|
|
671
683
|
self.environment : Optional[Environment] = None
|
|
672
684
|
self.preferred_channel = ""
|
|
673
685
|
|
|
@@ -691,7 +703,7 @@ class ServiceManager:
|
|
|
691
703
|
def get_instance(self, type: Type[T]) -> T:
|
|
692
704
|
instance = self.instances.get(type)
|
|
693
705
|
if instance is None:
|
|
694
|
-
ServiceManager.logger.
|
|
706
|
+
ServiceManager.logger.debug("create implementation %s", type.__name__)
|
|
695
707
|
|
|
696
708
|
instance = self.environment.get(type)
|
|
697
709
|
self.instances[type] = instance
|
|
@@ -700,9 +712,16 @@ class ServiceManager:
|
|
|
700
712
|
|
|
701
713
|
# lifecycle
|
|
702
714
|
|
|
715
|
+
|
|
703
716
|
def startup(self, server: Server) -> None:
|
|
704
717
|
self.logger.info("startup on port %s", server.port)
|
|
705
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
|
+
|
|
706
725
|
for descriptor in self.descriptors.values():
|
|
707
726
|
if descriptor.is_component():
|
|
708
727
|
# register local address
|
|
@@ -725,8 +744,6 @@ class ServiceManager:
|
|
|
725
744
|
|
|
726
745
|
self.component_registry.register(descriptor.get_component_descriptor(), [ChannelAddress("local", "")])
|
|
727
746
|
|
|
728
|
-
#health_name = next((decorator.args[0] for decorator in Decorators.get(descriptor.type) if decorator.decorator is health), None)
|
|
729
|
-
|
|
730
747
|
# startup
|
|
731
748
|
|
|
732
749
|
instance.startup()
|
|
@@ -738,9 +755,10 @@ class ServiceManager:
|
|
|
738
755
|
|
|
739
756
|
# register addresses
|
|
740
757
|
|
|
741
|
-
|
|
758
|
+
for address in descriptor.addresses:
|
|
759
|
+
self.channel_factory.prepare_channel(server, address.channel, descriptor.get_component_descriptor())
|
|
742
760
|
|
|
743
|
-
|
|
761
|
+
self.component_registry.register(descriptor.get_component_descriptor(), descriptor.addresses)
|
|
744
762
|
|
|
745
763
|
def shutdown(self):
|
|
746
764
|
self.logger.info("shutdown")
|
|
@@ -823,7 +841,7 @@ class ServiceManager:
|
|
|
823
841
|
if channel_instance is None:
|
|
824
842
|
# create channel
|
|
825
843
|
|
|
826
|
-
channel_instance = self.
|
|
844
|
+
channel_instance = self.channel_factory.make(address.channel, component_descriptor, address)
|
|
827
845
|
|
|
828
846
|
# cache
|
|
829
847
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: aspyx_service
|
|
3
|
-
Version: 0.11.
|
|
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
|
|
@@ -114,9 +114,6 @@ After booting the DI infrastructure with a main module we could already call a s
|
|
|
114
114
|
```python
|
|
115
115
|
@module(imports=[ServiceModule])
|
|
116
116
|
class Module:
|
|
117
|
-
def __init__(self):
|
|
118
|
-
pass
|
|
119
|
-
|
|
120
117
|
@create()
|
|
121
118
|
def create_registry(self) -> ConsulComponentRegistry:
|
|
122
119
|
return ConsulComponentRegistry(Server.port, Consul(host="localhost", port=8500)) # a consul based registry!
|
|
@@ -137,11 +134,6 @@ As we can also host implementations, lets look at this side as well:
|
|
|
137
134
|
```python
|
|
138
135
|
@implementation()
|
|
139
136
|
class TestComponentImpl(AbstractComponent, TestComponent):
|
|
140
|
-
# constructor
|
|
141
|
-
|
|
142
|
-
def __init__(self):
|
|
143
|
-
super().__init__()
|
|
144
|
-
|
|
145
137
|
# implement Component
|
|
146
138
|
|
|
147
139
|
def get_addresses(self, port: int) -> list[ChannelAddress]:
|
|
@@ -149,9 +141,6 @@ class TestComponentImpl(AbstractComponent, TestComponent):
|
|
|
149
141
|
|
|
150
142
|
@implementation()
|
|
151
143
|
class TestServiceImpl(TestService):
|
|
152
|
-
def __init__(self):
|
|
153
|
-
pass
|
|
154
|
-
|
|
155
144
|
def hello(self, message: str) -> str:
|
|
156
145
|
return f"hello {message}"
|
|
157
146
|
```
|
|
@@ -263,8 +252,7 @@ Service implementations implement the corresponding interface and are decorated
|
|
|
263
252
|
```python
|
|
264
253
|
@implementation()
|
|
265
254
|
class TestServiceImpl(TestService):
|
|
266
|
-
|
|
267
|
-
pass
|
|
255
|
+
pass
|
|
268
256
|
```
|
|
269
257
|
|
|
270
258
|
The constructor is required since the instances are managed by the DI framework.
|
|
@@ -274,11 +262,6 @@ Component implementations derive from the interface and the abstract base class
|
|
|
274
262
|
```python
|
|
275
263
|
@implementation()
|
|
276
264
|
class TestComponentImpl(AbstractComponent, TestComponent):
|
|
277
|
-
# constructor
|
|
278
|
-
|
|
279
|
-
def __init__(self):
|
|
280
|
-
super().__init__()
|
|
281
|
-
|
|
282
265
|
# implement Component
|
|
283
266
|
|
|
284
267
|
def get_addresses(self, port: int) -> list[ChannelAddress]:
|
|
@@ -313,7 +296,9 @@ For this purpose injectable classes can be decorated with `@health_checks()` tha
|
|
|
313
296
|
@injectable()
|
|
314
297
|
class Checks:
|
|
315
298
|
def __init__(self):
|
|
316
|
-
pass
|
|
299
|
+
pass # normally, we would inject stuff here
|
|
300
|
+
|
|
301
|
+
# checks
|
|
317
302
|
|
|
318
303
|
@health_check(fail_if_slower_than=1)
|
|
319
304
|
def check_performance(self, result: HealthCheckManager.Result):
|
|
@@ -382,6 +367,8 @@ Several channels are implemented:
|
|
|
382
367
|
channel that posts generic `Request` objects via a `invoke` POST-call
|
|
383
368
|
- `dispatch-msgpack`
|
|
384
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
|
|
385
372
|
- `rest`
|
|
386
373
|
channel that executes regular rest-calls as defined by a couple of decorators.
|
|
387
374
|
|
|
@@ -401,14 +388,6 @@ To customize the behavior, an `around` advice can be implemented easily:
|
|
|
401
388
|
```python
|
|
402
389
|
@advice
|
|
403
390
|
class ChannelAdvice:
|
|
404
|
-
def __init__(self):
|
|
405
|
-
pass
|
|
406
|
-
|
|
407
|
-
@advice
|
|
408
|
-
class ChannelAdvice:
|
|
409
|
-
def __init__(self):
|
|
410
|
-
pass
|
|
411
|
-
|
|
412
391
|
@around(methods().named("customize").of_type(Channel))
|
|
413
392
|
def customize_channel(self, invocation: Invocation):
|
|
414
393
|
channel = cast(Channel, invocation.args[0])
|
|
@@ -426,6 +405,7 @@ The avg response times - on a local server - where all below 1ms per call.
|
|
|
426
405
|
- rest calls are the slowest ( about 0.7ms )
|
|
427
406
|
- dispatching-json 20% faster
|
|
428
407
|
- dispatching-msgpack 30% faster
|
|
408
|
+
- dispatching protobuf
|
|
429
409
|
|
|
430
410
|
The biggest advantage of the dispatching flavors is, that you don't have to worry about the additional decorators!
|
|
431
411
|
|
|
@@ -456,6 +436,11 @@ Additional annotations are
|
|
|
456
436
|
- `Body` the post body
|
|
457
437
|
- `QueryParam`marked for query params
|
|
458
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
|
+
|
|
459
444
|
### Intercepting calls
|
|
460
445
|
|
|
461
446
|
The client side HTTP calling is done with `httpx` instances of type `Httpx.Client` or `Httpx.AsyncClient`.
|
|
@@ -501,7 +486,7 @@ The required - `FastAPI` - infrastructure to expose those services requires:
|
|
|
501
486
|
- and a final `boot` call with the root module, which will return an `Environment`
|
|
502
487
|
|
|
503
488
|
```python
|
|
504
|
-
fast_api = FastAPI() # so you can run it with
|
|
489
|
+
fast_api = FastAPI() # so you can run it with uvicorn from command-line
|
|
505
490
|
|
|
506
491
|
@module(imports=[ServiceModule])
|
|
507
492
|
class Module:
|
|
@@ -513,7 +498,7 @@ class Module:
|
|
|
513
498
|
return FastAPIServer(fastapi, service_manager, component_registry)
|
|
514
499
|
|
|
515
500
|
|
|
516
|
-
environment = FastAPIServer.boot(
|
|
501
|
+
environment = FastAPIServer.boot(Module, host="0.0.0.0", port=8000)
|
|
517
502
|
```
|
|
518
503
|
|
|
519
504
|
This setup will also expose all service interfaces decorated with the corresponding http decorators!
|
|
@@ -560,6 +545,10 @@ class FancyChannel(Channel):
|
|
|
560
545
|
|
|
561
546
|
- first release version
|
|
562
547
|
|
|
548
|
+
**0.11.0**
|
|
549
|
+
|
|
550
|
+
- added protobuf support
|
|
551
|
+
|
|
563
552
|
|
|
564
553
|
|
|
565
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,14 +0,0 @@
|
|
|
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,,
|
|
File without changes
|
|
File without changes
|