aspyx-service 0.10.7__tar.gz → 0.11.1__tar.gz
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-0.10.7 → aspyx_service-0.11.1}/PKG-INFO +21 -31
- {aspyx_service-0.10.7 → aspyx_service-0.11.1}/README.md +18 -29
- {aspyx_service-0.10.7 → aspyx_service-0.11.1}/performance-test/main.py +4 -4
- {aspyx_service-0.10.7 → aspyx_service-0.11.1}/performance-test/performance-test.py +28 -1
- {aspyx_service-0.10.7 → aspyx_service-0.11.1}/performance-test/server.py +6 -7
- {aspyx_service-0.10.7 → aspyx_service-0.11.1}/pyproject.toml +4 -3
- {aspyx_service-0.10.7 → aspyx_service-0.11.1}/src/aspyx_service/__init__.py +5 -0
- {aspyx_service-0.10.7 → aspyx_service-0.11.1}/src/aspyx_service/channels.py +24 -60
- {aspyx_service-0.10.7 → aspyx_service-0.11.1}/src/aspyx_service/healthcheck.py +2 -2
- aspyx_service-0.11.1/src/aspyx_service/protobuf.py +1093 -0
- {aspyx_service-0.10.7 → aspyx_service-0.11.1}/src/aspyx_service/restchannel.py +23 -3
- {aspyx_service-0.10.7 → aspyx_service-0.11.1}/src/aspyx_service/server.py +97 -54
- {aspyx_service-0.10.7 → aspyx_service-0.11.1}/src/aspyx_service/service.py +37 -14
- {aspyx_service-0.10.7 → aspyx_service-0.11.1}/tests/common.py +60 -44
- aspyx_service-0.11.1/tests/other.py +8 -0
- {aspyx_service-0.10.7 → aspyx_service-0.11.1}/tests/test_async_service.py +16 -8
- aspyx_service-0.11.1/tests/test_proto.py +157 -0
- {aspyx_service-0.10.7 → aspyx_service-0.11.1}/tests/test_service.py +33 -13
- {aspyx_service-0.10.7 → aspyx_service-0.11.1}/.gitignore +0 -0
- {aspyx_service-0.10.7 → aspyx_service-0.11.1}/LICENSE +0 -0
- {aspyx_service-0.10.7 → aspyx_service-0.11.1}/performance-test/__init__.py +0 -0
- {aspyx_service-0.10.7 → aspyx_service-0.11.1}/performance-test/client.py +0 -0
- {aspyx_service-0.10.7 → aspyx_service-0.11.1}/performance-test/config.yaml +0 -0
- {aspyx_service-0.10.7 → aspyx_service-0.11.1}/performance-test/readme.txt +0 -0
- {aspyx_service-0.10.7 → aspyx_service-0.11.1}/performance-test/start_server_8000.sh +0 -0
- {aspyx_service-0.10.7 → aspyx_service-0.11.1}/performance-test/start_server_8001.sh +0 -0
- {aspyx_service-0.10.7 → aspyx_service-0.11.1}/src/aspyx_service/authorization.py +0 -0
- {aspyx_service-0.10.7 → aspyx_service-0.11.1}/src/aspyx_service/registries.py +0 -0
- {aspyx_service-0.10.7 → aspyx_service-0.11.1}/src/aspyx_service/session.py +0 -0
- {aspyx_service-0.10.7 → aspyx_service-0.11.1}/tests/__init__.py +0 -0
- {aspyx_service-0.10.7 → aspyx_service-0.11.1}/tests/config.yaml +0 -0
- {aspyx_service-0.10.7 → aspyx_service-0.11.1}/tests/test_healthcheck.py +0 -0
- {aspyx_service-0.10.7 → aspyx_service-0.11.1}/tests/test_jwt.py +0 -0
- {aspyx_service-0.10.7 → aspyx_service-0.11.1}/tests/test_serialization.py +0 -0
|
@@ -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
|
|
|
@@ -77,9 +77,6 @@ After booting the DI infrastructure with a main module we could already call a s
|
|
|
77
77
|
```python
|
|
78
78
|
@module(imports=[ServiceModule])
|
|
79
79
|
class Module:
|
|
80
|
-
def __init__(self):
|
|
81
|
-
pass
|
|
82
|
-
|
|
83
80
|
@create()
|
|
84
81
|
def create_registry(self) -> ConsulComponentRegistry:
|
|
85
82
|
return ConsulComponentRegistry(Server.port, Consul(host="localhost", port=8500)) # a consul based registry!
|
|
@@ -100,11 +97,6 @@ As we can also host implementations, lets look at this side as well:
|
|
|
100
97
|
```python
|
|
101
98
|
@implementation()
|
|
102
99
|
class TestComponentImpl(AbstractComponent, TestComponent):
|
|
103
|
-
# constructor
|
|
104
|
-
|
|
105
|
-
def __init__(self):
|
|
106
|
-
super().__init__()
|
|
107
|
-
|
|
108
100
|
# implement Component
|
|
109
101
|
|
|
110
102
|
def get_addresses(self, port: int) -> list[ChannelAddress]:
|
|
@@ -112,9 +104,6 @@ class TestComponentImpl(AbstractComponent, TestComponent):
|
|
|
112
104
|
|
|
113
105
|
@implementation()
|
|
114
106
|
class TestServiceImpl(TestService):
|
|
115
|
-
def __init__(self):
|
|
116
|
-
pass
|
|
117
|
-
|
|
118
107
|
def hello(self, message: str) -> str:
|
|
119
108
|
return f"hello {message}"
|
|
120
109
|
```
|
|
@@ -226,8 +215,7 @@ Service implementations implement the corresponding interface and are decorated
|
|
|
226
215
|
```python
|
|
227
216
|
@implementation()
|
|
228
217
|
class TestServiceImpl(TestService):
|
|
229
|
-
|
|
230
|
-
pass
|
|
218
|
+
pass
|
|
231
219
|
```
|
|
232
220
|
|
|
233
221
|
The constructor is required since the instances are managed by the DI framework.
|
|
@@ -237,11 +225,6 @@ Component implementations derive from the interface and the abstract base class
|
|
|
237
225
|
```python
|
|
238
226
|
@implementation()
|
|
239
227
|
class TestComponentImpl(AbstractComponent, TestComponent):
|
|
240
|
-
# constructor
|
|
241
|
-
|
|
242
|
-
def __init__(self):
|
|
243
|
-
super().__init__()
|
|
244
|
-
|
|
245
228
|
# implement Component
|
|
246
229
|
|
|
247
230
|
def get_addresses(self, port: int) -> list[ChannelAddress]:
|
|
@@ -276,7 +259,9 @@ For this purpose injectable classes can be decorated with `@health_checks()` tha
|
|
|
276
259
|
@injectable()
|
|
277
260
|
class Checks:
|
|
278
261
|
def __init__(self):
|
|
279
|
-
pass
|
|
262
|
+
pass # normally, we would inject stuff here
|
|
263
|
+
|
|
264
|
+
# checks
|
|
280
265
|
|
|
281
266
|
@health_check(fail_if_slower_than=1)
|
|
282
267
|
def check_performance(self, result: HealthCheckManager.Result):
|
|
@@ -345,6 +330,8 @@ Several channels are implemented:
|
|
|
345
330
|
channel that posts generic `Request` objects via a `invoke` POST-call
|
|
346
331
|
- `dispatch-msgpack`
|
|
347
332
|
channel that posts generic `Request` objects via a `invoke` POST-call after packing the json with msgpack
|
|
333
|
+
- `dispatch-protobuf`
|
|
334
|
+
channel that posts parameters via a `invoke` POST-call after packing the arguments with protobuf
|
|
348
335
|
- `rest`
|
|
349
336
|
channel that executes regular rest-calls as defined by a couple of decorators.
|
|
350
337
|
|
|
@@ -364,14 +351,6 @@ To customize the behavior, an `around` advice can be implemented easily:
|
|
|
364
351
|
```python
|
|
365
352
|
@advice
|
|
366
353
|
class ChannelAdvice:
|
|
367
|
-
def __init__(self):
|
|
368
|
-
pass
|
|
369
|
-
|
|
370
|
-
@advice
|
|
371
|
-
class ChannelAdvice:
|
|
372
|
-
def __init__(self):
|
|
373
|
-
pass
|
|
374
|
-
|
|
375
354
|
@around(methods().named("customize").of_type(Channel))
|
|
376
355
|
def customize_channel(self, invocation: Invocation):
|
|
377
356
|
channel = cast(Channel, invocation.args[0])
|
|
@@ -389,6 +368,7 @@ The avg response times - on a local server - where all below 1ms per call.
|
|
|
389
368
|
- rest calls are the slowest ( about 0.7ms )
|
|
390
369
|
- dispatching-json 20% faster
|
|
391
370
|
- dispatching-msgpack 30% faster
|
|
371
|
+
- dispatching protobuf
|
|
392
372
|
|
|
393
373
|
The biggest advantage of the dispatching flavors is, that you don't have to worry about the additional decorators!
|
|
394
374
|
|
|
@@ -419,6 +399,11 @@ Additional annotations are
|
|
|
419
399
|
- `Body` the post body
|
|
420
400
|
- `QueryParam`marked for query params
|
|
421
401
|
|
|
402
|
+
You can skip the annotations, assuming the following heuristic:
|
|
403
|
+
|
|
404
|
+
- if no body is marked it will pick the first parameter which is a dataclass or a pydantic model
|
|
405
|
+
- all parameters which are not in the path or equal to the body are assumed to be query params.
|
|
406
|
+
|
|
422
407
|
### Intercepting calls
|
|
423
408
|
|
|
424
409
|
The client side HTTP calling is done with `httpx` instances of type `Httpx.Client` or `Httpx.AsyncClient`.
|
|
@@ -464,7 +449,7 @@ The required - `FastAPI` - infrastructure to expose those services requires:
|
|
|
464
449
|
- and a final `boot` call with the root module, which will return an `Environment`
|
|
465
450
|
|
|
466
451
|
```python
|
|
467
|
-
fast_api = FastAPI() # so you can run it with
|
|
452
|
+
fast_api = FastAPI() # so you can run it with uvicorn from command-line
|
|
468
453
|
|
|
469
454
|
@module(imports=[ServiceModule])
|
|
470
455
|
class Module:
|
|
@@ -476,7 +461,7 @@ class Module:
|
|
|
476
461
|
return FastAPIServer(fastapi, service_manager, component_registry)
|
|
477
462
|
|
|
478
463
|
|
|
479
|
-
environment = FastAPIServer.boot(
|
|
464
|
+
environment = FastAPIServer.boot(Module, host="0.0.0.0", port=8000)
|
|
480
465
|
```
|
|
481
466
|
|
|
482
467
|
This setup will also expose all service interfaces decorated with the corresponding http decorators!
|
|
@@ -523,6 +508,10 @@ class FancyChannel(Channel):
|
|
|
523
508
|
|
|
524
509
|
- first release version
|
|
525
510
|
|
|
511
|
+
**0.11.0**
|
|
512
|
+
|
|
513
|
+
- added protobuf support
|
|
514
|
+
|
|
526
515
|
|
|
527
516
|
|
|
528
517
|
|
|
@@ -11,11 +11,11 @@ from server import ServerModule
|
|
|
11
11
|
from aspyx.util import Logger
|
|
12
12
|
|
|
13
13
|
|
|
14
|
-
Logger.configure(default_level=logging.
|
|
14
|
+
Logger.configure(default_level=logging.INFO, levels={
|
|
15
15
|
"httpx": logging.ERROR,
|
|
16
|
-
"aspyx.di": logging.
|
|
17
|
-
"aspyx.di.aop": logging.
|
|
18
|
-
"aspyx.service": logging.
|
|
16
|
+
"aspyx.di": logging.INFO,
|
|
17
|
+
"aspyx.di.aop": logging.INFO,
|
|
18
|
+
"aspyx.service": logging.INFO
|
|
19
19
|
})
|
|
20
20
|
|
|
21
21
|
PORT = int(os.getenv("FAST_API_PORT", "8000"))
|
|
@@ -6,7 +6,7 @@ import threading
|
|
|
6
6
|
import time
|
|
7
7
|
import logging
|
|
8
8
|
|
|
9
|
-
from typing import Callable, TypeVar, Type, Awaitable, Any,
|
|
9
|
+
from typing import Callable, TypeVar, Type, Awaitable, Any, cast
|
|
10
10
|
|
|
11
11
|
from consul import Consul
|
|
12
12
|
|
|
@@ -95,6 +95,8 @@ data = Data(i=1, f=1.0, b=True, s="s",
|
|
|
95
95
|
)
|
|
96
96
|
|
|
97
97
|
def run_loops(name: str, loops: int, type: Type[T], instance: T, callable: Callable[[T], None]):
|
|
98
|
+
callable(instance) # initialization
|
|
99
|
+
|
|
98
100
|
start = time.perf_counter()
|
|
99
101
|
for _ in range(loops):
|
|
100
102
|
callable(instance)
|
|
@@ -105,6 +107,8 @@ def run_loops(name: str, loops: int, type: Type[T], instance: T, callable: Call
|
|
|
105
107
|
print(f"run {name}, loops={loops}: avg={avg_ms:.3f} ms")
|
|
106
108
|
|
|
107
109
|
async def run_async_loops(name: str, loops: int, type: Type[T], instance: T, callable: Callable[[T], Awaitable[Any]]):
|
|
110
|
+
await callable(instance) # initialization
|
|
111
|
+
|
|
108
112
|
start = time.perf_counter()
|
|
109
113
|
for _ in range(loops):
|
|
110
114
|
await callable(instance)
|
|
@@ -192,12 +196,17 @@ async def main():
|
|
|
192
196
|
run_loops("rest", loops, TestRestService, manager.get_service(TestRestService, preferred_channel="rest"), lambda service: service.get("world"))
|
|
193
197
|
run_loops("json", loops, TestService, manager.get_service(TestService, preferred_channel="dispatch-json"), lambda service: service.hello("world"))
|
|
194
198
|
run_loops("msgpack", loops, TestService, manager.get_service(TestService, preferred_channel="dispatch-msgpack"), lambda service: service.hello("world"))
|
|
199
|
+
run_loops("protobuf", loops, TestService, manager.get_service(TestService, preferred_channel="dispatch-protobuf"),
|
|
200
|
+
lambda service: service.hello("world"))
|
|
195
201
|
|
|
196
202
|
# pydantic
|
|
197
203
|
|
|
198
204
|
run_loops("rest & pydantic", loops, TestRestService, manager.get_service(TestRestService, preferred_channel="rest"), lambda service: service.post_pydantic("hello", pydantic))
|
|
199
205
|
run_loops("json & pydantic", loops, TestService, manager.get_service(TestService, preferred_channel="dispatch-json"), lambda service: service.pydantic(pydantic))
|
|
200
206
|
run_loops("msgpack & pydantic", loops, TestService, manager.get_service(TestService, preferred_channel="dispatch-msgpack"), lambda service: service.pydantic(pydantic))
|
|
207
|
+
run_loops("protobuf & pydantic", loops, TestService,
|
|
208
|
+
manager.get_service(TestService, preferred_channel="dispatch-protobuf"),
|
|
209
|
+
lambda service: service.pydantic(pydantic))
|
|
201
210
|
|
|
202
211
|
# data class
|
|
203
212
|
|
|
@@ -209,6 +218,9 @@ async def main():
|
|
|
209
218
|
run_loops("msgpack & data", loops, TestService,
|
|
210
219
|
manager.get_service(TestService, preferred_channel="dispatch-msgpack"),
|
|
211
220
|
lambda service: service.data(data))
|
|
221
|
+
run_loops("protobuf & data", loops, TestService,
|
|
222
|
+
manager.get_service(TestService, preferred_channel="dispatch-protobuf"),
|
|
223
|
+
lambda service: service.data(data))
|
|
212
224
|
|
|
213
225
|
# async
|
|
214
226
|
|
|
@@ -218,6 +230,9 @@ async def main():
|
|
|
218
230
|
lambda service: service.hello("world"))
|
|
219
231
|
await run_async_loops("async msgpack", loops, TestAsyncService, manager.get_service(TestAsyncService, preferred_channel="dispatch-msgpack"),
|
|
220
232
|
lambda service: service.hello("world"))
|
|
233
|
+
await run_async_loops("async protobuf", loops, TestAsyncService,
|
|
234
|
+
manager.get_service(TestAsyncService, preferred_channel="dispatch-protobuf"),
|
|
235
|
+
lambda service: service.hello("world"))
|
|
221
236
|
|
|
222
237
|
# pydantic
|
|
223
238
|
|
|
@@ -229,6 +244,9 @@ async def main():
|
|
|
229
244
|
await run_async_loops("async msgpack & pydantic", loops, TestAsyncService,
|
|
230
245
|
manager.get_service(TestAsyncService, preferred_channel="dispatch-msgpack"),
|
|
231
246
|
lambda service: service.pydantic(pydantic))
|
|
247
|
+
await run_async_loops("async protobuf & pydantic", loops, TestAsyncService,
|
|
248
|
+
manager.get_service(TestAsyncService, preferred_channel="dispatch-protobuf"),
|
|
249
|
+
lambda service: service.pydantic(pydantic))
|
|
232
250
|
|
|
233
251
|
# data class
|
|
234
252
|
|
|
@@ -242,6 +260,9 @@ async def main():
|
|
|
242
260
|
await run_async_loops("async msgpack & data", loops, TestAsyncService,
|
|
243
261
|
manager.get_service(TestAsyncService, preferred_channel="dispatch-msgpack"),
|
|
244
262
|
lambda service: service.data(data))
|
|
263
|
+
await run_async_loops("async protobuf & data", loops, TestAsyncService,
|
|
264
|
+
manager.get_service(TestAsyncService, preferred_channel="dispatch-protobuf"),
|
|
265
|
+
lambda service: service.data(data))
|
|
245
266
|
|
|
246
267
|
# a real thread test
|
|
247
268
|
|
|
@@ -262,6 +283,9 @@ async def main():
|
|
|
262
283
|
run_threaded_sync_loops("threaded sync json, 16 thread", loops, 16, TestService,
|
|
263
284
|
manager.get_service(TestService, preferred_channel="dispatch-json"),
|
|
264
285
|
lambda service: service.hello("world"))
|
|
286
|
+
run_threaded_sync_loops("threaded sync protobuf, 16 thread", loops, 16, TestService,
|
|
287
|
+
manager.get_service(TestService, preferred_channel="dispatch-protobuf"),
|
|
288
|
+
lambda service: service.hello("world"))
|
|
265
289
|
|
|
266
290
|
# async
|
|
267
291
|
|
|
@@ -280,6 +304,9 @@ async def main():
|
|
|
280
304
|
run_threaded_async_loops("threaded async json, 16 thread", loops, 16, TestAsyncService,
|
|
281
305
|
manager.get_service(TestAsyncService, preferred_channel="dispatch-json"),
|
|
282
306
|
lambda service: service.hello("world"))
|
|
307
|
+
run_threaded_async_loops("threaded async protobuf, 16 thread", loops, 16, TestAsyncService,
|
|
308
|
+
manager.get_service(TestAsyncService, preferred_channel="dispatch-protobuf"),
|
|
309
|
+
lambda service: service.hello("world"))
|
|
283
310
|
|
|
284
311
|
if __name__ == "__main__":
|
|
285
312
|
asyncio.run(main())
|
|
@@ -7,7 +7,8 @@ from typing import Optional
|
|
|
7
7
|
from consul import Consul
|
|
8
8
|
from fastapi import FastAPI
|
|
9
9
|
|
|
10
|
-
from aspyx_service import HealthCheckManager, ServiceModule, ConsulComponentRegistry, SessionManager, FastAPIServer
|
|
10
|
+
from aspyx_service import HealthCheckManager, ServiceModule, ConsulComponentRegistry, SessionManager, FastAPIServer, \
|
|
11
|
+
ProtobufManager
|
|
11
12
|
|
|
12
13
|
from client import ClientModule, TestService, TestAsyncService, Data, Pydantic, TestRestService, TestAsyncRestService, TestComponent
|
|
13
14
|
|
|
@@ -17,9 +18,6 @@ from aspyx.di import on_running, module, create
|
|
|
17
18
|
from aspyx.di.aop import Invocation, advice, error
|
|
18
19
|
from aspyx.exception import handle, ExceptionManager
|
|
19
20
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
21
|
# implementation classes
|
|
24
22
|
|
|
25
23
|
@implementation()
|
|
@@ -133,7 +131,8 @@ class TestComponentImpl(AbstractComponent, TestComponent):
|
|
|
133
131
|
return [
|
|
134
132
|
ChannelAddress("rest", f"http://{Server.get_local_ip()}:{port}"),
|
|
135
133
|
ChannelAddress("dispatch-json", f"http://{Server.get_local_ip()}:{port}"),
|
|
136
|
-
ChannelAddress("dispatch-msgpack", f"http://{Server.get_local_ip()}:{port}")
|
|
134
|
+
ChannelAddress("dispatch-msgpack", f"http://{Server.get_local_ip()}:{port}"),
|
|
135
|
+
ChannelAddress("dispatch-protobuf", f"http://{Server.get_local_ip()}:{port}")
|
|
137
136
|
]
|
|
138
137
|
|
|
139
138
|
def startup(self) -> None:
|
|
@@ -156,8 +155,8 @@ class ServerModule:
|
|
|
156
155
|
# return YamlConfigurationSource(f"{Path(__file__).parent}/config.yaml")
|
|
157
156
|
|
|
158
157
|
@create()
|
|
159
|
-
def create_server(self, service_manager: ServiceManager, component_registry: ComponentRegistry) -> FastAPIServer:
|
|
160
|
-
return FastAPIServer(self.fastapi, service_manager, component_registry)
|
|
158
|
+
def create_server(self, service_manager: ServiceManager, component_registry: ComponentRegistry, protobuf_manager: ProtobufManager) -> FastAPIServer:
|
|
159
|
+
return FastAPIServer(self.fastapi, service_manager, component_registry, protobuf_manager)
|
|
161
160
|
|
|
162
161
|
@create()
|
|
163
162
|
def create_session_storage(self) -> SessionManager.Storage:
|
|
@@ -2,19 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
[project]
|
|
4
4
|
name = "aspyx_service"
|
|
5
|
-
version = "0.
|
|
5
|
+
version = "0.11.1"
|
|
6
6
|
description = "Aspyx Service framework"
|
|
7
7
|
authors = [{ name = "Andreas Ernst", email = "andreas.ernst7@gmail.com" }]
|
|
8
8
|
readme = "README.md"
|
|
9
9
|
license = { file = "LICENSE" }
|
|
10
10
|
requires-python = ">=3.9"
|
|
11
11
|
dependencies = [
|
|
12
|
-
"aspyx>=1.
|
|
12
|
+
"aspyx>=1.7.0",
|
|
13
13
|
"python-consul2~=0.1.5",
|
|
14
14
|
"fastapi~=0.115.13",
|
|
15
15
|
"httpx~=0.28.1",
|
|
16
16
|
"msgpack~=1.1.1",
|
|
17
|
-
"uvicorn[standard]"
|
|
17
|
+
"uvicorn[standard]",
|
|
18
|
+
"protobuf~=5.29.4"
|
|
18
19
|
]
|
|
19
20
|
|
|
20
21
|
[build-system]
|
|
@@ -12,6 +12,7 @@ from .healthcheck import health_checks, health_check, HealthCheckManager, Health
|
|
|
12
12
|
from .restchannel import RestChannel, post, get, put, delete, QueryParam, Body, rest
|
|
13
13
|
from .session import Session, SessionManager, SessionContext
|
|
14
14
|
from .authorization import AuthorizationManager, AbstractAuthorizationFactory
|
|
15
|
+
from .protobuf import ProtobufManager
|
|
15
16
|
|
|
16
17
|
@module()
|
|
17
18
|
class ServiceModule:
|
|
@@ -48,6 +49,10 @@ __all__ = [
|
|
|
48
49
|
"MissingTokenException",
|
|
49
50
|
"AuthorizationException",
|
|
50
51
|
|
|
52
|
+
# protobuf
|
|
53
|
+
|
|
54
|
+
"ProtobufManager",
|
|
55
|
+
|
|
51
56
|
# authorization
|
|
52
57
|
|
|
53
58
|
"AuthorizationManager",
|
|
@@ -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
|
|
|
@@ -291,16 +271,11 @@ class DispatchJSONChannel(HTTPXChannel):
|
|
|
291
271
|
def invoke(self, invocation: DynamicProxy.Invocation):
|
|
292
272
|
service_name = self.service_names[invocation.type] # map type to registered service name
|
|
293
273
|
|
|
294
|
-
request
|
|
295
|
-
"method": f"{self.component_descriptor.name}:{service_name}:{invocation.method.__name__}"
|
|
296
|
-
|
|
274
|
+
request = {
|
|
275
|
+
"method": f"{self.component_descriptor.name}:{service_name}:{invocation.method.__name__}",
|
|
276
|
+
"args": self.serialize_args(invocation)
|
|
297
277
|
}
|
|
298
278
|
|
|
299
|
-
if self.optimize_serialization:
|
|
300
|
-
request["args"] = self.serialize_args(invocation)
|
|
301
|
-
else:
|
|
302
|
-
request["args"] = self.to_dict(invocation.args)
|
|
303
|
-
|
|
304
279
|
try:
|
|
305
280
|
http_result = self.request( "post", f"{self.get_url()}/invoke", json=request, timeout=self.timeout)
|
|
306
281
|
result = http_result.json()
|
|
@@ -317,15 +292,11 @@ class DispatchJSONChannel(HTTPXChannel):
|
|
|
317
292
|
|
|
318
293
|
async def invoke_async(self, invocation: DynamicProxy.Invocation):
|
|
319
294
|
service_name = self.service_names[invocation.type] # map type to registered service name
|
|
320
|
-
request
|
|
321
|
-
"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)
|
|
322
298
|
}
|
|
323
299
|
|
|
324
|
-
if self.optimize_serialization:
|
|
325
|
-
request["args"] = self.serialize_args(invocation)
|
|
326
|
-
else:
|
|
327
|
-
request["args"] = self.to_dict(invocation.args)
|
|
328
|
-
|
|
329
300
|
try:
|
|
330
301
|
data = await self.request_async("post", f"{self.get_url()}/invoke", json=request, timeout=self.timeout)
|
|
331
302
|
result = data.json()
|
|
@@ -361,15 +332,11 @@ class DispatchMSPackChannel(HTTPXChannel):
|
|
|
361
332
|
|
|
362
333
|
def invoke(self, invocation: DynamicProxy.Invocation):
|
|
363
334
|
service_name = self.service_names[invocation.type] # map type to registered service name
|
|
364
|
-
request
|
|
365
|
-
"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)
|
|
366
338
|
}
|
|
367
339
|
|
|
368
|
-
if self.optimize_serialization:
|
|
369
|
-
request["args"] = self.serialize_args(invocation)
|
|
370
|
-
else:
|
|
371
|
-
request["args"] = self.to_dict(invocation.args)
|
|
372
|
-
|
|
373
340
|
try:
|
|
374
341
|
packed = msgpack.packb(request, use_bin_type=True)
|
|
375
342
|
|
|
@@ -397,10 +364,11 @@ class DispatchMSPackChannel(HTTPXChannel):
|
|
|
397
364
|
if "invalid_token" in www_auth:
|
|
398
365
|
if 'expired' in www_auth:
|
|
399
366
|
raise TokenExpiredException() from e
|
|
400
|
-
|
|
367
|
+
|
|
368
|
+
if 'missing' in www_auth:
|
|
401
369
|
raise MissingTokenException() from e
|
|
402
|
-
|
|
403
|
-
|
|
370
|
+
|
|
371
|
+
raise InvalidTokenException() from e
|
|
404
372
|
|
|
405
373
|
raise RemoteServiceException(str(e)) from e
|
|
406
374
|
except httpx.HTTPError as e:
|
|
@@ -417,15 +385,11 @@ class DispatchMSPackChannel(HTTPXChannel):
|
|
|
417
385
|
|
|
418
386
|
async def invoke_async(self, invocation: DynamicProxy.Invocation):
|
|
419
387
|
service_name = self.service_names[invocation.type] # map type to registered service name
|
|
420
|
-
request
|
|
421
|
-
"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)
|
|
422
391
|
}
|
|
423
392
|
|
|
424
|
-
if self.optimize_serialization:
|
|
425
|
-
request["args"] = self.serialize_args(invocation)
|
|
426
|
-
else:
|
|
427
|
-
request["args"] = self.to_dict(invocation.args)
|
|
428
|
-
|
|
429
393
|
try:
|
|
430
394
|
packed = msgpack.packb(request, use_bin_type=True)
|
|
431
395
|
|
|
@@ -15,7 +15,7 @@ from aspyx.reflection import Decorators, TypeDescriptor
|
|
|
15
15
|
|
|
16
16
|
def health_checks():
|
|
17
17
|
"""
|
|
18
|
-
Instances of classes that are annotated with @
|
|
18
|
+
Instances of classes that are annotated with @health_checks contain healt mehtods.
|
|
19
19
|
"""
|
|
20
20
|
def decorator(cls):
|
|
21
21
|
Decorators.add(cls, health_checks)
|
|
@@ -31,7 +31,7 @@ def health_checks():
|
|
|
31
31
|
|
|
32
32
|
def health_check(name="", cache = 0, fail_if_slower_than = 0):
|
|
33
33
|
"""
|
|
34
|
-
Methods annotated with `@
|
|
34
|
+
Methods annotated with `@health_check` specify health checks that will be executed.
|
|
35
35
|
"""
|
|
36
36
|
def decorator(func):
|
|
37
37
|
Decorators.add(func, health_check, name, cache, fail_if_slower_than)
|