aspyx-service 0.11.2__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.
@@ -0,0 +1,136 @@
1
+ """
2
+ session related module
3
+ """
4
+ from abc import ABC, abstractmethod
5
+ import contextvars
6
+ from typing import Type, Optional, Callable, Any, TypeVar
7
+ from datetime import datetime, timezone, timedelta
8
+ from cachetools import TTLCache
9
+
10
+ from aspyx.di import injectable
11
+
12
+
13
+ class Session:
14
+ """
15
+ Base class for objects covers data related to a server side session.
16
+ """
17
+ def __init__(self):
18
+ pass
19
+
20
+ T = TypeVar("T")
21
+
22
+ class SessionContext:
23
+ # class properties
24
+
25
+ # current_session = ThreadLocal[Session]()
26
+ current_session = contextvars.ContextVar("session")
27
+
28
+ @classmethod
29
+ def get(cls, type: Type[T]) -> T:
30
+ """
31
+ return the current session associated with the context
32
+ Args:
33
+ type: the session type
34
+
35
+ Returns:
36
+ the current session
37
+ """
38
+ return cls.current_session.get()
39
+
40
+ @classmethod
41
+ def set(cls, session: Session) -> None:
42
+ """
43
+ set the current session in the context
44
+ Args:
45
+ session: the session
46
+ """
47
+ cls.current_session.set(session)
48
+
49
+ @classmethod
50
+ def clear(cls) -> None:
51
+ """
52
+ delete the current session
53
+ """
54
+ cls.current_session.set(None) # clear()
55
+
56
+ @injectable()
57
+ class SessionManager(SessionContext):
58
+ """
59
+ A SessionManager controls the lifecycle of sessions and is responsible to establish a session context local.
60
+ """
61
+
62
+ # local classes
63
+
64
+ class Storage(ABC):
65
+ @abstractmethod
66
+ def store(self, token: str, session: Session, ttl_seconds: int):
67
+ pass
68
+
69
+ @abstractmethod
70
+ def read(self, token: str) -> Optional[Session]:
71
+ pass
72
+
73
+ class InMemoryStorage(Storage):
74
+ """
75
+ InMemoryStorage is a simple in-memory storage for sessions.
76
+ It uses a TTLCache to store sessions with a time-to-live.
77
+ """
78
+ # constructor
79
+
80
+ def __init__(self, max_size = 1000, ttl = 3600):
81
+ self.cache = TTLCache(maxsize=max_size, ttl=ttl)
82
+
83
+ # implement
84
+
85
+ def store(self, token: str, session: 'Session', ttl_seconds: int):
86
+ expiry_time = datetime.now(timezone.utc) + timedelta(seconds=ttl_seconds)
87
+ self.cache[token] = (session, expiry_time)
88
+
89
+ def read(self, token: str) -> Optional['Session']:
90
+ value = self.cache.get(token)
91
+ if value is None:
92
+ return None
93
+
94
+ session, expiry = value
95
+ if expiry < datetime.now(timezone.utc):
96
+ del self.cache[token]
97
+ return None
98
+
99
+ return session
100
+
101
+ # constructor
102
+
103
+ def __init__(self, storage: 'SessionManager.Storage'):
104
+ self.storage = storage
105
+ self.session_factory : Optional[Callable[[Any], Session]] = None
106
+
107
+ # public
108
+
109
+ def set_factory(self, factory: Callable[..., Session]) -> None:
110
+ """
111
+ set a factory function that will be used to create a concrete session
112
+ Args:
113
+ factory: the function
114
+ """
115
+ self.session_factory = factory
116
+
117
+ def create_session(self, *args, **kwargs) -> Session:
118
+ """
119
+ create a session given the arguments (usually a token, etc.)
120
+ Args:
121
+ args: rest args
122
+ kwargs: keyword args
123
+
124
+ Returns:
125
+ the new session
126
+ """
127
+ return self.session_factory(*args, **kwargs)
128
+
129
+ def store_session(self, token: str, session: Session, expiry: datetime):
130
+ now = datetime.now(timezone.utc)
131
+ ttl_seconds = max(int((expiry - now).total_seconds()), 0)
132
+
133
+ self.storage.store(token, session, ttl_seconds)
134
+
135
+ def read_session(self, token: str) -> Optional[Session]:
136
+ return self.storage.read(token)
@@ -0,0 +1,555 @@
1
+ Metadata-Version: 2.4
2
+ Name: aspyx_service
3
+ Version: 0.11.2
4
+ Summary: Aspyx Service framework
5
+ Author-email: Andreas Ernst <andreas.ernst7@gmail.com>
6
+ License: MIT License
7
+
8
+ Copyright (c) 2025 Andreas Ernst
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+ License-File: LICENSE
28
+ Requires-Python: >=3.9
29
+ Requires-Dist: aspyx>=1.9.1
30
+ Requires-Dist: fastapi~=0.115.13
31
+ Requires-Dist: httpx~=0.28.1
32
+ Requires-Dist: msgpack~=1.1.1
33
+ Requires-Dist: protobuf~=5.29.4
34
+ Requires-Dist: python-consul2~=0.1.5
35
+ Requires-Dist: uvicorn[standard]
36
+ Description-Content-Type: text/markdown
37
+
38
+ # aspyx
39
+
40
+ ![Pylint](https://github.com/coolsamson7/aspyx/actions/workflows/pylint.yml/badge.svg)
41
+ ![Build Status](https://github.com/coolsamson7/aspyx/actions/workflows/ci.yml/badge.svg)
42
+ ![Python Versions](https://img.shields.io/badge/python-3.9%20|%203.10%20|%203.11%20|%203.12-blue)
43
+ ![License](https://img.shields.io/github/license/coolsamson7/aspyx)
44
+ ![coverage](https://img.shields.io/badge/coverage-94%25-brightgreen)
45
+ [![PyPI](https://img.shields.io/pypi/v/aspyx)](https://pypi.org/project/aspyx/)
46
+ [![Docs](https://img.shields.io/badge/docs-online-blue?logo=github)](https://coolsamson7.github.io/aspyx/index/introduction)
47
+
48
+ ![image](https://github.com/user-attachments/assets/e808210a-b1a4-4fd0-93f1-b5f9845fa520)
49
+
50
+ # Service
51
+
52
+ - [Introduction](#introduction)
53
+ - [Features](#features)
54
+ - [Service and Component declaration](#service-and-component-declaration)
55
+ - [Service and Component implementation](#service-and-component-implementation)
56
+ - [Health Checks](#health-checks)
57
+ - [Service Manager](#service-manager)
58
+ - [Component Registry](#component-registry)
59
+ - [Channels](#channels)
60
+ - [Performance](#performance)
61
+ - [Rest Calls](#rest-calls)
62
+ - [Intercepting calls](#intercepting-calls)
63
+ - [FastAPI server](#fastapi-server)
64
+ - [Session](#session)
65
+ - [Authorization](#authorization)
66
+ - [Implementing Channels](#implementing-channels)
67
+ - [Version History](#version-history)
68
+
69
+ ## Introduction
70
+
71
+ The Aspyx service library is built on top of the DI core framework and adds a microservice based architecture,
72
+ that lets you deploy, discover and call services with different remoting protocols and pluggable discovery services.
73
+
74
+ The basic design consists of four different concepts:
75
+
76
+ **Service**
77
+
78
+ defines a group of methods that can be called either locally or remotely.
79
+ These methods represent the functional interface exposed to clients — similar to an interface in traditional programming
80
+
81
+ **Component**
82
+
83
+ a component bundles one or more services and declares the channels (protocols) used to expose them.
84
+ Think of a component as a deployment unit or module.
85
+
86
+ **Component Registry**
87
+
88
+ acts as the central directory for managing available components.
89
+ It allows the framework to register, discover, and resolve components and their services.
90
+
91
+ **Channel**
92
+
93
+ is a pluggable transport layer that defines how service method invocations are transmitted and handled.
94
+
95
+ Let's look at the "interface" layer first.
96
+
97
+ **Example**:
98
+ ```python
99
+ @service(name="test-service", description="test service")
100
+ class TestService(Service):
101
+ @abstractmethod
102
+ def hello(self, message: str) -> str:
103
+ pass
104
+
105
+ @component(name="test-component", services =[TestService])
106
+ class TestComponent(Component):
107
+ pass
108
+ ```
109
+
110
+ After booting the DI infrastructure with a main module we could already call a service:
111
+
112
+ **Example**:
113
+
114
+ ```python
115
+ @module(imports=[ServiceModule])
116
+ class Module:
117
+ @create()
118
+ def create_registry(self) -> ConsulComponentRegistry:
119
+ return ConsulComponentRegistry(Server.port, Consul(host="localhost", port=8500)) # a consul based registry!
120
+
121
+
122
+ environment = Environment(Module)
123
+ service_manager = environment.read(ServiceManager)
124
+
125
+ service = service_manager.get_service(TestService)
126
+
127
+ service.hello("world")
128
+ ```
129
+
130
+ The technical details are completely transparent, as a dynamic proxy encapsulates the internals.
131
+
132
+ As we can also host implementations, lets look at this side as well:
133
+
134
+ ```python
135
+ @implementation()
136
+ class TestComponentImpl(AbstractComponent, TestComponent):
137
+ # implement Component
138
+
139
+ def get_addresses(self, port: int) -> list[ChannelAddress]:
140
+ return [ChannelAddress("dispatch-json", f"http://{Server.get_local_ip()}:{port}")]
141
+
142
+ @implementation()
143
+ class TestServiceImpl(TestService):
144
+ def hello(self, message: str) -> str:
145
+ return f"hello {message}"
146
+ ```
147
+
148
+ The interesting part if the `get_addresses` method that return a list of channel addresses, that can be used to execute remote calls.
149
+ In this case a channel is used that exposes a single http endpoint, that will dispatch to the correct service method.
150
+ This information is registered with the appropriate component registry and is used by other processes.
151
+
152
+ The required - `FastAPI` - infrastructure to expose those services is started with the call:
153
+
154
+ ```python
155
+ server = FastAPIServer(host="0.0.0.0", port=8000)
156
+
157
+ environment = server.boot(Module)
158
+ ```
159
+
160
+ Of course, service can also be called locally. In case of multiple possible channels, a keyword argument is used to
161
+ determine a specific channel. As a local channel has the name "local", the appropriate call is:
162
+
163
+ **Example**:
164
+
165
+ ```python
166
+ service = service_manager.get_service(TestService, preferred_channel="local")
167
+ ```
168
+
169
+ The default can be set globally with the method `set_preferred_channel(channel: str)`
170
+
171
+ Injecting services is also possible via the decorator `@inject_service(preferred_channel=""")`
172
+
173
+ **Example**:
174
+
175
+ ```python
176
+ @inject_service()
177
+ def set_service(self, service: TestService)
178
+ self.service = service
179
+ ```
180
+
181
+ ## Features
182
+
183
+ The library offers:
184
+
185
+ - sync and async support
186
+ - multiple - extensible - channel implementations supporting dataclasses and pydantic data models.
187
+ - ability to customize http calls with interceptors ( via the AOP abilities )
188
+ - `fastapi` based channels covering simple rest endpoints including `msgpack` support.
189
+ - `httpx` based clients for dispatching channels and simple rest endpoint with the help of low-level decorators.
190
+ - first registry implementation based on `consul`
191
+ - support for configurable health checks
192
+
193
+ As well as the DI and AOP core, all mechanisms are heavily optimized.
194
+ A simple benchmark resulted in message roundtrips in significanlty under a ms per call.
195
+
196
+ ## Installation
197
+
198
+ Just install from PyPI with
199
+
200
+ `pip install aspyx-service`
201
+
202
+ The library is tested with all Python version >= 3.9
203
+
204
+ Let's see some details
205
+
206
+ ## Service and Component declaration
207
+
208
+ Every service needs to inherit from the "tagging interface" `Service`
209
+
210
+ ```python
211
+ @service(name="test-service", description="test service")
212
+ class TestService(Service):
213
+ @abstractmethod
214
+ def hello(self, message: str) -> str:
215
+ pass
216
+ ```
217
+
218
+ The decorator can add a name and a description. If `name` is not set, the class name converted to snake case is used.
219
+
220
+ A component needs to derive from `Component`:
221
+
222
+ ```python
223
+ @component(services =[TestService])
224
+ class TestComponent(Component):
225
+ pass
226
+ ```
227
+
228
+ The `services` argument references a list of service interfaces that are managed by this component, meaning that they all are
229
+ exposed by the same channels.
230
+
231
+ `Component` defines the abstract methods:
232
+
233
+ - `def startup(self) -> None`
234
+ called initially after booting the system
235
+
236
+ - `def shutdown(self) -> None:`
237
+ called before shutting fown the system
238
+
239
+ - `def get_addresses(self, port: int) -> list[ChannelAddress]:`
240
+ return a list of available `ChannelAddress`es that this component exposes
241
+
242
+ - `def get_status(self) -> ComponentStatus:`
243
+ return the status of this component ( one of the `ComponentStatus` enums `VIRGIN`, `RUNNING`, and `STOPPED`)
244
+
245
+ - `async def get_health(self) -> HealthCheckManager.Health:`
246
+ return the health status of a component implementation.
247
+
248
+ ## Service and Component implementation
249
+
250
+ Service implementations implement the corresponding interface and are decorated with `@implementation`
251
+
252
+ ```python
253
+ @implementation()
254
+ class TestServiceImpl(TestService):
255
+ pass
256
+ ```
257
+
258
+ The constructor is required since the instances are managed by the DI framework.
259
+
260
+ Component implementations derive from the interface and the abstract base class `AbstractComponent`
261
+
262
+ ```python
263
+ @implementation()
264
+ class TestComponentImpl(AbstractComponent, TestComponent):
265
+ # implement Component
266
+
267
+ def get_addresses(self, port: int) -> list[ChannelAddress]:
268
+ return [ChannelAddress("dispatch-json", f"http://{Server.get_local_ip()}:{port}")]
269
+ ```
270
+
271
+ As a minimum you have to declare the constructor and the `get_addresses` method, that exposes channel addresses
272
+
273
+ ## Health Checks
274
+
275
+ Every component can declare a HTTP health endpoint and the corresponding logic to compute the current status.
276
+
277
+ Two additional things have to be done:
278
+
279
+ - adding a `@health(<endpoint>)` decorator to the class
280
+ - implementing the `get_health()` method that returns a `HealthCheckManager.Health`
281
+
282
+ While you can instantiate the `Health` class directly via
283
+
284
+ ```
285
+ HealthCheckManager.Health(HealtStatus.OK)
286
+ ```
287
+
288
+ it typically makes more sense to let the system execute a number of configured checks and compute the overall result automatically.
289
+
290
+ For this purpose injectable classes can be decorated with `@health_checks()` that contain methods in turn decorated with `@health_check`
291
+
292
+ **Example**:
293
+
294
+ ```python
295
+ @health_checks()
296
+ @injectable()
297
+ class Checks:
298
+ def __init__(self):
299
+ pass # normally, we would inject stuff here
300
+
301
+ # checks
302
+
303
+ @health_check(fail_if_slower_than=1)
304
+ def check_performance(self, result: HealthCheckManager.Result):
305
+ ... # should be done in under a second
306
+
307
+ @health_check(name="check", cache=10)
308
+ def check(self, result: HealthCheckManager.Result):
309
+ ok = ...
310
+ result.set_status(if ok HealthStatus.OK else HealthStatus.ERROR)
311
+ ```
312
+
313
+ The methods are expected to have a single parameter of type `HealthCheckManager.Result` that can be used to set the status including detail information with
314
+
315
+ ```
316
+ set_status(status: HealthStatus, details = "")
317
+ ```
318
+
319
+ When called, the default is already `OK`.
320
+
321
+ The decorator accepts a couple of parameters:
322
+
323
+ - `fail_if_slower_than=0`
324
+ time in `s` that the check is expected to take as a maximum. As soon as the time is exceeded, the status is set to `ERROR`
325
+ - `cache`
326
+ time in 's' that the last result is cached. This is done in order to prevent health-checks putting even more strain on a heavily used system.
327
+
328
+ ## Service Manager
329
+
330
+ `ServiceManager` is the central class used to retrieve service proxies.
331
+
332
+ ```python
333
+ def get_service(self, service_type: Type[T], preferred_channel="") -> T
334
+ ```
335
+
336
+ - `type` is the requested service interface
337
+ - `preferred_channel` the name of the preferred channel.
338
+
339
+ If not specified, the first registered channel is used, which btw. is a local channel - called `local` - in case of implementing services.
340
+
341
+ ## Component Registry
342
+
343
+ The component registry is the place where component implementations are registered and retrieved.
344
+
345
+ In addition to a `LocalComponentRegistry` ( which is used for testing purposes ) the only implementation is
346
+
347
+ `ConsulComponentRegistry`
348
+
349
+ Constructor arguments are
350
+
351
+ - `port: int` the own port
352
+ - `consul: Consul` the consul instance
353
+
354
+ The component registry is also responsible to execute regular health-checks to track component healths.
355
+ As soon as - in our case consul - decides that a component is not alive anymore, it will notify the clients via regular heartbeats about address changes
356
+ which will be propagated to channels talking to the appropriate component.
357
+
358
+ Currently, this only affects the list of possible URLs which are required by the channels!
359
+
360
+ ## Channels
361
+
362
+ Channels implement the possible transport layer protocols. In the sense of a dynamic proxy, they are the invocation handlers!
363
+
364
+ Several channels are implemented:
365
+
366
+ - `dispatch-json`
367
+ channel that posts generic `Request` objects via a `invoke` POST-call
368
+ - `dispatch-msgpack`
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
372
+ - `rest`
373
+ channel that executes regular rest-calls as defined by a couple of decorators.
374
+
375
+ The `dispatch`channels have the big advantage, that you don`t have to deal with additional http decorators!
376
+
377
+ All channels react on changed URLs as provided by the component registry.
378
+
379
+ A so called `URLSelector` is used internally to provide URLs for every single call. Two subclasses exist that offer a different logic
380
+
381
+ - `FirstURLSelector` always returns the first URL of the list of possible URLs
382
+ - `RoundRobinURLSelector` switches sequentially between all URLs.
383
+
384
+ To customize the behavior, an `around` advice can be implemented easily:
385
+
386
+ **Example**:
387
+
388
+ ```python
389
+ @advice
390
+ class ChannelAdvice:
391
+ @around(methods().named("customize").of_type(Channel))
392
+ def customize_channel(self, invocation: Invocation):
393
+ channel = cast(Channel, invocation.args[0])
394
+
395
+ channel.select_round_robin() # or select_first_url()
396
+
397
+ return invocation.proceed()
398
+ ```
399
+
400
+ ### Performance
401
+
402
+ I benchmarked the different implementations with a recursive dataclass as an argument and return value.
403
+ The avg response times - on a local server - where all below 1ms per call.
404
+
405
+ - rest calls are the slowest ( about 0.7ms )
406
+ - dispatching-json 20% faster
407
+ - dispatching-msgpack 30% faster
408
+ - dispatching protobuf
409
+
410
+ The biggest advantage of the dispatching flavors is, that you don't have to worry about the additional decorators!
411
+
412
+ ### Rest Calls
413
+
414
+ Invoking rest calls requires decorators and some marker annotations.
415
+
416
+ **Example**:
417
+
418
+ ```python
419
+ @service()
420
+ @rest("/api")
421
+ class TestService(Service):
422
+ @get("/hello/{message}")
423
+ def hello(self, message: str) -> str:
424
+ pass
425
+
426
+ @post("/post/")
427
+ def set_data(self, data: Body(Data)) -> Data:
428
+ pass
429
+ ```
430
+
431
+ The decorators `get`, `put`, `post` and `delete` specify the methods.
432
+
433
+ If the class is decorated with `@rest(<prefix>)`, the corresponding prefix will be appended at the beginning.
434
+
435
+ Additional annotations are
436
+ - `Body` the post body
437
+ - `QueryParam`marked for query params
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
+
444
+ ### Intercepting calls
445
+
446
+ The client side HTTP calling is done with `httpx` instances of type `Httpx.Client` or `Httpx.AsyncClient`.
447
+
448
+ To add the possibility to add interceptors - for token handling, etc. - the channel base class `HTTPXChannel` defines
449
+ the methods `make_client()` and `make_async_client` that can be modified with an around advice.
450
+
451
+ **Example**:
452
+
453
+ ```python
454
+ class InterceptingClient(httpx.Client):
455
+ # constructor
456
+
457
+ def __init__(self, *args, **kwargs):
458
+ self.token_provider = ...
459
+ super().__init__(*args, **kwargs)
460
+
461
+ # override
462
+
463
+ def request(self, method, url, *args, **kwargs):
464
+ headers = kwargs.pop("headers", {})
465
+ headers["Authorization"] = f"Bearer {self.token_provider()}"
466
+ kwargs["headers"] = headers
467
+
468
+ return super().request(method, url, *args, **kwargs)
469
+
470
+ @advice
471
+ class ChannelAdvice:
472
+ def __init__(self):
473
+ pass
474
+
475
+ @around(methods().named("make_client").of_type(HTTPXChannel))
476
+ def make_client(self, invocation: Invocation):
477
+ return InterceptingClient()
478
+ ```
479
+
480
+ ## FastAPI server
481
+
482
+ The required - `FastAPI` - infrastructure to expose those services requires:
483
+
484
+ - a `FastAPI` instance
485
+ - an injectable `FastAPIServer`
486
+ - and a final `boot` call with the root module, which will return an `Environment`
487
+
488
+ ```python
489
+ fast_api = FastAPI() # so you can run it with uvicorn from command-line
490
+
491
+ @module(imports=[ServiceModule])
492
+ class Module:
493
+ def __init__(self):
494
+ pass
495
+
496
+ @create()
497
+ def create_server(self, service_manager: ServiceManager, component_registry: ComponentRegistry) -> FastAPIServer:
498
+ return FastAPIServer(fastapi, service_manager, component_registry)
499
+
500
+
501
+ environment = FastAPIServer.boot(Module, host="0.0.0.0", port=8000)
502
+ ```
503
+
504
+ This setup will also expose all service interfaces decorated with the corresponding http decorators!
505
+ No need to add any FastAPI decorators, since the mapping is already done internally!
506
+
507
+ ## Session
508
+
509
+ TODO
510
+
511
+ ## Authorization
512
+
513
+ TODO
514
+
515
+ ## Implementing Channels
516
+
517
+ To implement a new channel, you only need to derive from one of the possible base classes ( `Channel` or `HTTPXChannel` that already has a `httpx` client)
518
+ and decorate it with `@channel(<name>)`
519
+
520
+ The main methods to implement are `ìnvoke` and `ìnvoke_async`
521
+
522
+ **Example**:
523
+
524
+ ```python
525
+ @channel("fancy")
526
+ class FancyChannel(Channel):
527
+ # constructor
528
+
529
+ def __init__(self):
530
+ super().__init__()
531
+
532
+ # override
533
+
534
+ def invoke(self, invocation: DynamicProxy.Invocation):
535
+ return ...
536
+
537
+ async def invoke_async(self, invocation: DynamicProxy.Invocation):
538
+ return await ...
539
+
540
+ ```
541
+
542
+ # Version History
543
+
544
+ **0.10.0**
545
+
546
+ - first release version
547
+
548
+ **0.11.0**
549
+
550
+ - added protobuf support
551
+
552
+
553
+
554
+
555
+
@@ -0,0 +1,17 @@
1
+ aspyx_service/__init__.py,sha256=Mzt6pBhME_qDij2timEZT0emTTXRues_xXu3muhk3Jc,2642
2
+ aspyx_service/authorization.py,sha256=0B1xb0WrRaj2rcGTHVUhh6i8aA0sy7BmpYA18xI9LQA,3833
3
+ aspyx_service/channels.py,sha256=-MSOv4wh9rUy2JbxwkOKjItgYDyHyjbFfAIy-BCaIOc,15587
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=9LAQA29fHuzao-U4-XWUEozugLczG2sqYLy-AeNNRAg,9138
8
+ aspyx_service/server.py,sha256=C17Rvs3yMUW6pH7YyvmpoFsjAhWpp-8w-NpMLGbENPw,21516
9
+ aspyx_service/service.py,sha256=gOX5rbOOLJ-cSIvxa_5lqo1DzcPmDoJCBbMP-ZYKYxs,27972
10
+ aspyx_service/session.py,sha256=HjGpnmwdislc8Ur6pQbSMi2K-lvTsb9_XyO80zupiF8,3713
11
+ aspyx_service/generator/__init__.py,sha256=FdQpi__6uB56pDjVk7MVf5fV3uLh4PeS9JNI3aKVVy8,297
12
+ aspyx_service/generator/json_schema_generator.py,sha256=A26cd0-3MAUQWe_VwP5mJDaGYhV0ukU3i9SPxZxVQXI,6857
13
+ aspyx_service/generator/openapi_generator.py,sha256=81ica1j60o6FQji7z_m0U0FfEieX4cjnaJpbtD7lQM0,4348
14
+ aspyx_service-0.11.2.dist-info/METADATA,sha256=frRVfFVnqZi3fd4DC2fixRk-bG3OmZHXVllX3JsjRQ4,18127
15
+ aspyx_service-0.11.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
16
+ aspyx_service-0.11.2.dist-info/licenses/LICENSE,sha256=n4jfx_MNj7cBtPhhI7MCoB_K35cj1icP9yJ4Rh4vlvY,1070
17
+ aspyx_service-0.11.2.dist-info/RECORD,,