aspyx-service 0.10.3__py3-none-any.whl → 0.10.4__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 +28 -4
- aspyx_service/authorization.py +135 -0
- aspyx_service/channels.py +234 -45
- aspyx_service/healthcheck.py +1 -1
- aspyx_service/registries.py +5 -5
- aspyx_service/restchannel.py +15 -18
- aspyx_service/serialization.py +3 -3
- aspyx_service/server.py +139 -69
- aspyx_service/service.py +47 -12
- aspyx_service/session.py +97 -0
- {aspyx_service-0.10.3.dist-info → aspyx_service-0.10.4.dist-info}/METADATA +31 -4
- aspyx_service-0.10.4.dist-info/RECORD +14 -0
- aspyx_service-0.10.3.dist-info/RECORD +0 -12
- {aspyx_service-0.10.3.dist-info → aspyx_service-0.10.4.dist-info}/WHEEL +0 -0
- {aspyx_service-0.10.3.dist-info → aspyx_service-0.10.4.dist-info}/licenses/LICENSE +0 -0
aspyx_service/restchannel.py
CHANGED
|
@@ -3,18 +3,21 @@ rest channel implementation
|
|
|
3
3
|
"""
|
|
4
4
|
import inspect
|
|
5
5
|
import re
|
|
6
|
-
from dataclasses import is_dataclass
|
|
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
9
|
|
|
10
|
+
|
|
10
11
|
from pydantic import BaseModel
|
|
11
12
|
|
|
13
|
+
from .channels import HTTPXChannel
|
|
14
|
+
|
|
12
15
|
from aspyx.reflection import DynamicProxy, Decorators
|
|
13
16
|
from .service import channel, ServiceCommunicationException
|
|
14
17
|
|
|
15
18
|
T = TypeVar("T")
|
|
16
19
|
|
|
17
|
-
|
|
20
|
+
|
|
18
21
|
|
|
19
22
|
class BodyMarker:
|
|
20
23
|
pass
|
|
@@ -169,7 +172,7 @@ class RestChannel(HTTPXChannel):
|
|
|
169
172
|
for param_name, hint in hints.items():
|
|
170
173
|
if get_origin(hint) is Annotated:
|
|
171
174
|
metadata = get_args(hint)[1:]
|
|
172
|
-
|
|
175
|
+
|
|
173
176
|
if BodyMarker in metadata:
|
|
174
177
|
self.body_param_name = param_name
|
|
175
178
|
param_names.remove(param_name)
|
|
@@ -179,7 +182,7 @@ class RestChannel(HTTPXChannel):
|
|
|
179
182
|
|
|
180
183
|
# check if something is missing
|
|
181
184
|
|
|
182
|
-
if
|
|
185
|
+
if param_names:
|
|
183
186
|
# check body params
|
|
184
187
|
if self.type in ("post", "put", "patch"):
|
|
185
188
|
if self.body_param_name is None:
|
|
@@ -248,14 +251,11 @@ class RestChannel(HTTPXChannel):
|
|
|
248
251
|
|
|
249
252
|
try:
|
|
250
253
|
result = None
|
|
251
|
-
if call.type
|
|
252
|
-
result = await self.
|
|
253
|
-
|
|
254
|
-
result = await self.get_async_client().put(self.get_url() + url, params=query_params, timeout=self.timeout)
|
|
255
|
-
elif call.type == "delete":
|
|
256
|
-
result = await self.get_async_client().delete(self.get_url() + url, params=query_params, timeout=self.timeout)
|
|
254
|
+
if call.type in ["get", "put", "delete"]:
|
|
255
|
+
result = await self.request_async(call.type, self.get_url() + url, params=query_params, timeout=self.timeout)
|
|
256
|
+
|
|
257
257
|
elif call.type == "post":
|
|
258
|
-
result = await self.
|
|
258
|
+
result = await self.request_async("post", self.get_url() + url, params=query_params, json=body, timeout=self.timeout)
|
|
259
259
|
|
|
260
260
|
return self.get_deserializer(invocation.type, invocation.method)(result.json())
|
|
261
261
|
except ServiceCommunicationException:
|
|
@@ -283,14 +283,11 @@ class RestChannel(HTTPXChannel):
|
|
|
283
283
|
|
|
284
284
|
try:
|
|
285
285
|
result = None
|
|
286
|
-
if call.type
|
|
287
|
-
result = self.
|
|
288
|
-
|
|
289
|
-
result = self.get_client().put(self.get_url() + url, params=query_params, timeout=self.timeout)
|
|
290
|
-
elif call.type == "delete":
|
|
291
|
-
result = self.get_client().delete(self.get_url() + url, params=query_params, timeout=self.timeout)
|
|
286
|
+
if call.type in ["get", "put", "delete"]:
|
|
287
|
+
result = self.request("get", self.get_url() + url, params=query_params, timeout=self.timeout)
|
|
288
|
+
|
|
292
289
|
elif call.type == "post":
|
|
293
|
-
result = self.
|
|
290
|
+
result = self.request( "post", self.get_url() + url, params=query_params, json=body, timeout=self.timeout)
|
|
294
291
|
|
|
295
292
|
return self.get_deserializer(invocation.type, invocation.method)(result.json())
|
|
296
293
|
except ServiceCommunicationException:
|
aspyx_service/serialization.py
CHANGED
|
@@ -37,7 +37,7 @@ class TypeDeserializer:
|
|
|
37
37
|
return deser_union
|
|
38
38
|
|
|
39
39
|
if isinstance(typ, type) and issubclass(typ, BaseModel):
|
|
40
|
-
return typ.
|
|
40
|
+
return typ.model_validate
|
|
41
41
|
|
|
42
42
|
if is_dataclass(typ):
|
|
43
43
|
field_deserializers = {f.name: TypeDeserializer(f.type) for f in fields(typ)}
|
|
@@ -61,7 +61,7 @@ class TypeDeserializer:
|
|
|
61
61
|
|
|
62
62
|
# Fallback
|
|
63
63
|
return lambda v: v
|
|
64
|
-
|
|
64
|
+
|
|
65
65
|
class TypeSerializer:
|
|
66
66
|
def __init__(self, typ):
|
|
67
67
|
self.typ = typ
|
|
@@ -88,7 +88,7 @@ class TypeSerializer:
|
|
|
88
88
|
return ser_union
|
|
89
89
|
|
|
90
90
|
if isinstance(typ, type) and issubclass(typ, BaseModel):
|
|
91
|
-
return lambda v: v.
|
|
91
|
+
return lambda v: v.model_dump() if v is not None else None
|
|
92
92
|
|
|
93
93
|
if is_dataclass(typ):
|
|
94
94
|
field_serializers = {f.name: TypeSerializer(f.type) for f in fields(typ)}
|
aspyx_service/server.py
CHANGED
|
@@ -4,15 +4,18 @@ FastAPI server implementation for the aspyx service framework.
|
|
|
4
4
|
import atexit
|
|
5
5
|
import inspect
|
|
6
6
|
import threading
|
|
7
|
-
from typing import Type, Optional, Callable
|
|
7
|
+
from typing import Type, Optional, Callable, Any
|
|
8
8
|
|
|
9
9
|
from fastapi.responses import JSONResponse
|
|
10
10
|
import msgpack
|
|
11
11
|
import uvicorn
|
|
12
12
|
|
|
13
|
-
from fastapi import FastAPI, APIRouter, Request as HttpRequest, Response as HttpResponse
|
|
13
|
+
from fastapi import FastAPI, APIRouter, Request as HttpRequest, Response as HttpResponse, HTTPException
|
|
14
|
+
import contextvars
|
|
14
15
|
|
|
15
|
-
from
|
|
16
|
+
from starlette.middleware.base import BaseHTTPMiddleware
|
|
17
|
+
|
|
18
|
+
from aspyx.di import Environment, injectable, on_init, inject_environment
|
|
16
19
|
from aspyx.reflection import TypeDescriptor, Decorators
|
|
17
20
|
|
|
18
21
|
from .service import ComponentRegistry
|
|
@@ -21,28 +24,110 @@ from .healthcheck import HealthCheckManager
|
|
|
21
24
|
from .serialization import get_deserializer
|
|
22
25
|
|
|
23
26
|
from .service import Server, ServiceManager
|
|
24
|
-
from .channels import Request, Response
|
|
27
|
+
from .channels import Request, Response, TokenContext
|
|
25
28
|
|
|
26
29
|
from .restchannel import get, post, put, delete, rest
|
|
27
30
|
|
|
31
|
+
class RequestContext:
|
|
32
|
+
"""
|
|
33
|
+
A request context is used to remember the current http request in the current thread
|
|
34
|
+
"""
|
|
35
|
+
request_var = contextvars.ContextVar("request")
|
|
36
|
+
|
|
37
|
+
@classmethod
|
|
38
|
+
def get_request(cls) -> Request:
|
|
39
|
+
"""
|
|
40
|
+
Return the current http request
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
the current http request
|
|
44
|
+
"""
|
|
45
|
+
return cls.request_var.get()
|
|
46
|
+
|
|
47
|
+
# constructor
|
|
48
|
+
|
|
49
|
+
def __init__(self, app):
|
|
50
|
+
self.app = app
|
|
51
|
+
|
|
52
|
+
async def __call__(self, scope, receive, send):
|
|
53
|
+
if scope["type"] != "http":
|
|
54
|
+
await self.app(scope, receive, send)
|
|
55
|
+
return
|
|
28
56
|
|
|
57
|
+
request = HttpRequest(scope)
|
|
58
|
+
token = self.request_var.set(request)
|
|
59
|
+
try:
|
|
60
|
+
await self.app(scope, receive, send)
|
|
61
|
+
finally:
|
|
62
|
+
self.request_var.reset(token)
|
|
63
|
+
|
|
64
|
+
class TokenMiddleware(BaseHTTPMiddleware):
|
|
65
|
+
async def dispatch(self, request: Request, call_next):
|
|
66
|
+
access_token = request.cookies.get("access_token") or request.headers.get("Authorization")
|
|
67
|
+
#refresh_token = request.cookies.get("refresh_token")
|
|
68
|
+
|
|
69
|
+
if access_token:
|
|
70
|
+
TokenContext.set(access_token)#, refresh_token)
|
|
71
|
+
|
|
72
|
+
try:
|
|
73
|
+
return await call_next(request)
|
|
74
|
+
finally:
|
|
75
|
+
TokenContext.clear()
|
|
76
|
+
|
|
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()
|
|
29
86
|
class FastAPIServer(Server):
|
|
30
87
|
"""
|
|
31
88
|
A server utilizing fastapi framework.
|
|
32
89
|
"""
|
|
90
|
+
|
|
91
|
+
fast_api = create_server()
|
|
92
|
+
|
|
93
|
+
# class methods
|
|
94
|
+
|
|
95
|
+
@classmethod
|
|
96
|
+
def boot(cls, module: Type, host="0.0.0.0", port=8000, start = True) -> 'FastAPIServer':
|
|
97
|
+
"""
|
|
98
|
+
boot the DI infrastructure of the supplied module and optionally start a fastapi thread given the url
|
|
99
|
+
Args:
|
|
100
|
+
module: the module to initialize the environment
|
|
101
|
+
host: listen address
|
|
102
|
+
port: the port
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
thr created server
|
|
106
|
+
"""
|
|
107
|
+
cls.port = port
|
|
108
|
+
|
|
109
|
+
environment = Environment(module)
|
|
110
|
+
|
|
111
|
+
server = environment.get(FastAPIServer)
|
|
112
|
+
|
|
113
|
+
if start:
|
|
114
|
+
server.start_fastapi(host)
|
|
115
|
+
|
|
116
|
+
return server
|
|
117
|
+
|
|
33
118
|
# constructor
|
|
34
119
|
|
|
35
|
-
def __init__(self,
|
|
120
|
+
def __init__(self, service_manager: ServiceManager, component_registry: ComponentRegistry):
|
|
36
121
|
super().__init__()
|
|
37
122
|
|
|
38
|
-
self.
|
|
39
|
-
|
|
123
|
+
self.environment : Optional[Environment] = None
|
|
124
|
+
self.service_manager = service_manager
|
|
125
|
+
self.component_registry = component_registry
|
|
126
|
+
|
|
127
|
+
self.host = "localhost"
|
|
40
128
|
self.server_thread = None
|
|
41
|
-
self.service_manager : Optional[ServiceManager] = None
|
|
42
|
-
self.component_registry: Optional[ComponentRegistry] = None
|
|
43
129
|
|
|
44
130
|
self.router = APIRouter()
|
|
45
|
-
self.fast_api = FastAPI()
|
|
46
131
|
|
|
47
132
|
# cache
|
|
48
133
|
|
|
@@ -52,15 +137,37 @@ class FastAPIServer(Server):
|
|
|
52
137
|
|
|
53
138
|
self.router.post("/invoke")(self.invoke)
|
|
54
139
|
|
|
140
|
+
# inject
|
|
141
|
+
|
|
142
|
+
@inject_environment()
|
|
143
|
+
def set_environment(self, environment: Environment):
|
|
144
|
+
self.environment = environment
|
|
145
|
+
|
|
146
|
+
@on_init()
|
|
147
|
+
def on_init(self):
|
|
148
|
+
self.service_manager.startup(self)
|
|
149
|
+
|
|
150
|
+
# add routes
|
|
151
|
+
|
|
152
|
+
self.add_routes()
|
|
153
|
+
self.fast_api.include_router(self.router)
|
|
154
|
+
|
|
155
|
+
#for route in self.fast_api.routes:
|
|
156
|
+
# print(f"{route.name}: {route.path} [{route.methods}]")
|
|
157
|
+
|
|
158
|
+
# add cleanup hook
|
|
159
|
+
|
|
160
|
+
def cleanup():
|
|
161
|
+
self.service_manager.shutdown()
|
|
162
|
+
|
|
163
|
+
atexit.register(cleanup)
|
|
164
|
+
|
|
55
165
|
# private
|
|
56
166
|
|
|
57
167
|
def add_routes(self):
|
|
58
168
|
"""
|
|
59
|
-
add everything that looks like
|
|
169
|
+
add everything that looks like an http endpoint
|
|
60
170
|
"""
|
|
61
|
-
|
|
62
|
-
# go
|
|
63
|
-
|
|
64
171
|
for descriptor in self.service_manager.descriptors.values():
|
|
65
172
|
if not descriptor.is_component() and descriptor.is_local():
|
|
66
173
|
prefix = ""
|
|
@@ -82,12 +189,13 @@ class FastAPIServer(Server):
|
|
|
82
189
|
response_model=method.return_type,
|
|
83
190
|
)
|
|
84
191
|
|
|
85
|
-
def
|
|
192
|
+
def start_fastapi(self, host: str):
|
|
86
193
|
"""
|
|
87
194
|
start the fastapi server in a thread
|
|
88
195
|
"""
|
|
196
|
+
self.host = host
|
|
89
197
|
|
|
90
|
-
config = uvicorn.Config(self.fast_api, host=self.host, port=self.port, access_log=False)
|
|
198
|
+
config = uvicorn.Config(self.fast_api, host=self.host, port=self.port, access_log=False)
|
|
91
199
|
server = uvicorn.Server(config)
|
|
92
200
|
|
|
93
201
|
thread = threading.Thread(target=server.run, daemon=True)
|
|
@@ -95,7 +203,6 @@ class FastAPIServer(Server):
|
|
|
95
203
|
|
|
96
204
|
return thread
|
|
97
205
|
|
|
98
|
-
|
|
99
206
|
def get_deserializers(self, service: Type, method):
|
|
100
207
|
deserializers = self.deserializers.get(method, None)
|
|
101
208
|
if deserializers is None:
|
|
@@ -106,8 +213,8 @@ class FastAPIServer(Server):
|
|
|
106
213
|
|
|
107
214
|
return deserializers
|
|
108
215
|
|
|
109
|
-
def deserialize_args(self,
|
|
110
|
-
args = list(request.args)
|
|
216
|
+
def deserialize_args(self, args: list[Any], type: Type, method: Callable) -> list:
|
|
217
|
+
#args = list(request.args)
|
|
111
218
|
|
|
112
219
|
deserializers = self.get_deserializers(type, method)
|
|
113
220
|
|
|
@@ -133,22 +240,22 @@ class FastAPIServer(Server):
|
|
|
133
240
|
media_type="text/plain"
|
|
134
241
|
)
|
|
135
242
|
|
|
136
|
-
request =
|
|
243
|
+
request = data
|
|
137
244
|
|
|
138
245
|
if content == "json":
|
|
139
|
-
return await self.dispatch(request)
|
|
246
|
+
return await self.dispatch(http_request, request)
|
|
140
247
|
else:
|
|
141
248
|
return HttpResponse(
|
|
142
|
-
content=msgpack.packb(await self.dispatch(request), use_bin_type=True),
|
|
249
|
+
content=msgpack.packb(await self.dispatch(http_request, request), use_bin_type=True),
|
|
143
250
|
media_type="application/msgpack"
|
|
144
251
|
)
|
|
145
252
|
|
|
146
|
-
async def dispatch(self, request:
|
|
147
|
-
ServiceManager.logger.debug("dispatch request %s", request
|
|
253
|
+
async def dispatch(self, http_request: HttpRequest, request: dict) :
|
|
254
|
+
ServiceManager.logger.debug("dispatch request %s", request["method"])
|
|
148
255
|
|
|
149
256
|
# <comp>:<service>:<method>
|
|
150
257
|
|
|
151
|
-
parts = request
|
|
258
|
+
parts = request["method"].split(":")
|
|
152
259
|
|
|
153
260
|
#component = parts[0]
|
|
154
261
|
service_name = parts[1]
|
|
@@ -159,16 +266,20 @@ class FastAPIServer(Server):
|
|
|
159
266
|
|
|
160
267
|
method = getattr(service, method_name)
|
|
161
268
|
|
|
162
|
-
args = self.deserialize_args(request, service_descriptor.type, method)
|
|
269
|
+
args = self.deserialize_args(request["args"], service_descriptor.type, method)
|
|
163
270
|
try:
|
|
164
271
|
if inspect.iscoroutinefunction(method):
|
|
165
272
|
result = await method(*args)
|
|
166
273
|
else:
|
|
167
274
|
result = method(*args)
|
|
168
275
|
|
|
169
|
-
return Response(result=result, exception=None).
|
|
276
|
+
return Response(result=result, exception=None).model_dump()
|
|
277
|
+
|
|
278
|
+
except HTTPException as e:
|
|
279
|
+
raise
|
|
280
|
+
|
|
170
281
|
except Exception as e:
|
|
171
|
-
return Response(result=None, exception=str(e)).
|
|
282
|
+
return Response(result=None, exception=str(e)).model_dump()
|
|
172
283
|
|
|
173
284
|
# override
|
|
174
285
|
|
|
@@ -185,44 +296,3 @@ class FastAPIServer(Server):
|
|
|
185
296
|
)
|
|
186
297
|
|
|
187
298
|
self.router.get(url)(get_health_response)
|
|
188
|
-
|
|
189
|
-
def boot(self, module_type: Type) -> Environment:
|
|
190
|
-
"""
|
|
191
|
-
startup the service manager, DI framework and the fastapi server based on the supplied module
|
|
192
|
-
|
|
193
|
-
Args:
|
|
194
|
-
module_type: the module
|
|
195
|
-
|
|
196
|
-
Returns:
|
|
197
|
-
|
|
198
|
-
"""
|
|
199
|
-
# setup environment
|
|
200
|
-
|
|
201
|
-
self.environment = Environment(module_type)
|
|
202
|
-
self.service_manager = self.environment.get(ServiceManager)
|
|
203
|
-
self.component_registry = self.environment.get(ComponentRegistry)
|
|
204
|
-
|
|
205
|
-
self.service_manager.startup(self)
|
|
206
|
-
|
|
207
|
-
# add routes
|
|
208
|
-
|
|
209
|
-
self.add_routes()
|
|
210
|
-
self.fast_api.include_router(self.router)
|
|
211
|
-
|
|
212
|
-
#for route in self.fast_api.routes:
|
|
213
|
-
# print(f"{route.name}: {route.path} [{route.methods}]")
|
|
214
|
-
|
|
215
|
-
# start server thread
|
|
216
|
-
|
|
217
|
-
self.start_fastapi_thread()
|
|
218
|
-
|
|
219
|
-
# shutdown
|
|
220
|
-
|
|
221
|
-
def cleanup():
|
|
222
|
-
self.service_manager.shutdown()
|
|
223
|
-
|
|
224
|
-
atexit.register(cleanup)
|
|
225
|
-
|
|
226
|
-
# done
|
|
227
|
-
|
|
228
|
-
return self.environment
|
aspyx_service/service.py
CHANGED
|
@@ -15,9 +15,10 @@ from typing import Type, TypeVar, Generic, Callable, Optional, cast
|
|
|
15
15
|
|
|
16
16
|
from aspyx.di import injectable, Environment, Providers, ClassInstanceProvider, inject_environment, order, \
|
|
17
17
|
Lifecycle, LifecycleCallable, InstanceProvider
|
|
18
|
+
from aspyx.di.aop.aop import ClassAspectTarget
|
|
18
19
|
from aspyx.reflection import Decorators, DynamicProxy, DecoratorDescriptor, TypeDescriptor
|
|
19
20
|
from aspyx.util import StringBuilder
|
|
20
|
-
from .healthcheck import HealthCheckManager
|
|
21
|
+
from .healthcheck import HealthCheckManager, HealthStatus
|
|
21
22
|
|
|
22
23
|
T = TypeVar("T")
|
|
23
24
|
|
|
@@ -55,10 +56,6 @@ class Server(ABC):
|
|
|
55
56
|
def get(self, type: Type[T]) -> T:
|
|
56
57
|
return self.environment.get(type)
|
|
57
58
|
|
|
58
|
-
@abstractmethod
|
|
59
|
-
def boot(self, module_type: Type):
|
|
60
|
-
pass
|
|
61
|
-
|
|
62
59
|
@abstractmethod
|
|
63
60
|
def route(self, url : str, callable: Callable):
|
|
64
61
|
pass
|
|
@@ -121,7 +118,7 @@ class Component(Service):
|
|
|
121
118
|
@abstractmethod
|
|
122
119
|
def get_addresses(self, port: int) -> list[ChannelAddress]:
|
|
123
120
|
"""
|
|
124
|
-
returns a list of channel addresses that expose this
|
|
121
|
+
returns a list of channel addresses that expose this component's services.
|
|
125
122
|
|
|
126
123
|
Args:
|
|
127
124
|
port: the port of a server hosting this component
|
|
@@ -164,6 +161,9 @@ class AbstractComponent(Component, ABC):
|
|
|
164
161
|
def get_status(self) -> ComponentStatus:
|
|
165
162
|
return self.status
|
|
166
163
|
|
|
164
|
+
async def get_health(self) -> HealthCheckManager.Health:
|
|
165
|
+
return HealthCheckManager.Health(HealthStatus.OK)
|
|
166
|
+
|
|
167
167
|
def to_snake_case(name: str) -> str:
|
|
168
168
|
return re.sub(r'(?<!^)(?=[A-Z])', '-', name).lower()
|
|
169
169
|
|
|
@@ -401,6 +401,22 @@ class RemoteServiceException(ServiceException):
|
|
|
401
401
|
base class for service exceptions occurring on the server side
|
|
402
402
|
"""
|
|
403
403
|
|
|
404
|
+
|
|
405
|
+
class AuthorizationException(ServiceException):
|
|
406
|
+
pass
|
|
407
|
+
|
|
408
|
+
class TokenException(AuthorizationException):
|
|
409
|
+
pass
|
|
410
|
+
|
|
411
|
+
class InvalidTokenException(TokenException):
|
|
412
|
+
pass
|
|
413
|
+
|
|
414
|
+
class MissingTokenException(TokenException):
|
|
415
|
+
pass
|
|
416
|
+
|
|
417
|
+
class TokenExpiredException(TokenException):
|
|
418
|
+
pass
|
|
419
|
+
|
|
404
420
|
class Channel(DynamicProxy.InvocationHandler, ABC):
|
|
405
421
|
"""
|
|
406
422
|
A channel is a dynamic proxy invocation handler and transparently takes care of remoting.
|
|
@@ -432,7 +448,7 @@ class Channel(DynamicProxy.InvocationHandler, ABC):
|
|
|
432
448
|
a url selector always retrieving the first URL given a list of possible URLS
|
|
433
449
|
"""
|
|
434
450
|
def get(self, urls: list[str]) -> str:
|
|
435
|
-
if
|
|
451
|
+
if not urls:
|
|
436
452
|
raise ServiceCommunicationException("no known url")
|
|
437
453
|
|
|
438
454
|
return urls[0]
|
|
@@ -445,7 +461,7 @@ class Channel(DynamicProxy.InvocationHandler, ABC):
|
|
|
445
461
|
self.index = 0
|
|
446
462
|
|
|
447
463
|
def get(self, urls: list[str]) -> str:
|
|
448
|
-
if
|
|
464
|
+
if urls:
|
|
449
465
|
try:
|
|
450
466
|
return urls[self.index]
|
|
451
467
|
finally:
|
|
@@ -462,6 +478,8 @@ class Channel(DynamicProxy.InvocationHandler, ABC):
|
|
|
462
478
|
self.address: Optional[ChannelInstances] = None
|
|
463
479
|
self.url_selector : Channel.URLSelector = Channel.FirstURLSelector()
|
|
464
480
|
|
|
481
|
+
self.select_round_robin()
|
|
482
|
+
|
|
465
483
|
# public
|
|
466
484
|
|
|
467
485
|
def customize(self):
|
|
@@ -692,11 +710,17 @@ class ServiceManager:
|
|
|
692
710
|
|
|
693
711
|
# fetch health
|
|
694
712
|
|
|
695
|
-
|
|
713
|
+
health_name = None
|
|
714
|
+
health_descriptor = Decorators.get_decorator(descriptor.implementation, health)
|
|
715
|
+
|
|
716
|
+
if health_descriptor is not None:
|
|
717
|
+
health_name = health_descriptor.args[0]
|
|
718
|
+
|
|
719
|
+
descriptor.health = health_name
|
|
696
720
|
|
|
697
721
|
self.component_registry.register(descriptor.get_component_descriptor(), [ChannelAddress("local", "")])
|
|
698
722
|
|
|
699
|
-
health_name = next((decorator.args[0] for decorator in Decorators.get(descriptor.type) if decorator.decorator is health), None)
|
|
723
|
+
#health_name = next((decorator.args[0] for decorator in Decorators.get(descriptor.type) if decorator.decorator is health), None)
|
|
700
724
|
|
|
701
725
|
# startup
|
|
702
726
|
|
|
@@ -704,7 +728,8 @@ class ServiceManager:
|
|
|
704
728
|
|
|
705
729
|
# add health route
|
|
706
730
|
|
|
707
|
-
|
|
731
|
+
if health_name is not None:
|
|
732
|
+
server.route_health(health_name, instance.get_health)
|
|
708
733
|
|
|
709
734
|
# register addresses
|
|
710
735
|
|
|
@@ -733,7 +758,7 @@ class ServiceManager:
|
|
|
733
758
|
addresses = self.component_registry.get_addresses(component_descriptor) # component, channel + urls
|
|
734
759
|
address = next((address for address in addresses if address.channel == preferred_channel), None)
|
|
735
760
|
if address is None:
|
|
736
|
-
if
|
|
761
|
+
if addresses:
|
|
737
762
|
# return the first match
|
|
738
763
|
address = addresses[0]
|
|
739
764
|
else:
|
|
@@ -908,3 +933,13 @@ class ServiceLifecycleCallable(LifecycleCallable):
|
|
|
908
933
|
|
|
909
934
|
def args(self, decorator: DecoratorDescriptor, method: TypeDescriptor.MethodDescriptor, environment: Environment):
|
|
910
935
|
return [self.manager.get_service(method.param_types[0], preferred_channel=decorator.args[0])]
|
|
936
|
+
|
|
937
|
+
def component_services(component_type: Type) -> ClassAspectTarget:
|
|
938
|
+
target = ClassAspectTarget()
|
|
939
|
+
|
|
940
|
+
descriptor = TypeDescriptor.for_type(component_type)
|
|
941
|
+
|
|
942
|
+
for service_type in descriptor.get_decorator(component).args[2]:
|
|
943
|
+
target.of_type(service_type)
|
|
944
|
+
|
|
945
|
+
return target
|
aspyx_service/session.py
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""
|
|
2
|
+
session related module
|
|
3
|
+
"""
|
|
4
|
+
import contextvars
|
|
5
|
+
from typing import Type, Optional, Callable, Any, TypeVar
|
|
6
|
+
from datetime import datetime, timezone
|
|
7
|
+
from cachetools import TTLCache
|
|
8
|
+
|
|
9
|
+
from aspyx.di import injectable
|
|
10
|
+
from aspyx.threading import ThreadLocal
|
|
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
|
+
@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")
|
|
29
|
+
|
|
30
|
+
@classmethod
|
|
31
|
+
def current(cls, type: Type[T]) -> T:
|
|
32
|
+
"""
|
|
33
|
+
return the current session associated with the thread
|
|
34
|
+
Args:
|
|
35
|
+
type: the session type
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
the current session
|
|
39
|
+
"""
|
|
40
|
+
return cls.current_session.get()
|
|
41
|
+
|
|
42
|
+
@classmethod
|
|
43
|
+
def set_session(cls, session: Session) -> None:
|
|
44
|
+
"""
|
|
45
|
+
set the current session in the thread context
|
|
46
|
+
Args:
|
|
47
|
+
session: the session
|
|
48
|
+
"""
|
|
49
|
+
cls.current_session.set(session)
|
|
50
|
+
|
|
51
|
+
@classmethod
|
|
52
|
+
def delete_session(cls) -> None:
|
|
53
|
+
"""
|
|
54
|
+
delete the current session
|
|
55
|
+
"""
|
|
56
|
+
cls.current_session.set(None)#clear()
|
|
57
|
+
|
|
58
|
+
# constructor
|
|
59
|
+
|
|
60
|
+
def __init__(self):
|
|
61
|
+
self.sessions = TTLCache(maxsize=1000, ttl=3600)
|
|
62
|
+
self.session_creator : Optional[Callable[[Any], Session]] = None
|
|
63
|
+
|
|
64
|
+
# public
|
|
65
|
+
|
|
66
|
+
def set_session_factory(self, callable: Callable[..., Session]) -> None:
|
|
67
|
+
"""
|
|
68
|
+
set a factory function that will be used to create a concrete session
|
|
69
|
+
Args:
|
|
70
|
+
callable: the function
|
|
71
|
+
"""
|
|
72
|
+
self.session_creator = callable
|
|
73
|
+
|
|
74
|
+
def create_session(self, *args, **kwargs) -> Session:
|
|
75
|
+
"""
|
|
76
|
+
create a session given the argument s(usually a token, etc.)
|
|
77
|
+
Args:
|
|
78
|
+
args: rest args
|
|
79
|
+
kwargs: keyword args
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
the new session
|
|
83
|
+
"""
|
|
84
|
+
return self.session_creator(*args, **kwargs)
|
|
85
|
+
|
|
86
|
+
def store_session(self, token: str, session: Session, expiry: datetime):
|
|
87
|
+
now = datetime.now(timezone.utc)
|
|
88
|
+
ttl_seconds = max(int((expiry - now).total_seconds()), 0)
|
|
89
|
+
self.sessions[token] = (session, ttl_seconds)
|
|
90
|
+
|
|
91
|
+
def get_session(self, token: str) -> Optional[Session]:
|
|
92
|
+
value = self.sessions.get(token)
|
|
93
|
+
if value is None:
|
|
94
|
+
return None
|
|
95
|
+
|
|
96
|
+
session, ttl = value
|
|
97
|
+
return session
|