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 +8 -7
- aspyx_service/authorization.py +4 -13
- aspyx_service/channels.py +9 -7
- aspyx_service/restchannel.py +2 -6
- aspyx_service/server.py +94 -32
- aspyx_service/session.py +69 -30
- {aspyx_service-0.10.4.dist-info → aspyx_service-0.10.5.dist-info}/METADATA +28 -11
- aspyx_service-0.10.5.dist-info/RECORD +13 -0
- aspyx_service/serialization.py +0 -137
- aspyx_service-0.10.4.dist-info/RECORD +0 -14
- {aspyx_service-0.10.4.dist-info → aspyx_service-0.10.5.dist-info}/WHEEL +0 -0
- {aspyx_service-0.10.4.dist-info → aspyx_service-0.10.5.dist-info}/licenses/LICENSE +0 -0
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
|
|
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
|
]
|
aspyx_service/authorization.py
CHANGED
|
@@ -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
|
|
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
|
|
105
|
+
def authorize(self, invocation: Invocation):
|
|
115
106
|
for check in self.get_checks(invocation.func):
|
|
116
|
-
check.
|
|
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
|
-
|
|
211
|
+
|
|
212
|
+
if 'missing' in www_auth:
|
|
212
213
|
raise MissingTokenException() from e
|
|
213
|
-
|
|
214
|
-
|
|
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
|
-
|
|
455
|
+
|
|
456
|
+
if 'missing' in www_auth:
|
|
455
457
|
raise MissingTokenException() from e
|
|
456
|
-
|
|
457
|
-
|
|
458
|
+
|
|
459
|
+
raise InvalidTokenException() from e
|
|
458
460
|
|
|
459
461
|
raise RemoteServiceException(str(e)) from e
|
|
460
462
|
|
aspyx_service/restchannel.py
CHANGED
|
@@ -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
|
|
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,
|
|
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
|
-
|
|
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
|
|
114
|
-
server.
|
|
131
|
+
if start_thread:
|
|
132
|
+
server.start_server(host)
|
|
115
133
|
|
|
116
|
-
return
|
|
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
|
|
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=
|
|
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
|
-
|
|
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
|
-
|
|
23
|
-
class
|
|
24
|
-
|
|
25
|
-
|
|
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
|
|
29
|
+
def get(cls, type: Type[T]) -> T:
|
|
32
30
|
"""
|
|
33
|
-
return the current session associated with the
|
|
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
|
|
41
|
+
def set(cls, session: Session) -> None:
|
|
44
42
|
"""
|
|
45
|
-
set the current session in the
|
|
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
|
|
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.
|
|
62
|
-
self.
|
|
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
|
|
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
|
-
|
|
113
|
+
factory: the function
|
|
71
114
|
"""
|
|
72
|
-
self.
|
|
115
|
+
self.session_factory = factory
|
|
73
116
|
|
|
74
117
|
def create_session(self, *args, **kwargs) -> Session:
|
|
75
118
|
"""
|
|
76
|
-
create a session given the
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
97
|
-
return
|
|
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
|
+
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.
|
|
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))
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
492
|
-
|
|
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
|
-
|
|
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,,
|
aspyx_service/serialization.py
DELETED
|
@@ -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,,
|
|
File without changes
|
|
File without changes
|