aspyx-service 0.10.4__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.

aspyx_service/__init__.py CHANGED
@@ -7,12 +7,11 @@ from aspyx.di import module
7
7
  from .service import AuthorizationException, MissingTokenException, RemoteServiceException, ServiceCommunicationException, TokenException, TokenExpiredException, InvalidTokenException, component_services, ServiceException, Server, Channel, ComponentDescriptor, inject_service, ChannelAddress, ChannelInstances, ServiceManager, Component, Service, AbstractComponent, ComponentStatus, ComponentRegistry, implementation, health, component, service
8
8
  from .channels import HTTPXChannel, DispatchJSONChannel, TokenContext
9
9
  from .registries import ConsulComponentRegistry
10
- from .server import FastAPIServer, RequestContext
10
+ from .server import FastAPIServer, RequestContext, ResponseContext, TokenContextMiddleware
11
11
  from .healthcheck import health_checks, health_check, HealthCheckManager, HealthStatus
12
12
  from .restchannel import RestChannel, post, get, put, delete, QueryParam, Body, rest
13
- from .session import Session, SessionManager
14
- from .authorization import AuthorizationManager, AbstractAuthorizationFactory, AuthorizationException
15
-
13
+ from .session import Session, SessionManager, SessionContext
14
+ from .authorization import AuthorizationManager, AbstractAuthorizationFactory
16
15
 
17
16
  @module()
18
17
  class ServiceModule:
@@ -53,12 +52,12 @@ __all__ = [
53
52
 
54
53
  "AuthorizationManager",
55
54
  "AbstractAuthorizationFactory",
56
- "AuthorizationException",
57
55
 
58
56
  # session
59
57
 
60
58
  "Session",
61
59
  "SessionManager",
60
+ "SessionContext",
62
61
 
63
62
  # healthcheck
64
63
 
@@ -91,10 +90,12 @@ __all__ = [
91
90
  # registries
92
91
 
93
92
  "ConsulComponentRegistry",
94
- "RequestContext",
95
93
 
96
94
  # server
97
95
 
98
96
  "FastAPIServer",
99
- "RequestContext"
97
+ "RequestContext",
98
+ "ResponseContext",
99
+ "TokenContext",
100
+ "TokenContextMiddleware",
100
101
  ]
@@ -9,13 +9,6 @@ from aspyx.di import injectable, inject, order
9
9
  from aspyx.di.aop import Invocation
10
10
  from aspyx.reflection import TypeDescriptor, Decorators
11
11
 
12
-
13
- class AuthorizationException(Exception):
14
- """
15
- Any authorization exception
16
- """
17
- pass
18
-
19
12
  def get_method_class(method):
20
13
  if inspect.ismethod(method) or inspect.isfunction(method):
21
14
  qualname = method.__qualname__
@@ -38,11 +31,10 @@ class AuthorizationManager:
38
31
  """
39
32
  Base class for authorization checks
40
33
  """
41
- def check(self, invocation: Invocation):
34
+ def authorize(self, invocation: Invocation):
42
35
  """
43
- execute the authorization check. Throws an exception in case of violations,
36
+ execute the authorization check. Throws an exception in case of violations
44
37
  """
45
- pass
46
38
 
47
39
  class AuthorizationFactory(ABC):
48
40
  """
@@ -62,7 +54,6 @@ class AuthorizationManager:
62
54
  Returns:
63
55
  an authorization check or None
64
56
  """
65
- pass
66
57
 
67
58
  # constructor
68
59
 
@@ -111,9 +102,9 @@ class AuthorizationManager:
111
102
 
112
103
  return checks
113
104
 
114
- def check(self, invocation: Invocation) -> Optional[Authorization]:
105
+ def authorize(self, invocation: Invocation):
115
106
  for check in self.get_checks(invocation.func):
116
- check.check(invocation)
107
+ check.authorize(invocation)
117
108
 
118
109
  class AbstractAuthorizationFactory(AuthorizationManager.AuthorizationFactory):
119
110
  """
aspyx_service/channels.py CHANGED
@@ -16,11 +16,11 @@ 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
20
  from .service import ServiceManager, ServiceCommunicationException, TokenExpiredException, InvalidTokenException, \
20
21
  AuthorizationException, MissingTokenException
21
22
 
22
23
  from .service import ComponentDescriptor, ChannelInstances, ServiceException, channel, Channel, RemoteServiceException
23
- from .serialization import get_deserializer, TypeDeserializer, TypeSerializer, get_serializer
24
24
 
25
25
  class TokenContext:
26
26
  """
@@ -208,10 +208,11 @@ class HTTPXChannel(Channel):
208
208
  if "invalid_token" in www_auth:
209
209
  if 'expired' in www_auth:
210
210
  raise TokenExpiredException() from e
211
- elif 'missing' in www_auth:
211
+
212
+ if 'missing' in www_auth:
212
213
  raise MissingTokenException() from e
213
- else:
214
- raise InvalidTokenException() from e
214
+
215
+ raise InvalidTokenException() from e
215
216
 
216
217
  raise AuthorizationException(str(e)) from e
217
218
  except httpx.HTTPError as e:
@@ -451,10 +452,11 @@ class DispatchMSPackChannel(HTTPXChannel):
451
452
  if "invalid_token" in www_auth:
452
453
  if 'expired' in www_auth:
453
454
  raise TokenExpiredException() from e
454
- elif 'missing' in www_auth:
455
+
456
+ if 'missing' in www_auth:
455
457
  raise MissingTokenException() from e
456
- else:
457
- raise InvalidTokenException() from e
458
+
459
+ raise InvalidTokenException() from e
458
460
 
459
461
  raise RemoteServiceException(str(e)) from e
460
462
 
@@ -6,19 +6,15 @@ import re
6
6
  from dataclasses import is_dataclass
7
7
 
8
8
  from typing import get_type_hints, TypeVar, Annotated, Callable, get_origin, get_args, Type
9
-
10
-
11
9
  from pydantic import BaseModel
12
10
 
13
- from .channels import HTTPXChannel
14
-
15
11
  from aspyx.reflection import DynamicProxy, Decorators
12
+
13
+ from .channels import HTTPXChannel
16
14
  from .service import channel, ServiceCommunicationException
17
15
 
18
16
  T = TypeVar("T")
19
17
 
20
-
21
-
22
18
  class BodyMarker:
23
19
  pass
24
20
 
aspyx_service/server.py CHANGED
@@ -1,33 +1,61 @@
1
1
  """
2
2
  FastAPI server implementation for the aspyx service framework.
3
3
  """
4
+ from __future__ import annotations
4
5
  import atexit
6
+ import functools
5
7
  import inspect
6
8
  import threading
7
9
  from typing import Type, Optional, Callable, Any
8
-
9
- from fastapi.responses import JSONResponse
10
+ import contextvars
10
11
  import msgpack
11
12
  import uvicorn
12
13
 
13
14
  from fastapi import FastAPI, APIRouter, Request as HttpRequest, Response as HttpResponse, HTTPException
14
- import contextvars
15
15
 
16
+
17
+ from fastapi.responses import JSONResponse
16
18
  from starlette.middleware.base import BaseHTTPMiddleware
17
19
 
18
- from aspyx.di import Environment, injectable, on_init, inject_environment
20
+ from aspyx.di import Environment, injectable, on_init, inject_environment, on_destroy
19
21
  from aspyx.reflection import TypeDescriptor, Decorators
22
+ from aspyx.util import get_deserializer, get_serializer
20
23
 
21
24
  from .service import ComponentRegistry
22
25
  from .healthcheck import HealthCheckManager
23
26
 
24
- from .serialization import get_deserializer
25
-
26
27
  from .service import Server, ServiceManager
27
28
  from .channels import Request, Response, TokenContext
28
29
 
29
30
  from .restchannel import get, post, put, delete, rest
30
31
 
32
+ class ResponseContext:
33
+ response_var = contextvars.ContextVar[Optional['ResponseContext.Response']]("response", default=None)
34
+
35
+ class Response:
36
+ def __init__(self):
37
+ self.cookies = {}
38
+
39
+ def set_cookie(self, key, value):
40
+ self.cookies[key] = value
41
+
42
+ @classmethod
43
+ def get(cls) -> ResponseContext.Response:
44
+ response = ResponseContext.Response()
45
+
46
+ cls.response_var.set(response)
47
+
48
+ return response
49
+
50
+ @classmethod
51
+ def is_set(cls) -> Optional[ResponseContext.Response]:
52
+ return cls.response_var.get()
53
+
54
+ @classmethod
55
+ def reset(cls) -> None:
56
+ cls.response_var.set(None)
57
+
58
+
31
59
  class RequestContext:
32
60
  """
33
61
  A request context is used to remember the current http request in the current thread
@@ -61,7 +89,7 @@ class RequestContext:
61
89
  finally:
62
90
  self.request_var.reset(token)
63
91
 
64
- class TokenMiddleware(BaseHTTPMiddleware):
92
+ class TokenContextMiddleware(BaseHTTPMiddleware):
65
93
  async def dispatch(self, request: Request, call_next):
66
94
  access_token = request.cookies.get("access_token") or request.headers.get("Authorization")
67
95
  #refresh_token = request.cookies.get("refresh_token")
@@ -74,26 +102,15 @@ class TokenMiddleware(BaseHTTPMiddleware):
74
102
  finally:
75
103
  TokenContext.clear()
76
104
 
77
- def create_server() -> FastAPI:
78
- server = FastAPI()
79
-
80
- server.add_middleware(RequestContext)
81
- server.add_middleware(TokenMiddleware)
82
-
83
- return server
84
-
85
- @injectable()
86
105
  class FastAPIServer(Server):
87
106
  """
88
107
  A server utilizing fastapi framework.
89
108
  """
90
109
 
91
- fast_api = create_server()
92
-
93
110
  # class methods
94
111
 
95
112
  @classmethod
96
- def boot(cls, module: Type, host="0.0.0.0", port=8000, start = True) -> 'FastAPIServer':
113
+ def boot(cls, module: Type, host="0.0.0.0", port=8000, start_thread = True) -> Environment:
97
114
  """
98
115
  boot the DI infrastructure of the supplied module and optionally start a fastapi thread given the url
99
116
  Args:
@@ -102,22 +119,23 @@ class FastAPIServer(Server):
102
119
  port: the port
103
120
 
104
121
  Returns:
105
- thr created server
122
+ the created environment
106
123
  """
124
+
107
125
  cls.port = port
108
126
 
109
127
  environment = Environment(module)
110
128
 
111
129
  server = environment.get(FastAPIServer)
112
130
 
113
- if start:
114
- server.start_fastapi(host)
131
+ if start_thread:
132
+ server.start_server(host)
115
133
 
116
- return server
134
+ return environment
117
135
 
118
136
  # constructor
119
137
 
120
- def __init__(self, service_manager: ServiceManager, component_registry: ComponentRegistry):
138
+ def __init__(self, fast_api: FastAPI, service_manager: ServiceManager, component_registry: ComponentRegistry):
121
139
  super().__init__()
122
140
 
123
141
  self.environment : Optional[Environment] = None
@@ -125,10 +143,14 @@ class FastAPIServer(Server):
125
143
  self.component_registry = component_registry
126
144
 
127
145
  self.host = "localhost"
146
+ self.fast_api = fast_api
128
147
  self.server_thread = None
129
148
 
130
149
  self.router = APIRouter()
131
150
 
151
+ self.server : Optional[uvicorn.Server] = None
152
+ self.thread : Optional[threading.Thread] = None
153
+
132
154
  # cache
133
155
 
134
156
  self.deserializers: dict[str, list[Callable]] = {}
@@ -143,6 +165,8 @@ class FastAPIServer(Server):
143
165
  def set_environment(self, environment: Environment):
144
166
  self.environment = environment
145
167
 
168
+ # lifecycle
169
+
146
170
  @on_init()
147
171
  def on_init(self):
148
172
  self.service_manager.startup(self)
@@ -162,12 +186,52 @@ class FastAPIServer(Server):
162
186
 
163
187
  atexit.register(cleanup)
164
188
 
189
+ @on_destroy()
190
+ def on_destroy(self):
191
+ if self.server is not None:
192
+ self.server.should_exit = True
193
+ self.thread.join()
194
+
165
195
  # private
166
196
 
167
197
  def add_routes(self):
168
198
  """
169
199
  add everything that looks like an http endpoint
170
200
  """
201
+
202
+ def wrap_service_method(handler, return_type):
203
+ sig = inspect.signature(handler)
204
+
205
+ @functools.wraps(handler)
206
+ async def wrapper(*args, **kwargs):
207
+ try:
208
+ result = handler(*args, **kwargs)
209
+ if inspect.iscoroutine(result):
210
+ result = await result
211
+
212
+ except HTTPException as e:
213
+ raise
214
+ except Exception as e:
215
+ result = {"error": str(e)}
216
+
217
+ json_response = JSONResponse(get_serializer(return_type)(result))
218
+
219
+ local_response = ResponseContext.is_set()
220
+ if local_response is not None:
221
+ for key, value in local_response.cookies.items():
222
+ json_response.set_cookie(key, value)
223
+
224
+ ResponseContext.reset()
225
+
226
+ return json_response
227
+
228
+ # Optionally attach response_model info for docs
229
+
230
+ wrapper.__signature__ = sig
231
+ wrapper.__annotations__ = {"return": return_type}
232
+
233
+ return wrapper
234
+
171
235
  for descriptor in self.service_manager.descriptors.values():
172
236
  if not descriptor.is_component() and descriptor.is_local():
173
237
  prefix = ""
@@ -183,25 +247,23 @@ class FastAPIServer(Server):
183
247
  if decorator is not None:
184
248
  self.router.add_api_route(
185
249
  path=prefix + decorator.args[0],
186
- endpoint=getattr(instance, method.get_name()),
250
+ endpoint=wrap_service_method(getattr(instance, method.get_name()), method.return_type),
187
251
  methods=[decorator.decorator.__name__],
188
252
  name=f"{descriptor.get_component_descriptor().name}.{descriptor.name}.{method.get_name()}",
189
253
  response_model=method.return_type,
190
254
  )
191
255
 
192
- def start_fastapi(self, host: str):
256
+ def start_server(self, host: str):
193
257
  """
194
258
  start the fastapi server in a thread
195
259
  """
196
260
  self.host = host
197
261
 
198
- config = uvicorn.Config(self.fast_api, host=self.host, port=self.port, access_log=False)
199
- server = uvicorn.Server(config)
200
-
201
- thread = threading.Thread(target=server.run, daemon=True)
202
- thread.start()
262
+ config = uvicorn.Config(self.fast_api, host=host, port=self.port, access_log=False)
203
263
 
204
- return thread
264
+ self.server = uvicorn.Server(config)
265
+ self.thread = threading.Thread(target=self.server.run, daemon=True)
266
+ self.thread.start()
205
267
 
206
268
  def get_deserializers(self, service: Type, method):
207
269
  deserializers = self.deserializers.get(method, None)
aspyx_service/session.py CHANGED
@@ -1,13 +1,13 @@
1
1
  """
2
2
  session related module
3
3
  """
4
+ from abc import ABC, abstractmethod
4
5
  import contextvars
5
6
  from typing import Type, Optional, Callable, Any, TypeVar
6
- from datetime import datetime, timezone
7
+ from datetime import datetime, timezone, timedelta
7
8
  from cachetools import TTLCache
8
9
 
9
10
  from aspyx.di import injectable
10
- from aspyx.threading import ThreadLocal
11
11
 
12
12
 
13
13
  class Session:
@@ -19,18 +19,16 @@ class Session:
19
19
 
20
20
  T = TypeVar("T")
21
21
 
22
- @injectable()
23
- class SessionManager:
24
- """
25
- A SessionManager controls the lifecycle of sessions and is responsible to establish a session thread local.
26
- """
27
- #current_session = ThreadLocal[Session]()
28
- current_session = contextvars.ContextVar("session")
22
+ class SessionContext:
23
+ # class properties
24
+
25
+ # current_session = ThreadLocal[Session]()
26
+ current_session = contextvars.ContextVar("session")
29
27
 
30
28
  @classmethod
31
- def current(cls, type: Type[T]) -> T:
29
+ def get(cls, type: Type[T]) -> T:
32
30
  """
33
- return the current session associated with the thread
31
+ return the current session associated with the context
34
32
  Args:
35
33
  type: the session type
36
34
 
@@ -40,40 +38,85 @@ class SessionManager:
40
38
  return cls.current_session.get()
41
39
 
42
40
  @classmethod
43
- def set_session(cls, session: Session) -> None:
41
+ def set(cls, session: Session) -> None:
44
42
  """
45
- set the current session in the thread context
43
+ set the current session in the context
46
44
  Args:
47
45
  session: the session
48
46
  """
49
47
  cls.current_session.set(session)
50
48
 
51
49
  @classmethod
52
- def delete_session(cls) -> None:
50
+ def clear(cls) -> None:
53
51
  """
54
52
  delete the current session
55
53
  """
56
- cls.current_session.set(None)#clear()
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
57
100
 
58
101
  # constructor
59
102
 
60
- def __init__(self):
61
- self.sessions = TTLCache(maxsize=1000, ttl=3600)
62
- self.session_creator : Optional[Callable[[Any], Session]] = None
103
+ def __init__(self, storage: 'SessionManager.Storage'):
104
+ self.storage = storage
105
+ self.session_factory : Optional[Callable[[Any], Session]] = None
63
106
 
64
107
  # public
65
108
 
66
- def set_session_factory(self, callable: Callable[..., Session]) -> None:
109
+ def set_factory(self, factory: Callable[..., Session]) -> None:
67
110
  """
68
111
  set a factory function that will be used to create a concrete session
69
112
  Args:
70
- callable: the function
113
+ factory: the function
71
114
  """
72
- self.session_creator = callable
115
+ self.session_factory = factory
73
116
 
74
117
  def create_session(self, *args, **kwargs) -> Session:
75
118
  """
76
- create a session given the argument s(usually a token, etc.)
119
+ create a session given the arguments (usually a token, etc.)
77
120
  Args:
78
121
  args: rest args
79
122
  kwargs: keyword args
@@ -81,17 +124,13 @@ class SessionManager:
81
124
  Returns:
82
125
  the new session
83
126
  """
84
- return self.session_creator(*args, **kwargs)
127
+ return self.session_factory(*args, **kwargs)
85
128
 
86
129
  def store_session(self, token: str, session: Session, expiry: datetime):
87
130
  now = datetime.now(timezone.utc)
88
131
  ttl_seconds = max(int((expiry - now).total_seconds()), 0)
89
- self.sessions[token] = (session, ttl_seconds)
90
132
 
91
- def get_session(self, token: str) -> Optional[Session]:
92
- value = self.sessions.get(token)
93
- if value is None:
94
- return None
133
+ self.storage.store(token, session, ttl_seconds)
95
134
 
96
- session, ttl = value
97
- return session
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.4
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,8 +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.3
30
- Requires-Dist: cachetools~=5.5.2
29
+ Requires-Dist: aspyx>=1.6.0
31
30
  Requires-Dist: fastapi~=0.115.13
32
31
  Requires-Dist: httpx~=0.28.1
33
32
  Requires-Dist: msgpack~=1.1.1
@@ -110,18 +109,20 @@ class TestComponent(Component):
110
109
  After booting the DI infrastructure with a main module we could already call a service:
111
110
 
112
111
  **Example**:
112
+
113
113
  ```python
114
114
  @module(imports=[ServiceModule])
115
115
  class Module:
116
116
  def __init__(self):
117
117
  pass
118
-
118
+
119
119
  @create()
120
120
  def create_registry(self) -> ConsulComponentRegistry:
121
- 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
+
122
123
 
123
124
  environment = Environment(Module)
124
- service_manager = environment.get(ServiceManager)
125
+ service_manager = environment.read(ServiceManager)
125
126
 
126
127
  service = service_manager.get_service(TestService)
127
128
 
@@ -202,6 +203,14 @@ The library offers:
202
203
  As well as the DI and AOP core, all mechanisms are heavily optimized.
203
204
  A simple benchmark resulted in message roundtrips in significanlty under a ms per call.
204
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
+
205
214
  Let's see some details
206
215
 
207
216
  ## Service and Component declaration
@@ -484,18 +493,26 @@ class ChannelAdvice:
484
493
 
485
494
  ## FastAPI server
486
495
 
487
- 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:
488
497
 
498
+ - a `FastAPI` instance
499
+ - an injectable `FastAPIServer`
500
+ - and a final `boot` call with the root module, which will return an `Environment`
489
501
 
490
502
  ```python
491
- @module()
492
- class Module():
503
+ fast_api = FastAPI() # so you can run it with uvivorn from command-line
504
+
505
+ @module(imports=[ServiceModule])
506
+ class Module:
493
507
  def __init__(self):
494
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)
495
513
 
496
- server = FastAPIServer(host="0.0.0.0", port=8000)
497
514
 
498
- environment = server.boot(Module) # will start the http server
515
+ environment = FastAPIServer.boot(Moudle, host="0.0.0.0", port=8000)
499
516
  ```
500
517
 
501
518
  This setup will also expose all service interfaces decorated with the corresponding http decorators!
@@ -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.model_validate
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.model_dump() 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,14 +0,0 @@
1
- aspyx_service/__init__.py,sha256=h9zcGzYaVdU2_mXON-k-mgYErEJ7eIs-wjNDKtet1_s,2488
2
- aspyx_service/authorization.py,sha256=vBM8uPsAZwMiTilqFZMJ101Qy37gL2Y9vdGTLp-ykFg,3983
3
- aspyx_service/channels.py,sha256=3Fv6055n1hw8HQ6MKu2BROsq6gmPdKIhayUhQiTRLic,16461
4
- aspyx_service/healthcheck.py,sha256=vjfY7s5kd5mRJynVpvAJ4BvVF7QY1xrvj94Y-m041LQ,5615
5
- aspyx_service/registries.py,sha256=bnTjKb40fbZXA52E2lDSEzCWI5_NBKZzQjc8ffufB5g,8039
6
- aspyx_service/restchannel.py,sha256=wutLGnxqMAS1oX7cc1pvN8qIIpjeBEvPz_hsKeWHVZs,8474
7
- aspyx_service/serialization.py,sha256=OrwOAUsHQyGDyhYTkTc-0v8urYMbh0_3fgkpTvNOl0o,4214
8
- aspyx_service/server.py,sha256=_LFRy1XIXTbu7CLoXsoPGQwKHkpSPDh82VV4ppahzr0,9057
9
- aspyx_service/service.py,sha256=drETAZasbYJZisnmbhAqW0-mHghJ3IWyPaU-7etxvBI,27003
10
- aspyx_service/session.py,sha256=ytWRTlnu1kDpTkLBCy_WF2i-mdffG-exIqsUQZ1Udo0,2592
11
- aspyx_service-0.10.4.dist-info/METADATA,sha256=-WA5_ta5gP3AXy8EA0JhFzoFstaS_RgeGjyglOAYqXw,17499
12
- aspyx_service-0.10.4.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
13
- aspyx_service-0.10.4.dist-info/licenses/LICENSE,sha256=n4jfx_MNj7cBtPhhI7MCoB_K35cj1icP9yJ4Rh4vlvY,1070
14
- aspyx_service-0.10.4.dist-info/RECORD,,