aspyx-service 0.10.3__py3-none-any.whl → 0.10.5__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.

@@ -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)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aspyx_service
3
- Version: 0.10.3
3
+ Version: 0.10.5
4
4
  Summary: Aspyx Service framework
5
5
  Author-email: Andreas Ernst <andreas.ernst7@gmail.com>
6
6
  License: MIT License
@@ -26,7 +26,7 @@ License: MIT License
26
26
  SOFTWARE.
27
27
  License-File: LICENSE
28
28
  Requires-Python: >=3.9
29
- Requires-Dist: aspyx>=1.5.1
29
+ Requires-Dist: aspyx>=1.6.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
@@ -60,6 +60,8 @@ Description-Content-Type: text/markdown
60
60
  - [Rest Calls](#rest-calls)
61
61
  - [Intercepting calls](#intercepting-calls)
62
62
  - [FastAPI server](#fastapi-server)
63
+ - [Session](#session)
64
+ - [Authorization](#authorization)
63
65
  - [Implementing Channels](#implementing-channels)
64
66
  - [Version History](#version-history)
65
67
 
@@ -107,18 +109,20 @@ class TestComponent(Component):
107
109
  After booting the DI infrastructure with a main module we could already call a service:
108
110
 
109
111
  **Example**:
112
+
110
113
  ```python
111
114
  @module(imports=[ServiceModule])
112
115
  class Module:
113
116
  def __init__(self):
114
117
  pass
115
-
118
+
116
119
  @create()
117
120
  def create_registry(self) -> ConsulComponentRegistry:
118
- return ConsulComponentRegistry(Server.port, Consul(host="localhost", port=8500)) # a consul based registry!
121
+ return ConsulComponentRegistry(Server.port, Consul(host="localhost", port=8500)) # a consul based registry!
122
+
119
123
 
120
124
  environment = Environment(Module)
121
- service_manager = environment.get(ServiceManager)
125
+ service_manager = environment.read(ServiceManager)
122
126
 
123
127
  service = service_manager.get_service(TestService)
124
128
 
@@ -166,10 +170,24 @@ environment = server.boot(Module)
166
170
  Of course, service can also be called locally. In case of multiple possible channels, a keyword argument is used to
167
171
  determine a specific channel. As a local channel has the name "local", the appropriate call is:
168
172
 
173
+ **Example**:
174
+
169
175
  ```python
170
176
  service = service_manager.get_service(TestService, preferred_channel="local")
171
177
  ```
172
178
 
179
+ The default can be set globally with the method `set_preferred_channel(channel: str)`
180
+
181
+ Injecting services is also possible via the decorator `@inject_service(preferred_channel=""")`
182
+
183
+ **Example**:
184
+
185
+ ```python
186
+ @inject_service()
187
+ def set_service(self, service: TestService)
188
+ self.service = service
189
+ ```
190
+
173
191
  ## Features
174
192
 
175
193
  The library offers:
@@ -185,6 +203,14 @@ The library offers:
185
203
  As well as the DI and AOP core, all mechanisms are heavily optimized.
186
204
  A simple benchmark resulted in message roundtrips in significanlty under a ms per call.
187
205
 
206
+ ## Installation
207
+
208
+ Just install from PyPI with
209
+
210
+ `pip install aspyx-service`
211
+
212
+ The library is tested with all Python version >= 3.9
213
+
188
214
  Let's see some details
189
215
 
190
216
  ## Service and Component declaration
@@ -352,12 +378,14 @@ Channels implement the possible transport layer protocols. In the sense of a dyn
352
378
  Several channels are implemented:
353
379
 
354
380
  - `dispatch-json`
355
- channel that dispatches generic `Request` objects via a `invoke` POST-call
381
+ channel that posts generic `Request` objects via a `invoke` POST-call
356
382
  - `dispatch-msgpack`
357
- channel that dispatches generic `Request` objects via a `invoke` POST-call after packing the json with msgpack
383
+ channel that posts generic `Request` objects via a `invoke` POST-call after packing the json with msgpack
358
384
  - `rest`
359
385
  channel that executes regular rest-calls as defined by a couple of decorators.
360
386
 
387
+ The `dispatch`channels have the big advantage, that you don`t have to deal with additional http decorators!
388
+
361
389
  All channels react on changed URLs as provided by the component registry.
362
390
 
363
391
  A so called `URLSelector` is used internally to provide URLs for every single call. Two subclasses exist that offer a different logic
@@ -465,23 +493,39 @@ class ChannelAdvice:
465
493
 
466
494
  ## FastAPI server
467
495
 
468
- In order to expose components via HTTP, the corresponding infrastructure in form of a FastAPI server needs to be setup.
496
+ The required - `FastAPI` - infrastructure to expose those services requires:
469
497
 
498
+ - a `FastAPI` instance
499
+ - an injectable `FastAPIServer`
500
+ - and a final `boot` call with the root module, which will return an `Environment`
470
501
 
471
502
  ```python
472
- @module()
473
- class Module():
503
+ fast_api = FastAPI() # so you can run it with uvivorn from command-line
504
+
505
+ @module(imports=[ServiceModule])
506
+ class Module:
474
507
  def __init__(self):
475
508
  pass
509
+
510
+ @create()
511
+ def create_server(self, service_manager: ServiceManager, component_registry: ComponentRegistry) -> FastAPIServer:
512
+ return FastAPIServer(fastapi, service_manager, component_registry)
476
513
 
477
- server = FastAPIServer(host="0.0.0.0", port=8000)
478
514
 
479
- environment = server.boot(Module) # will start the http server
515
+ environment = FastAPIServer.boot(Moudle, host="0.0.0.0", port=8000)
480
516
  ```
481
517
 
482
518
  This setup will also expose all service interfaces decorated with the corresponding http decorators!
483
519
  No need to add any FastAPI decorators, since the mapping is already done internally!
484
520
 
521
+ ## Session
522
+
523
+ TODO
524
+
525
+ ## Authorization
526
+
527
+ TODO
528
+
485
529
  ## Implementing Channels
486
530
 
487
531
  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)
@@ -0,0 +1,13 @@
1
+ aspyx_service/__init__.py,sha256=OWJoScdDVK1NTs9cIgImShgEdJb8TZHLBQjS11rA0Yo,2564
2
+ aspyx_service/authorization.py,sha256=0B1xb0WrRaj2rcGTHVUhh6i8aA0sy7BmpYA18xI9LQA,3833
3
+ aspyx_service/channels.py,sha256=u1afqUfcmVgLxXDXC2BYHH-dMozLcmVROuPRpypSwr8,16397
4
+ aspyx_service/healthcheck.py,sha256=vjfY7s5kd5mRJynVpvAJ4BvVF7QY1xrvj94Y-m041LQ,5615
5
+ aspyx_service/registries.py,sha256=bnTjKb40fbZXA52E2lDSEzCWI5_NBKZzQjc8ffufB5g,8039
6
+ aspyx_service/restchannel.py,sha256=0Xb8grEE8Dyx3g3ENl78DDMKa2WGjIKIPgOrpw5p9ak,8470
7
+ aspyx_service/server.py,sha256=HLMsEpiXgpF7s1r1_1iRiufAvDfrOZGvljPjpf-7RCM,11096
8
+ aspyx_service/service.py,sha256=drETAZasbYJZisnmbhAqW0-mHghJ3IWyPaU-7etxvBI,27003
9
+ aspyx_service/session.py,sha256=HjGpnmwdislc8Ur6pQbSMi2K-lvTsb9_XyO80zupiF8,3713
10
+ aspyx_service-0.10.5.dist-info/METADATA,sha256=QHq4kp7Zn9roaenqpDa_t9KqZBlr0Ku4F7621L1ZM5k,17946
11
+ aspyx_service-0.10.5.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
12
+ aspyx_service-0.10.5.dist-info/licenses/LICENSE,sha256=n4jfx_MNj7cBtPhhI7MCoB_K35cj1icP9yJ4Rh4vlvY,1070
13
+ aspyx_service-0.10.5.dist-info/RECORD,,
@@ -1,137 +0,0 @@
1
- """
2
- deserialization functions
3
- """
4
- from dataclasses import is_dataclass, fields
5
- from functools import lru_cache
6
- from typing import get_origin, get_args, Union
7
-
8
- from pydantic import BaseModel
9
-
10
- class TypeDeserializer:
11
- # constructor
12
-
13
- def __init__(self, typ):
14
- self.typ = typ
15
- self.deserializer = self._build_deserializer(typ)
16
-
17
- def __call__(self, value):
18
- return self.deserializer(value)
19
-
20
- # internal
21
-
22
- def _build_deserializer(self, typ):
23
- origin = get_origin(typ)
24
- args = get_args(typ)
25
-
26
- if origin is Union:
27
- deserializers = [TypeDeserializer(arg) for arg in args if arg is not type(None)]
28
- def deser_union(value):
29
- if value is None:
30
- return None
31
- for d in deserializers:
32
- try:
33
- return d(value)
34
- except Exception:
35
- continue
36
- return value
37
- return deser_union
38
-
39
- if isinstance(typ, type) and issubclass(typ, BaseModel):
40
- return typ.parse_obj
41
-
42
- if is_dataclass(typ):
43
- field_deserializers = {f.name: TypeDeserializer(f.type) for f in fields(typ)}
44
- def deser_dataclass(value):
45
- if is_dataclass(value):
46
- return value
47
-
48
- return typ(**{
49
- k: field_deserializers[k](v) for k, v in value.items()
50
- })
51
- return deser_dataclass
52
-
53
- if origin is list:
54
- item_deser = TypeDeserializer(args[0]) if args else lambda x: x
55
- return lambda v: [item_deser(item) for item in v]
56
-
57
- if origin is dict:
58
- key_deser = TypeDeserializer(args[0]) if args else lambda x: x
59
- val_deser = TypeDeserializer(args[1]) if len(args) > 1 else lambda x: x
60
- return lambda v: {key_deser(k): val_deser(val) for k, val in v.items()}
61
-
62
- # Fallback
63
- return lambda v: v
64
-
65
- class TypeSerializer:
66
- def __init__(self, typ):
67
- self.typ = typ
68
- self.serializer = self._build_serializer(typ)
69
-
70
- def __call__(self, value):
71
- return self.serializer(value)
72
-
73
- def _build_serializer(self, typ):
74
- origin = get_origin(typ)
75
- args = get_args(typ)
76
-
77
- if origin is Union:
78
- serializers = [TypeSerializer(arg) for arg in args if arg is not type(None)]
79
- def ser_union(value):
80
- if value is None:
81
- return None
82
- for s in serializers:
83
- try:
84
- return s(value)
85
- except Exception:
86
- continue
87
- return value
88
- return ser_union
89
-
90
- if isinstance(typ, type) and issubclass(typ, BaseModel):
91
- return lambda v: v.dict() if v is not None else None
92
-
93
- if is_dataclass(typ):
94
- field_serializers = {f.name: TypeSerializer(f.type) for f in fields(typ)}
95
- def ser_dataclass(obj):
96
- if obj is None:
97
- return None
98
- return {k: field_serializers[k](getattr(obj, k)) for k in field_serializers}
99
- return ser_dataclass
100
-
101
- if origin is list:
102
- item_ser = TypeSerializer(args[0]) if args else lambda x: x
103
- return lambda v: [item_ser(item) for item in v] if v is not None else None
104
-
105
- if origin is dict:
106
- key_ser = TypeSerializer(args[0]) if args else lambda x: x
107
- val_ser = TypeSerializer(args[1]) if len(args) > 1 else lambda x: x
108
- return lambda v: {key_ser(k): val_ser(val) for k, val in v.items()} if v is not None else None
109
-
110
- # Fallback: primitive Typen oder unbekannt
111
- return lambda v: v
112
-
113
- @lru_cache(maxsize=512)
114
- def get_deserializer(typ):
115
- """
116
- return a function that is able to deserialize a value of the specified type
117
-
118
- Args:
119
- typ: the type
120
-
121
- Returns:
122
-
123
- """
124
- return TypeDeserializer(typ)
125
-
126
- @lru_cache(maxsize=512)
127
- def get_serializer(typ):
128
- """
129
- return a function that is able to deserialize a value of the specified type
130
-
131
- Args:
132
- typ: the type
133
-
134
- Returns:
135
-
136
- """
137
- return TypeSerializer(typ)
@@ -1,12 +0,0 @@
1
- aspyx_service/__init__.py,sha256=6t24VPrSCG83EAvYlqCKdEcEbyCY3vrSb5GoAx01Ymg,1662
2
- aspyx_service/channels.py,sha256=p5JIyo7eWyBiR2xQrfsEvq2L89FzeFT1tqKYhvXQbXs,9035
3
- aspyx_service/healthcheck.py,sha256=8ZPSkAx6ypoYaxDMkJT_MtL2pEN2LcUAishAWPCy-3I,5624
4
- aspyx_service/registries.py,sha256=JSsD32F8VffZMHyEDuapEWtvmem5SK9kR6bgsFRLFZQ,8002
5
- aspyx_service/restchannel.py,sha256=Q_7RURjZZW7N2LBLY9BL7qKyS_A62X7yFoGUoE-_0YY,9103
6
- aspyx_service/serialization.py,sha256=GEgfg1cNSOJ_oe0gEm0ajzugLyUmPiEsp9Qz6Fu4vkA,4207
7
- aspyx_service/server.py,sha256=PHKx3O90jnxm8dA4z331_51znhU-HlRB2Fa1AikmFXA,7052
8
- aspyx_service/service.py,sha256=Odl6_nvOOJ2lFjqCLJD8KLPt-sCG55VtQQiKTyNEOPs,26062
9
- aspyx_service-0.10.3.dist-info/METADATA,sha256=MrWNfy3T2m4G5MyKlV4Spr2yi6N_hE1VLRT3vNJ6Hm0,16955
10
- aspyx_service-0.10.3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
11
- aspyx_service-0.10.3.dist-info/licenses/LICENSE,sha256=n4jfx_MNj7cBtPhhI7MCoB_K35cj1icP9yJ4Rh4vlvY,1070
12
- aspyx_service-0.10.3.dist-info/RECORD,,