aspyx-service 0.10.3__py3-none-any.whl → 0.10.5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of aspyx-service might be problematic. Click here for more details.
- aspyx_service/__init__.py +30 -5
- aspyx_service/authorization.py +126 -0
- aspyx_service/channels.py +236 -45
- aspyx_service/healthcheck.py +1 -1
- aspyx_service/registries.py +5 -5
- aspyx_service/restchannel.py +13 -20
- aspyx_service/server.py +209 -77
- aspyx_service/service.py +47 -12
- aspyx_service/session.py +136 -0
- {aspyx_service-0.10.3.dist-info → aspyx_service-0.10.5.dist-info}/METADATA +56 -12
- aspyx_service-0.10.5.dist-info/RECORD +13 -0
- aspyx_service/serialization.py +0 -137
- aspyx_service-0.10.3.dist-info/RECORD +0 -12
- {aspyx_service-0.10.3.dist-info → aspyx_service-0.10.5.dist-info}/WHEEL +0 -0
- {aspyx_service-0.10.3.dist-info → aspyx_service-0.10.5.dist-info}/licenses/LICENSE +0 -0
aspyx_service/restchannel.py
CHANGED
|
@@ -3,19 +3,18 @@ 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
|
-
|
|
10
9
|
from pydantic import BaseModel
|
|
11
10
|
|
|
12
11
|
from aspyx.reflection import DynamicProxy, Decorators
|
|
12
|
+
|
|
13
|
+
from .channels import HTTPXChannel
|
|
13
14
|
from .service import channel, ServiceCommunicationException
|
|
14
15
|
|
|
15
16
|
T = TypeVar("T")
|
|
16
17
|
|
|
17
|
-
from .channels import HTTPXChannel
|
|
18
|
-
|
|
19
18
|
class BodyMarker:
|
|
20
19
|
pass
|
|
21
20
|
|
|
@@ -169,7 +168,7 @@ class RestChannel(HTTPXChannel):
|
|
|
169
168
|
for param_name, hint in hints.items():
|
|
170
169
|
if get_origin(hint) is Annotated:
|
|
171
170
|
metadata = get_args(hint)[1:]
|
|
172
|
-
|
|
171
|
+
|
|
173
172
|
if BodyMarker in metadata:
|
|
174
173
|
self.body_param_name = param_name
|
|
175
174
|
param_names.remove(param_name)
|
|
@@ -179,7 +178,7 @@ class RestChannel(HTTPXChannel):
|
|
|
179
178
|
|
|
180
179
|
# check if something is missing
|
|
181
180
|
|
|
182
|
-
if
|
|
181
|
+
if param_names:
|
|
183
182
|
# check body params
|
|
184
183
|
if self.type in ("post", "put", "patch"):
|
|
185
184
|
if self.body_param_name is None:
|
|
@@ -248,14 +247,11 @@ class RestChannel(HTTPXChannel):
|
|
|
248
247
|
|
|
249
248
|
try:
|
|
250
249
|
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)
|
|
250
|
+
if call.type in ["get", "put", "delete"]:
|
|
251
|
+
result = await self.request_async(call.type, self.get_url() + url, params=query_params, timeout=self.timeout)
|
|
252
|
+
|
|
257
253
|
elif call.type == "post":
|
|
258
|
-
result = await self.
|
|
254
|
+
result = await self.request_async("post", self.get_url() + url, params=query_params, json=body, timeout=self.timeout)
|
|
259
255
|
|
|
260
256
|
return self.get_deserializer(invocation.type, invocation.method)(result.json())
|
|
261
257
|
except ServiceCommunicationException:
|
|
@@ -283,14 +279,11 @@ class RestChannel(HTTPXChannel):
|
|
|
283
279
|
|
|
284
280
|
try:
|
|
285
281
|
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)
|
|
282
|
+
if call.type in ["get", "put", "delete"]:
|
|
283
|
+
result = self.request("get", self.get_url() + url, params=query_params, timeout=self.timeout)
|
|
284
|
+
|
|
292
285
|
elif call.type == "post":
|
|
293
|
-
result = self.
|
|
286
|
+
result = self.request( "post", self.get_url() + url, params=query_params, json=body, timeout=self.timeout)
|
|
294
287
|
|
|
295
288
|
return self.get_deserializer(invocation.type, invocation.method)(result.json())
|
|
296
289
|
except ServiceCommunicationException:
|
aspyx_service/server.py
CHANGED
|
@@ -1,48 +1,155 @@
|
|
|
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
|
-
from typing import Type, Optional, Callable
|
|
8
|
-
|
|
9
|
-
from fastapi.responses import JSONResponse
|
|
9
|
+
from typing import Type, Optional, Callable, Any
|
|
10
|
+
import contextvars
|
|
10
11
|
import msgpack
|
|
11
12
|
import uvicorn
|
|
12
13
|
|
|
13
|
-
from fastapi import FastAPI, APIRouter, Request as HttpRequest, Response as HttpResponse
|
|
14
|
+
from fastapi import FastAPI, APIRouter, Request as HttpRequest, Response as HttpResponse, HTTPException
|
|
14
15
|
|
|
15
|
-
|
|
16
|
+
|
|
17
|
+
from fastapi.responses import JSONResponse
|
|
18
|
+
from starlette.middleware.base import BaseHTTPMiddleware
|
|
19
|
+
|
|
20
|
+
from aspyx.di import Environment, injectable, on_init, inject_environment, on_destroy
|
|
16
21
|
from aspyx.reflection import TypeDescriptor, Decorators
|
|
22
|
+
from aspyx.util import get_deserializer, get_serializer
|
|
17
23
|
|
|
18
24
|
from .service import ComponentRegistry
|
|
19
25
|
from .healthcheck import HealthCheckManager
|
|
20
26
|
|
|
21
|
-
from .serialization import get_deserializer
|
|
22
|
-
|
|
23
27
|
from .service import Server, ServiceManager
|
|
24
|
-
from .channels import Request, Response
|
|
28
|
+
from .channels import Request, Response, TokenContext
|
|
25
29
|
|
|
26
30
|
from .restchannel import get, post, put, delete, rest
|
|
27
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
|
+
|
|
59
|
+
class RequestContext:
|
|
60
|
+
"""
|
|
61
|
+
A request context is used to remember the current http request in the current thread
|
|
62
|
+
"""
|
|
63
|
+
request_var = contextvars.ContextVar("request")
|
|
64
|
+
|
|
65
|
+
@classmethod
|
|
66
|
+
def get_request(cls) -> Request:
|
|
67
|
+
"""
|
|
68
|
+
Return the current http request
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
the current http request
|
|
72
|
+
"""
|
|
73
|
+
return cls.request_var.get()
|
|
74
|
+
|
|
75
|
+
# constructor
|
|
76
|
+
|
|
77
|
+
def __init__(self, app):
|
|
78
|
+
self.app = app
|
|
79
|
+
|
|
80
|
+
async def __call__(self, scope, receive, send):
|
|
81
|
+
if scope["type"] != "http":
|
|
82
|
+
await self.app(scope, receive, send)
|
|
83
|
+
return
|
|
84
|
+
|
|
85
|
+
request = HttpRequest(scope)
|
|
86
|
+
token = self.request_var.set(request)
|
|
87
|
+
try:
|
|
88
|
+
await self.app(scope, receive, send)
|
|
89
|
+
finally:
|
|
90
|
+
self.request_var.reset(token)
|
|
91
|
+
|
|
92
|
+
class TokenContextMiddleware(BaseHTTPMiddleware):
|
|
93
|
+
async def dispatch(self, request: Request, call_next):
|
|
94
|
+
access_token = request.cookies.get("access_token") or request.headers.get("Authorization")
|
|
95
|
+
#refresh_token = request.cookies.get("refresh_token")
|
|
96
|
+
|
|
97
|
+
if access_token:
|
|
98
|
+
TokenContext.set(access_token)#, refresh_token)
|
|
99
|
+
|
|
100
|
+
try:
|
|
101
|
+
return await call_next(request)
|
|
102
|
+
finally:
|
|
103
|
+
TokenContext.clear()
|
|
28
104
|
|
|
29
105
|
class FastAPIServer(Server):
|
|
30
106
|
"""
|
|
31
107
|
A server utilizing fastapi framework.
|
|
32
108
|
"""
|
|
109
|
+
|
|
110
|
+
# class methods
|
|
111
|
+
|
|
112
|
+
@classmethod
|
|
113
|
+
def boot(cls, module: Type, host="0.0.0.0", port=8000, start_thread = True) -> Environment:
|
|
114
|
+
"""
|
|
115
|
+
boot the DI infrastructure of the supplied module and optionally start a fastapi thread given the url
|
|
116
|
+
Args:
|
|
117
|
+
module: the module to initialize the environment
|
|
118
|
+
host: listen address
|
|
119
|
+
port: the port
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
the created environment
|
|
123
|
+
"""
|
|
124
|
+
|
|
125
|
+
cls.port = port
|
|
126
|
+
|
|
127
|
+
environment = Environment(module)
|
|
128
|
+
|
|
129
|
+
server = environment.get(FastAPIServer)
|
|
130
|
+
|
|
131
|
+
if start_thread:
|
|
132
|
+
server.start_server(host)
|
|
133
|
+
|
|
134
|
+
return environment
|
|
135
|
+
|
|
33
136
|
# constructor
|
|
34
137
|
|
|
35
|
-
def __init__(self,
|
|
138
|
+
def __init__(self, fast_api: FastAPI, service_manager: ServiceManager, component_registry: ComponentRegistry):
|
|
36
139
|
super().__init__()
|
|
37
140
|
|
|
38
|
-
self.
|
|
39
|
-
|
|
141
|
+
self.environment : Optional[Environment] = None
|
|
142
|
+
self.service_manager = service_manager
|
|
143
|
+
self.component_registry = component_registry
|
|
144
|
+
|
|
145
|
+
self.host = "localhost"
|
|
146
|
+
self.fast_api = fast_api
|
|
40
147
|
self.server_thread = None
|
|
41
|
-
self.service_manager : Optional[ServiceManager] = None
|
|
42
|
-
self.component_registry: Optional[ComponentRegistry] = None
|
|
43
148
|
|
|
44
149
|
self.router = APIRouter()
|
|
45
|
-
|
|
150
|
+
|
|
151
|
+
self.server : Optional[uvicorn.Server] = None
|
|
152
|
+
self.thread : Optional[threading.Thread] = None
|
|
46
153
|
|
|
47
154
|
# cache
|
|
48
155
|
|
|
@@ -52,14 +159,78 @@ class FastAPIServer(Server):
|
|
|
52
159
|
|
|
53
160
|
self.router.post("/invoke")(self.invoke)
|
|
54
161
|
|
|
162
|
+
# inject
|
|
163
|
+
|
|
164
|
+
@inject_environment()
|
|
165
|
+
def set_environment(self, environment: Environment):
|
|
166
|
+
self.environment = environment
|
|
167
|
+
|
|
168
|
+
# lifecycle
|
|
169
|
+
|
|
170
|
+
@on_init()
|
|
171
|
+
def on_init(self):
|
|
172
|
+
self.service_manager.startup(self)
|
|
173
|
+
|
|
174
|
+
# add routes
|
|
175
|
+
|
|
176
|
+
self.add_routes()
|
|
177
|
+
self.fast_api.include_router(self.router)
|
|
178
|
+
|
|
179
|
+
#for route in self.fast_api.routes:
|
|
180
|
+
# print(f"{route.name}: {route.path} [{route.methods}]")
|
|
181
|
+
|
|
182
|
+
# add cleanup hook
|
|
183
|
+
|
|
184
|
+
def cleanup():
|
|
185
|
+
self.service_manager.shutdown()
|
|
186
|
+
|
|
187
|
+
atexit.register(cleanup)
|
|
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
|
+
|
|
55
195
|
# private
|
|
56
196
|
|
|
57
197
|
def add_routes(self):
|
|
58
198
|
"""
|
|
59
|
-
add everything that looks like
|
|
199
|
+
add everything that looks like an http endpoint
|
|
60
200
|
"""
|
|
61
201
|
|
|
62
|
-
|
|
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
|
|
63
234
|
|
|
64
235
|
for descriptor in self.service_manager.descriptors.values():
|
|
65
236
|
if not descriptor.is_component() and descriptor.is_local():
|
|
@@ -76,25 +247,23 @@ class FastAPIServer(Server):
|
|
|
76
247
|
if decorator is not None:
|
|
77
248
|
self.router.add_api_route(
|
|
78
249
|
path=prefix + decorator.args[0],
|
|
79
|
-
endpoint=getattr(instance, method.get_name()),
|
|
250
|
+
endpoint=wrap_service_method(getattr(instance, method.get_name()), method.return_type),
|
|
80
251
|
methods=[decorator.decorator.__name__],
|
|
81
252
|
name=f"{descriptor.get_component_descriptor().name}.{descriptor.name}.{method.get_name()}",
|
|
82
253
|
response_model=method.return_type,
|
|
83
254
|
)
|
|
84
255
|
|
|
85
|
-
def
|
|
256
|
+
def start_server(self, host: str):
|
|
86
257
|
"""
|
|
87
258
|
start the fastapi server in a thread
|
|
88
259
|
"""
|
|
260
|
+
self.host = host
|
|
89
261
|
|
|
90
|
-
config = uvicorn.Config(self.fast_api, host=
|
|
91
|
-
server = uvicorn.Server(config)
|
|
92
|
-
|
|
93
|
-
thread = threading.Thread(target=server.run, daemon=True)
|
|
94
|
-
thread.start()
|
|
95
|
-
|
|
96
|
-
return thread
|
|
262
|
+
config = uvicorn.Config(self.fast_api, host=host, port=self.port, access_log=False)
|
|
97
263
|
|
|
264
|
+
self.server = uvicorn.Server(config)
|
|
265
|
+
self.thread = threading.Thread(target=self.server.run, daemon=True)
|
|
266
|
+
self.thread.start()
|
|
98
267
|
|
|
99
268
|
def get_deserializers(self, service: Type, method):
|
|
100
269
|
deserializers = self.deserializers.get(method, None)
|
|
@@ -106,8 +275,8 @@ class FastAPIServer(Server):
|
|
|
106
275
|
|
|
107
276
|
return deserializers
|
|
108
277
|
|
|
109
|
-
def deserialize_args(self,
|
|
110
|
-
args = list(request.args)
|
|
278
|
+
def deserialize_args(self, args: list[Any], type: Type, method: Callable) -> list:
|
|
279
|
+
#args = list(request.args)
|
|
111
280
|
|
|
112
281
|
deserializers = self.get_deserializers(type, method)
|
|
113
282
|
|
|
@@ -133,22 +302,22 @@ class FastAPIServer(Server):
|
|
|
133
302
|
media_type="text/plain"
|
|
134
303
|
)
|
|
135
304
|
|
|
136
|
-
request =
|
|
305
|
+
request = data
|
|
137
306
|
|
|
138
307
|
if content == "json":
|
|
139
|
-
return await self.dispatch(request)
|
|
308
|
+
return await self.dispatch(http_request, request)
|
|
140
309
|
else:
|
|
141
310
|
return HttpResponse(
|
|
142
|
-
content=msgpack.packb(await self.dispatch(request), use_bin_type=True),
|
|
311
|
+
content=msgpack.packb(await self.dispatch(http_request, request), use_bin_type=True),
|
|
143
312
|
media_type="application/msgpack"
|
|
144
313
|
)
|
|
145
314
|
|
|
146
|
-
async def dispatch(self, request:
|
|
147
|
-
ServiceManager.logger.debug("dispatch request %s", request
|
|
315
|
+
async def dispatch(self, http_request: HttpRequest, request: dict) :
|
|
316
|
+
ServiceManager.logger.debug("dispatch request %s", request["method"])
|
|
148
317
|
|
|
149
318
|
# <comp>:<service>:<method>
|
|
150
319
|
|
|
151
|
-
parts = request
|
|
320
|
+
parts = request["method"].split(":")
|
|
152
321
|
|
|
153
322
|
#component = parts[0]
|
|
154
323
|
service_name = parts[1]
|
|
@@ -159,16 +328,20 @@ class FastAPIServer(Server):
|
|
|
159
328
|
|
|
160
329
|
method = getattr(service, method_name)
|
|
161
330
|
|
|
162
|
-
args = self.deserialize_args(request, service_descriptor.type, method)
|
|
331
|
+
args = self.deserialize_args(request["args"], service_descriptor.type, method)
|
|
163
332
|
try:
|
|
164
333
|
if inspect.iscoroutinefunction(method):
|
|
165
334
|
result = await method(*args)
|
|
166
335
|
else:
|
|
167
336
|
result = method(*args)
|
|
168
337
|
|
|
169
|
-
return Response(result=result, exception=None).
|
|
338
|
+
return Response(result=result, exception=None).model_dump()
|
|
339
|
+
|
|
340
|
+
except HTTPException as e:
|
|
341
|
+
raise
|
|
342
|
+
|
|
170
343
|
except Exception as e:
|
|
171
|
-
return Response(result=None, exception=str(e)).
|
|
344
|
+
return Response(result=None, exception=str(e)).model_dump()
|
|
172
345
|
|
|
173
346
|
# override
|
|
174
347
|
|
|
@@ -185,44 +358,3 @@ class FastAPIServer(Server):
|
|
|
185
358
|
)
|
|
186
359
|
|
|
187
360
|
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
|