aspyx-service 0.10.4__py3-none-any.whl → 0.10.6__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,98 @@
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
9
+ import typing
10
+ from datetime import datetime
7
11
  from typing import Type, Optional, Callable, Any
8
-
9
- from fastapi.responses import JSONResponse
12
+ import contextvars
10
13
  import msgpack
11
14
  import uvicorn
12
15
 
13
16
  from fastapi import FastAPI, APIRouter, Request as HttpRequest, Response as HttpResponse, HTTPException
14
- import contextvars
15
17
 
18
+
19
+ from fastapi.responses import JSONResponse
16
20
  from starlette.middleware.base import BaseHTTPMiddleware
17
21
 
18
- from aspyx.di import Environment, injectable, on_init, inject_environment
22
+ from aspyx.di import Environment, injectable, on_init, inject_environment, on_destroy
19
23
  from aspyx.reflection import TypeDescriptor, Decorators
24
+ from aspyx.util import get_deserializer, get_serializer
20
25
 
21
26
  from .service import ComponentRegistry
22
27
  from .healthcheck import HealthCheckManager
23
28
 
24
- from .serialization import get_deserializer
25
-
26
29
  from .service import Server, ServiceManager
27
30
  from .channels import Request, Response, TokenContext
28
31
 
29
32
  from .restchannel import get, post, put, delete, rest
30
33
 
34
+ class ResponseContext:
35
+ response_var = contextvars.ContextVar[Optional['ResponseContext.Response']]("response", default=None)
36
+
37
+ class Response:
38
+ def __init__(self):
39
+ self.cookies = {}
40
+ self.delete_cookies = {}
41
+
42
+ def delete_cookie(self,
43
+ key: str,
44
+ path: str = "/",
45
+ domain: str | None = None,
46
+ secure: bool = False,
47
+ httponly: bool = False,
48
+ samesite: typing.Literal["lax", "strict", "none"] | None = "lax",
49
+ ):
50
+ self.delete_cookies[key] = {
51
+ "path": path,
52
+ "domain": domain,
53
+ "secure": secure,
54
+ "httponly": httponly,
55
+ "samesite": samesite
56
+ }
57
+
58
+ def set_cookie(self,
59
+ key: str,
60
+ value: str = "",
61
+ max_age: int | None = None,
62
+ expires: datetime | str | int | None = None,
63
+ path: str | None = "/",
64
+ domain: str | None = None,
65
+ secure: bool = False,
66
+ httponly: bool = False,
67
+ samesite: typing.Literal["lax", "strict", "none"] | None = "lax"):
68
+ self.cookies[key] = {
69
+ "value": value,
70
+ "max_age": max_age,
71
+ "expires": expires,
72
+ "path": path,
73
+ "domain": domain,
74
+ "secure": secure,
75
+ "httponly": httponly,
76
+ "samesite": samesite
77
+ }
78
+
79
+ @classmethod
80
+ def create(cls) -> ResponseContext.Response:
81
+ response = ResponseContext.Response()
82
+
83
+ cls.response_var.set(response)
84
+
85
+ return response
86
+
87
+ @classmethod
88
+ def get(cls) -> Optional[ResponseContext.Response]:
89
+ return cls.response_var.get()
90
+
91
+ @classmethod
92
+ def reset(cls) -> None:
93
+ cls.response_var.set(None)
94
+
95
+
31
96
  class RequestContext:
32
97
  """
33
98
  A request context is used to remember the current http request in the current thread
@@ -61,7 +126,7 @@ class RequestContext:
61
126
  finally:
62
127
  self.request_var.reset(token)
63
128
 
64
- class TokenMiddleware(BaseHTTPMiddleware):
129
+ class TokenContextMiddleware(BaseHTTPMiddleware):
65
130
  async def dispatch(self, request: Request, call_next):
66
131
  access_token = request.cookies.get("access_token") or request.headers.get("Authorization")
67
132
  #refresh_token = request.cookies.get("refresh_token")
@@ -74,26 +139,15 @@ class TokenMiddleware(BaseHTTPMiddleware):
74
139
  finally:
75
140
  TokenContext.clear()
76
141
 
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
142
  class FastAPIServer(Server):
87
143
  """
88
144
  A server utilizing fastapi framework.
89
145
  """
90
146
 
91
- fast_api = create_server()
92
-
93
147
  # class methods
94
148
 
95
149
  @classmethod
96
- def boot(cls, module: Type, host="0.0.0.0", port=8000, start = True) -> 'FastAPIServer':
150
+ def boot(cls, module: Type, host="0.0.0.0", port=8000, start_thread = True) -> Environment:
97
151
  """
98
152
  boot the DI infrastructure of the supplied module and optionally start a fastapi thread given the url
99
153
  Args:
@@ -102,22 +156,23 @@ class FastAPIServer(Server):
102
156
  port: the port
103
157
 
104
158
  Returns:
105
- thr created server
159
+ the created environment
106
160
  """
161
+
107
162
  cls.port = port
108
163
 
109
164
  environment = Environment(module)
110
165
 
111
166
  server = environment.get(FastAPIServer)
112
167
 
113
- if start:
114
- server.start_fastapi(host)
168
+ if start_thread:
169
+ server.start_server(host)
115
170
 
116
- return server
171
+ return environment
117
172
 
118
173
  # constructor
119
174
 
120
- def __init__(self, service_manager: ServiceManager, component_registry: ComponentRegistry):
175
+ def __init__(self, fast_api: FastAPI, service_manager: ServiceManager, component_registry: ComponentRegistry):
121
176
  super().__init__()
122
177
 
123
178
  self.environment : Optional[Environment] = None
@@ -125,10 +180,14 @@ class FastAPIServer(Server):
125
180
  self.component_registry = component_registry
126
181
 
127
182
  self.host = "localhost"
183
+ self.fast_api = fast_api
128
184
  self.server_thread = None
129
185
 
130
186
  self.router = APIRouter()
131
187
 
188
+ self.server : Optional[uvicorn.Server] = None
189
+ self.thread : Optional[threading.Thread] = None
190
+
132
191
  # cache
133
192
 
134
193
  self.deserializers: dict[str, list[Callable]] = {}
@@ -143,6 +202,8 @@ class FastAPIServer(Server):
143
202
  def set_environment(self, environment: Environment):
144
203
  self.environment = environment
145
204
 
205
+ # lifecycle
206
+
146
207
  @on_init()
147
208
  def on_init(self):
148
209
  self.service_manager.startup(self)
@@ -162,12 +223,74 @@ class FastAPIServer(Server):
162
223
 
163
224
  atexit.register(cleanup)
164
225
 
226
+ @on_destroy()
227
+ def on_destroy(self):
228
+ if self.server is not None:
229
+ self.server.should_exit = True
230
+ self.thread.join()
231
+
165
232
  # private
166
233
 
167
234
  def add_routes(self):
168
235
  """
169
236
  add everything that looks like an http endpoint
170
237
  """
238
+
239
+ def wrap_service_method(handler, return_type):
240
+ sig = inspect.signature(handler)
241
+
242
+ @functools.wraps(handler)
243
+ async def wrapper(*args, **kwargs):
244
+ try:
245
+ result = handler(*args, **kwargs)
246
+ if inspect.iscoroutine(result):
247
+ result = await result
248
+
249
+ except HTTPException as e:
250
+ raise
251
+ except Exception as e:
252
+ result = {"error": str(e)}
253
+
254
+ json_response = JSONResponse(get_serializer(return_type)(result))
255
+
256
+ local_response = ResponseContext.get()
257
+ if local_response is not None:
258
+ # delete
259
+
260
+ for key, value in local_response.delete_cookies.items():
261
+ json_response.delete_cookie(
262
+ key,
263
+ path=value.path,
264
+ domain=value.domain,
265
+ secure=value.secure,
266
+ httponly=value.httponly
267
+ )
268
+
269
+ # create
270
+
271
+ for key, value in local_response.cookies.items():
272
+ json_response.set_cookie(
273
+ key,
274
+ value=value.value,
275
+ max_age=value.max_age,
276
+ expires=value.expires,
277
+ path=value.path,
278
+ domain=value.domain,
279
+ secure=value.secure,
280
+ httponly=value.httponly
281
+ )
282
+
283
+ ResponseContext.reset()
284
+
285
+ return json_response
286
+
287
+ # Optionally attach response_model info for docs
288
+
289
+ wrapper.__signature__ = sig
290
+ wrapper.__annotations__ = {"return": return_type}
291
+
292
+ return wrapper
293
+
171
294
  for descriptor in self.service_manager.descriptors.values():
172
295
  if not descriptor.is_component() and descriptor.is_local():
173
296
  prefix = ""
@@ -183,25 +306,23 @@ class FastAPIServer(Server):
183
306
  if decorator is not None:
184
307
  self.router.add_api_route(
185
308
  path=prefix + decorator.args[0],
186
- endpoint=getattr(instance, method.get_name()),
309
+ endpoint=wrap_service_method(getattr(instance, method.get_name()), method.return_type),
187
310
  methods=[decorator.decorator.__name__],
188
311
  name=f"{descriptor.get_component_descriptor().name}.{descriptor.name}.{method.get_name()}",
189
312
  response_model=method.return_type,
190
313
  )
191
314
 
192
- def start_fastapi(self, host: str):
315
+ def start_server(self, host: str):
193
316
  """
194
317
  start the fastapi server in a thread
195
318
  """
196
319
  self.host = host
197
320
 
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()
321
+ config = uvicorn.Config(self.fast_api, host=host, port=self.port, access_log=False)
203
322
 
204
- return thread
323
+ self.server = uvicorn.Server(config)
324
+ self.thread = threading.Thread(target=self.server.run, daemon=True)
325
+ self.thread.start()
205
326
 
206
327
  def get_deserializers(self, service: Type, method):
207
328
  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.6
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=uph9QtIvJLxq9I0JRCq1sbyF2z_oLNHDmHZGMwd1YLk,13321
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.6.dist-info/METADATA,sha256=cutZq0v5pHKAvNXLUWg7MGuVs6e1KpMuAduufbMReLI,17946
11
+ aspyx_service-0.10.6.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
12
+ aspyx_service-0.10.6.dist-info/licenses/LICENSE,sha256=n4jfx_MNj7cBtPhhI7MCoB_K35cj1icP9yJ4Rh4vlvY,1070
13
+ aspyx_service-0.10.6.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,,