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.

Files changed (34) hide show
  1. {aspyx_service-0.10.7 → aspyx_service-0.11.1}/PKG-INFO +21 -31
  2. {aspyx_service-0.10.7 → aspyx_service-0.11.1}/README.md +18 -29
  3. {aspyx_service-0.10.7 → aspyx_service-0.11.1}/performance-test/main.py +4 -4
  4. {aspyx_service-0.10.7 → aspyx_service-0.11.1}/performance-test/performance-test.py +28 -1
  5. {aspyx_service-0.10.7 → aspyx_service-0.11.1}/performance-test/server.py +6 -7
  6. {aspyx_service-0.10.7 → aspyx_service-0.11.1}/pyproject.toml +4 -3
  7. {aspyx_service-0.10.7 → aspyx_service-0.11.1}/src/aspyx_service/__init__.py +5 -0
  8. {aspyx_service-0.10.7 → aspyx_service-0.11.1}/src/aspyx_service/channels.py +24 -60
  9. {aspyx_service-0.10.7 → aspyx_service-0.11.1}/src/aspyx_service/healthcheck.py +2 -2
  10. aspyx_service-0.11.1/src/aspyx_service/protobuf.py +1093 -0
  11. {aspyx_service-0.10.7 → aspyx_service-0.11.1}/src/aspyx_service/restchannel.py +23 -3
  12. {aspyx_service-0.10.7 → aspyx_service-0.11.1}/src/aspyx_service/server.py +97 -54
  13. {aspyx_service-0.10.7 → aspyx_service-0.11.1}/src/aspyx_service/service.py +37 -14
  14. {aspyx_service-0.10.7 → aspyx_service-0.11.1}/tests/common.py +60 -44
  15. aspyx_service-0.11.1/tests/other.py +8 -0
  16. {aspyx_service-0.10.7 → aspyx_service-0.11.1}/tests/test_async_service.py +16 -8
  17. aspyx_service-0.11.1/tests/test_proto.py +157 -0
  18. {aspyx_service-0.10.7 → aspyx_service-0.11.1}/tests/test_service.py +33 -13
  19. {aspyx_service-0.10.7 → aspyx_service-0.11.1}/.gitignore +0 -0
  20. {aspyx_service-0.10.7 → aspyx_service-0.11.1}/LICENSE +0 -0
  21. {aspyx_service-0.10.7 → aspyx_service-0.11.1}/performance-test/__init__.py +0 -0
  22. {aspyx_service-0.10.7 → aspyx_service-0.11.1}/performance-test/client.py +0 -0
  23. {aspyx_service-0.10.7 → aspyx_service-0.11.1}/performance-test/config.yaml +0 -0
  24. {aspyx_service-0.10.7 → aspyx_service-0.11.1}/performance-test/readme.txt +0 -0
  25. {aspyx_service-0.10.7 → aspyx_service-0.11.1}/performance-test/start_server_8000.sh +0 -0
  26. {aspyx_service-0.10.7 → aspyx_service-0.11.1}/performance-test/start_server_8001.sh +0 -0
  27. {aspyx_service-0.10.7 → aspyx_service-0.11.1}/src/aspyx_service/authorization.py +0 -0
  28. {aspyx_service-0.10.7 → aspyx_service-0.11.1}/src/aspyx_service/registries.py +0 -0
  29. {aspyx_service-0.10.7 → aspyx_service-0.11.1}/src/aspyx_service/session.py +0 -0
  30. {aspyx_service-0.10.7 → aspyx_service-0.11.1}/tests/__init__.py +0 -0
  31. {aspyx_service-0.10.7 → aspyx_service-0.11.1}/tests/config.yaml +0 -0
  32. {aspyx_service-0.10.7 → aspyx_service-0.11.1}/tests/test_healthcheck.py +0 -0
  33. {aspyx_service-0.10.7 → aspyx_service-0.11.1}/tests/test_jwt.py +0 -0
  34. {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.10.7
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.6.0
29
+ Requires-Dist: aspyx>=1.7.0
30
30
  Requires-Dist: fastapi~=0.115.13
31
31
  Requires-Dist: httpx~=0.28.1
32
32
  Requires-Dist: msgpack~=1.1.1
33
+ Requires-Dist: protobuf~=5.29.4
33
34
  Requires-Dist: python-consul2~=0.1.5
34
35
  Requires-Dist: uvicorn[standard]
35
36
  Description-Content-Type: text/markdown
@@ -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
- def __init__(self):
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 uvivorn from command-line
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(Moudle, host="0.0.0.0", port=8000)
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
- def __init__(self):
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 uvivorn from command-line
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(Moudle, host="0.0.0.0", port=8000)
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.DEBUG, levels={
14
+ Logger.configure(default_level=logging.INFO, levels={
15
15
  "httpx": logging.ERROR,
16
- "aspyx.di": logging.ERROR,
17
- "aspyx.di.aop": logging.ERROR,
18
- "aspyx.service": logging.ERROR
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, Dict, cast
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.10.7"
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.6.0",
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: dict[Callable, list[Callable]] = {}
108
- self.deserializers: dict[Callable, Callable] = {}
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[method] = 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[method] = deserializer
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 : dict = {
295
- "method": f"{self.component_descriptor.name}:{service_name}:{invocation.method.__name__}"
296
- #"args": invocation.args
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 : dict = {
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: dict = {
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
- elif 'missing' in www_auth:
367
+
368
+ if 'missing' in www_auth:
401
369
  raise MissingTokenException() from e
402
- else:
403
- raise InvalidTokenException() from e
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: dict = {
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 @injectable can be created by an Environment.
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 `@on_init` will be called when the instance is created.
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)