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/__init__.py
CHANGED
|
@@ -4,13 +4,14 @@ This module provides the core Aspyx service management framework allowing for se
|
|
|
4
4
|
|
|
5
5
|
from aspyx.di import module
|
|
6
6
|
|
|
7
|
-
from .service import ServiceException, Server, Channel, ComponentDescriptor, inject_service, ChannelAddress, ChannelInstances, ServiceManager, Component, Service, AbstractComponent, ComponentStatus, ComponentRegistry, implementation, health, component, service
|
|
8
|
-
from .channels import HTTPXChannel, DispatchJSONChannel
|
|
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
|
+
from .channels import HTTPXChannel, DispatchJSONChannel, TokenContext
|
|
9
9
|
from .registries import ConsulComponentRegistry
|
|
10
|
-
from .server import FastAPIServer
|
|
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
|
-
|
|
13
|
+
from .session import Session, SessionManager, SessionContext
|
|
14
|
+
from .authorization import AuthorizationManager, AbstractAuthorizationFactory
|
|
14
15
|
|
|
15
16
|
@module()
|
|
16
17
|
class ServiceModule:
|
|
@@ -38,6 +39,25 @@ __all__ = [
|
|
|
38
39
|
"service",
|
|
39
40
|
"implementation",
|
|
40
41
|
"inject_service",
|
|
42
|
+
"component_services",
|
|
43
|
+
"RemoteServiceException",
|
|
44
|
+
"ServiceCommunicationException",
|
|
45
|
+
"TokenException",
|
|
46
|
+
"TokenExpiredException",
|
|
47
|
+
"InvalidTokenException",
|
|
48
|
+
"MissingTokenException",
|
|
49
|
+
"AuthorizationException",
|
|
50
|
+
|
|
51
|
+
# authorization
|
|
52
|
+
|
|
53
|
+
"AuthorizationManager",
|
|
54
|
+
"AbstractAuthorizationFactory",
|
|
55
|
+
|
|
56
|
+
# session
|
|
57
|
+
|
|
58
|
+
"Session",
|
|
59
|
+
"SessionManager",
|
|
60
|
+
"SessionContext",
|
|
41
61
|
|
|
42
62
|
# healthcheck
|
|
43
63
|
|
|
@@ -54,6 +74,7 @@ __all__ = [
|
|
|
54
74
|
|
|
55
75
|
"HTTPXChannel",
|
|
56
76
|
"DispatchJSONChannel",
|
|
77
|
+
"TokenContext",
|
|
57
78
|
|
|
58
79
|
# rest
|
|
59
80
|
|
|
@@ -72,5 +93,9 @@ __all__ = [
|
|
|
72
93
|
|
|
73
94
|
# server
|
|
74
95
|
|
|
75
|
-
"FastAPIServer"
|
|
96
|
+
"FastAPIServer",
|
|
97
|
+
"RequestContext",
|
|
98
|
+
"ResponseContext",
|
|
99
|
+
"TokenContext",
|
|
100
|
+
"TokenContextMiddleware",
|
|
76
101
|
]
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""
|
|
2
|
+
authorization logic
|
|
3
|
+
"""
|
|
4
|
+
import inspect
|
|
5
|
+
from abc import abstractmethod, ABC
|
|
6
|
+
from typing import Optional, Callable
|
|
7
|
+
|
|
8
|
+
from aspyx.di import injectable, inject, order
|
|
9
|
+
from aspyx.di.aop import Invocation
|
|
10
|
+
from aspyx.reflection import TypeDescriptor, Decorators
|
|
11
|
+
|
|
12
|
+
def get_method_class(method):
|
|
13
|
+
if inspect.ismethod(method) or inspect.isfunction(method):
|
|
14
|
+
qualname = method.__qualname__
|
|
15
|
+
parts = qualname.split('.')
|
|
16
|
+
if len(parts) > 1:
|
|
17
|
+
cls_name = parts[-2]
|
|
18
|
+
module = inspect.getmodule(method)
|
|
19
|
+
if module:
|
|
20
|
+
for name, obj in inspect.getmembers(module, inspect.isclass):
|
|
21
|
+
if name == cls_name and hasattr(obj, method.__name__):
|
|
22
|
+
return obj
|
|
23
|
+
return None
|
|
24
|
+
|
|
25
|
+
@injectable()
|
|
26
|
+
class AuthorizationManager:
|
|
27
|
+
"""
|
|
28
|
+
The authorization manager is used to remember and execute pluggable authorization checks.
|
|
29
|
+
"""
|
|
30
|
+
class Authorization():
|
|
31
|
+
"""
|
|
32
|
+
Base class for authorization checks
|
|
33
|
+
"""
|
|
34
|
+
def authorize(self, invocation: Invocation):
|
|
35
|
+
"""
|
|
36
|
+
execute the authorization check. Throws an exception in case of violations
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
class AuthorizationFactory(ABC):
|
|
40
|
+
"""
|
|
41
|
+
An authorization factory is used to create possible authorization checks given a method descriptor
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
def __init__(self, order = 0):
|
|
45
|
+
self.order = order
|
|
46
|
+
|
|
47
|
+
@abstractmethod
|
|
48
|
+
def compute_authorization(self, method_descriptor: TypeDescriptor.MethodDescriptor) -> Optional['AuthorizationManager.Authorization']:
|
|
49
|
+
"""
|
|
50
|
+
return a possible authorization check given a method descriptor
|
|
51
|
+
Args:
|
|
52
|
+
method_descriptor: the corresponding method descriptor
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
an authorization check or None
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
# constructor
|
|
59
|
+
|
|
60
|
+
def __init__(self):
|
|
61
|
+
self.factories : list[AuthorizationManager.AuthorizationFactory] = []
|
|
62
|
+
self.checks : dict[Callable, list[AuthorizationManager.Authorization]] = {}
|
|
63
|
+
|
|
64
|
+
# public
|
|
65
|
+
|
|
66
|
+
def register_factory(self, factory: 'AuthorizationManager.AuthorizationFactory'):
|
|
67
|
+
self.factories.append(factory)
|
|
68
|
+
|
|
69
|
+
self.factories.sort(key=lambda factory: factory.order)
|
|
70
|
+
|
|
71
|
+
# internal
|
|
72
|
+
|
|
73
|
+
def compute_checks(self, func: Callable) -> list[Authorization]:
|
|
74
|
+
checks = []
|
|
75
|
+
|
|
76
|
+
clazz = get_method_class(func)
|
|
77
|
+
|
|
78
|
+
descriptor = TypeDescriptor.for_type(clazz).get_method(func.__name__)
|
|
79
|
+
|
|
80
|
+
for factory in self.factories:
|
|
81
|
+
check = factory.compute_authorization(descriptor)
|
|
82
|
+
if check is not None:
|
|
83
|
+
checks.append(check)
|
|
84
|
+
|
|
85
|
+
return checks
|
|
86
|
+
|
|
87
|
+
def get_checks(self, func: Callable) -> list[Authorization]:
|
|
88
|
+
"""
|
|
89
|
+
return a list of authorization checks given a function.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
func: the corresponding function.
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
list of authorization checks
|
|
96
|
+
"""
|
|
97
|
+
checks = self.checks.get(func, None)
|
|
98
|
+
if checks is None:
|
|
99
|
+
checks = self.compute_checks(func)
|
|
100
|
+
self.checks[func] = checks
|
|
101
|
+
print(checks)
|
|
102
|
+
|
|
103
|
+
return checks
|
|
104
|
+
|
|
105
|
+
def authorize(self, invocation: Invocation):
|
|
106
|
+
for check in self.get_checks(invocation.func):
|
|
107
|
+
check.authorize(invocation)
|
|
108
|
+
|
|
109
|
+
class AbstractAuthorizationFactory(AuthorizationManager.AuthorizationFactory):
|
|
110
|
+
"""
|
|
111
|
+
Abstract base class for authorization factories
|
|
112
|
+
"""
|
|
113
|
+
|
|
114
|
+
# constructor
|
|
115
|
+
|
|
116
|
+
def __init__(self):
|
|
117
|
+
super().__init__(0)
|
|
118
|
+
|
|
119
|
+
if Decorators.has_decorator(type(self), order):
|
|
120
|
+
self.order = Decorators.get_decorator(type(self), order).args[0]
|
|
121
|
+
|
|
122
|
+
# inject
|
|
123
|
+
|
|
124
|
+
@inject()
|
|
125
|
+
def set_authorization_manager(self, authorization_manager: AuthorizationManager):
|
|
126
|
+
authorization_manager.register_factory(self)
|
aspyx_service/channels.py
CHANGED
|
@@ -3,22 +3,62 @@ Service management and dependency injection framework.
|
|
|
3
3
|
"""
|
|
4
4
|
from __future__ import annotations
|
|
5
5
|
|
|
6
|
-
import
|
|
7
|
-
from
|
|
6
|
+
import typing
|
|
7
|
+
from contextlib import contextmanager
|
|
8
|
+
from dataclasses import is_dataclass, fields
|
|
8
9
|
from typing import Type, Optional, Any, Callable
|
|
9
10
|
|
|
11
|
+
import httpx
|
|
10
12
|
import msgpack
|
|
11
|
-
from httpx import Client, AsyncClient
|
|
13
|
+
from httpx import Client, AsyncClient, USE_CLIENT_DEFAULT
|
|
12
14
|
from pydantic import BaseModel
|
|
13
15
|
|
|
14
16
|
from aspyx.di.configuration import inject_value
|
|
15
17
|
from aspyx.reflection import DynamicProxy, TypeDescriptor
|
|
16
|
-
from aspyx.threading import ThreadLocal
|
|
17
|
-
from .
|
|
18
|
+
from aspyx.threading import ThreadLocal, ContextLocal
|
|
19
|
+
from aspyx.util import get_deserializer, TypeDeserializer, TypeSerializer, get_serializer
|
|
20
|
+
from .service import ServiceManager, ServiceCommunicationException, TokenExpiredException, InvalidTokenException, \
|
|
21
|
+
AuthorizationException, MissingTokenException
|
|
18
22
|
|
|
19
23
|
from .service import ComponentDescriptor, ChannelInstances, ServiceException, channel, Channel, RemoteServiceException
|
|
20
|
-
from .serialization import get_deserializer
|
|
21
24
|
|
|
25
|
+
class TokenContext:
|
|
26
|
+
"""
|
|
27
|
+
TokeContext covers two context locals for both the access and - optional - refresh topen
|
|
28
|
+
"""
|
|
29
|
+
access_token = ContextLocal[str]("access_token", default=None)
|
|
30
|
+
refresh_token = ContextLocal[str]("refresh_token", default=None)
|
|
31
|
+
|
|
32
|
+
@classmethod
|
|
33
|
+
def get_access_token(cls) -> Optional[str]:
|
|
34
|
+
return cls.access_token.get()
|
|
35
|
+
|
|
36
|
+
@classmethod
|
|
37
|
+
def get_refresh_token(cls) -> Optional[str]:
|
|
38
|
+
return cls.refresh_token.get()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@classmethod
|
|
42
|
+
def set(cls, access_token: str, refresh_token: Optional[str] = None):
|
|
43
|
+
cls.access_token.set(access_token)
|
|
44
|
+
if refresh_token:
|
|
45
|
+
cls.refresh_token.set(refresh_token)
|
|
46
|
+
|
|
47
|
+
@classmethod
|
|
48
|
+
def clear(cls):
|
|
49
|
+
cls.access_token.set(None)
|
|
50
|
+
cls.refresh_token.set(None)
|
|
51
|
+
|
|
52
|
+
@classmethod
|
|
53
|
+
@contextmanager
|
|
54
|
+
def use(cls, access_token: str, refresh_token: Optional[str] = None):
|
|
55
|
+
access_token = cls.access_token.set(access_token)
|
|
56
|
+
refresh_token = cls.refresh_token.set(refresh_token)
|
|
57
|
+
try:
|
|
58
|
+
yield
|
|
59
|
+
finally:
|
|
60
|
+
cls.access_token.reset(access_token)
|
|
61
|
+
cls.refresh_token.reset(refresh_token)
|
|
22
62
|
|
|
23
63
|
class HTTPXChannel(Channel):
|
|
24
64
|
__slots__ = [
|
|
@@ -26,7 +66,8 @@ class HTTPXChannel(Channel):
|
|
|
26
66
|
"async_client",
|
|
27
67
|
"service_names",
|
|
28
68
|
"deserializers",
|
|
29
|
-
"timeout"
|
|
69
|
+
"timeout",
|
|
70
|
+
"optimize_serialization"
|
|
30
71
|
]
|
|
31
72
|
|
|
32
73
|
# class properties
|
|
@@ -39,8 +80,7 @@ class HTTPXChannel(Channel):
|
|
|
39
80
|
@classmethod
|
|
40
81
|
def to_dict(cls, obj: Any) -> Any:
|
|
41
82
|
if isinstance(obj, BaseModel):
|
|
42
|
-
return obj.
|
|
43
|
-
|
|
83
|
+
return obj.model_dump()
|
|
44
84
|
|
|
45
85
|
elif is_dataclass(obj):
|
|
46
86
|
return {
|
|
@@ -57,10 +97,6 @@ class HTTPXChannel(Channel):
|
|
|
57
97
|
|
|
58
98
|
return obj
|
|
59
99
|
|
|
60
|
-
@classmethod
|
|
61
|
-
def to_json(cls, obj) -> str:
|
|
62
|
-
return json.dumps(cls.to_dict(obj))
|
|
63
|
-
|
|
64
100
|
# constructor
|
|
65
101
|
|
|
66
102
|
def __init__(self):
|
|
@@ -68,7 +104,9 @@ class HTTPXChannel(Channel):
|
|
|
68
104
|
|
|
69
105
|
self.timeout = 1000.0
|
|
70
106
|
self.service_names: dict[Type, str] = {}
|
|
107
|
+
self.serializers: dict[Callable, list[Callable]] = {}
|
|
71
108
|
self.deserializers: dict[Callable, Callable] = {}
|
|
109
|
+
self.optimize_serialization = True
|
|
72
110
|
|
|
73
111
|
# inject
|
|
74
112
|
|
|
@@ -78,7 +116,27 @@ class HTTPXChannel(Channel):
|
|
|
78
116
|
|
|
79
117
|
# protected
|
|
80
118
|
|
|
81
|
-
def
|
|
119
|
+
def serialize_args(self, invocation: DynamicProxy.Invocation) -> list[Any]:
|
|
120
|
+
deserializers = self.get_serializers(invocation.type, invocation.method)
|
|
121
|
+
|
|
122
|
+
args = list(invocation.args)
|
|
123
|
+
for index, deserializer in enumerate(deserializers):
|
|
124
|
+
args[index] = deserializer(args[index])
|
|
125
|
+
|
|
126
|
+
return args
|
|
127
|
+
|
|
128
|
+
def get_serializers(self, type: Type, method: Callable) -> list[TypeSerializer]:
|
|
129
|
+
serializers = self.serializers.get(method, None)
|
|
130
|
+
if serializers is None:
|
|
131
|
+
param_types = TypeDescriptor.for_type(type).get_method(method.__name__).param_types
|
|
132
|
+
|
|
133
|
+
serializers = [get_serializer(type) for type in param_types]
|
|
134
|
+
|
|
135
|
+
self.serializers[method] = serializers
|
|
136
|
+
|
|
137
|
+
return serializers
|
|
138
|
+
|
|
139
|
+
def get_deserializer(self, type: Type, method: Callable) -> TypeDeserializer:
|
|
82
140
|
deserializer = self.deserializers.get(method, None)
|
|
83
141
|
if deserializer is None:
|
|
84
142
|
return_type = TypeDescriptor.for_type(type).get_method(method.__name__).return_type
|
|
@@ -125,6 +183,80 @@ class HTTPXChannel(Channel):
|
|
|
125
183
|
def make_async_client(self) -> AsyncClient:
|
|
126
184
|
return AsyncClient() # base_url=url
|
|
127
185
|
|
|
186
|
+
def request(self, http_method: str, url: str, json: Optional[typing.Any] = None,
|
|
187
|
+
params: Optional[Any] = None, headers: Optional[Any] = None,
|
|
188
|
+
timeout: Any = USE_CLIENT_DEFAULT, content: Optional[Any] = None) -> httpx.Response:
|
|
189
|
+
|
|
190
|
+
token = TokenContext.get_access_token()
|
|
191
|
+
if token is not None:
|
|
192
|
+
if headers is None: # None is also valid!
|
|
193
|
+
headers = {}
|
|
194
|
+
|
|
195
|
+
## add bearer token
|
|
196
|
+
|
|
197
|
+
headers["Authorization"] = f"Bearer {token}"
|
|
198
|
+
|
|
199
|
+
try:
|
|
200
|
+
response = self.get_client().request(http_method, url, params=params, json=json, headers=headers, timeout=timeout, content=content)
|
|
201
|
+
response.raise_for_status()
|
|
202
|
+
except httpx.RequestError as e:
|
|
203
|
+
raise ServiceCommunicationException(str(e)) from e
|
|
204
|
+
|
|
205
|
+
except httpx.HTTPStatusError as e:
|
|
206
|
+
if e.response.status_code == 401:
|
|
207
|
+
www_auth = e.response.headers.get("www-authenticate", "")
|
|
208
|
+
if "invalid_token" in www_auth:
|
|
209
|
+
if 'expired' in www_auth:
|
|
210
|
+
raise TokenExpiredException() from e
|
|
211
|
+
|
|
212
|
+
if 'missing' in www_auth:
|
|
213
|
+
raise MissingTokenException() from e
|
|
214
|
+
|
|
215
|
+
raise InvalidTokenException() from e
|
|
216
|
+
|
|
217
|
+
raise AuthorizationException(str(e)) from e
|
|
218
|
+
except httpx.HTTPError as e:
|
|
219
|
+
raise RemoteServiceException(str(e)) from e
|
|
220
|
+
|
|
221
|
+
return response
|
|
222
|
+
|
|
223
|
+
async def request_async(self, http_method: str, url: str, json: Optional[typing.Any] = None,
|
|
224
|
+
params: Optional[Any] = None, headers: Optional[Any] = None,
|
|
225
|
+
timeout: Any = USE_CLIENT_DEFAULT, content: Optional[Any] = None) -> httpx.Response:
|
|
226
|
+
|
|
227
|
+
token = TokenContext.get_access_token()
|
|
228
|
+
if token is not None:
|
|
229
|
+
if headers is None: # None is also valid!
|
|
230
|
+
headers = {}
|
|
231
|
+
|
|
232
|
+
## add bearer token
|
|
233
|
+
|
|
234
|
+
headers["Authorization"] = f"Bearer {token}"
|
|
235
|
+
|
|
236
|
+
try:
|
|
237
|
+
response = await self.get_async_client().request(http_method, url, params=params, json=json, headers=headers,
|
|
238
|
+
timeout=timeout, content=content)
|
|
239
|
+
response.raise_for_status()
|
|
240
|
+
except httpx.RequestError as e:
|
|
241
|
+
raise ServiceCommunicationException(str(e)) from e
|
|
242
|
+
|
|
243
|
+
except httpx.HTTPStatusError as e:
|
|
244
|
+
if e.response.status_code == 401:
|
|
245
|
+
www_auth = e.response.headers.get("www-authenticate", "")
|
|
246
|
+
if "invalid_token" in www_auth:
|
|
247
|
+
if 'expired' in www_auth:
|
|
248
|
+
raise TokenExpiredException() from e
|
|
249
|
+
elif 'missing' in www_auth:
|
|
250
|
+
raise MissingTokenException() from e
|
|
251
|
+
else:
|
|
252
|
+
raise InvalidTokenException() from e
|
|
253
|
+
|
|
254
|
+
raise RemoteServiceException(str(e)) from e
|
|
255
|
+
except httpx.HTTPError as e:
|
|
256
|
+
raise RemoteServiceException(str(e)) from e
|
|
257
|
+
|
|
258
|
+
return response
|
|
259
|
+
|
|
128
260
|
class Request(BaseModel):
|
|
129
261
|
method: str # component:service:method
|
|
130
262
|
args: tuple[Any, ...]
|
|
@@ -158,20 +290,25 @@ class DispatchJSONChannel(HTTPXChannel):
|
|
|
158
290
|
|
|
159
291
|
def invoke(self, invocation: DynamicProxy.Invocation):
|
|
160
292
|
service_name = self.service_names[invocation.type] # map type to registered service name
|
|
161
|
-
request = Request(method=f"{self.component_descriptor.name}:{service_name}:{invocation.method.__name__}",
|
|
162
|
-
args=invocation.args)
|
|
163
293
|
|
|
164
|
-
dict =
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
raise RemoteServiceException(f"server side exception {result.exception}")
|
|
294
|
+
request : dict = {
|
|
295
|
+
"method": f"{self.component_descriptor.name}:{service_name}:{invocation.method.__name__}"
|
|
296
|
+
#"args": invocation.args
|
|
297
|
+
}
|
|
169
298
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
299
|
+
if self.optimize_serialization:
|
|
300
|
+
request["args"] = self.serialize_args(invocation)
|
|
301
|
+
else:
|
|
302
|
+
request["args"] = self.to_dict(invocation.args)
|
|
173
303
|
|
|
174
|
-
|
|
304
|
+
try:
|
|
305
|
+
http_result = self.request( "post", f"{self.get_url()}/invoke", json=request, timeout=self.timeout)
|
|
306
|
+
result = http_result.json()
|
|
307
|
+
if result["exception"] is not None:
|
|
308
|
+
raise RemoteServiceException(f"server side exception {result['exception']}")
|
|
309
|
+
|
|
310
|
+
return self.get_deserializer(invocation.type, invocation.method)(result["result"])
|
|
311
|
+
except (ServiceCommunicationException, AuthorizationException, RemoteServiceException) as e:
|
|
175
312
|
raise
|
|
176
313
|
|
|
177
314
|
except Exception as e:
|
|
@@ -180,22 +317,25 @@ class DispatchJSONChannel(HTTPXChannel):
|
|
|
180
317
|
|
|
181
318
|
async def invoke_async(self, invocation: DynamicProxy.Invocation):
|
|
182
319
|
service_name = self.service_names[invocation.type] # map type to registered service name
|
|
183
|
-
request =
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
try:
|
|
187
|
-
data = await self.get_async_client().post(f"{self.get_url()}/invoke", json=dict, timeout=self.timeout)
|
|
188
|
-
result = Response(**data.json())
|
|
320
|
+
request : dict = {
|
|
321
|
+
"method": f"{self.component_descriptor.name}:{service_name}:{invocation.method.__name__}"
|
|
322
|
+
}
|
|
189
323
|
|
|
190
|
-
|
|
191
|
-
|
|
324
|
+
if self.optimize_serialization:
|
|
325
|
+
request["args"] = self.serialize_args(invocation)
|
|
326
|
+
else:
|
|
327
|
+
request["args"] = self.to_dict(invocation.args)
|
|
192
328
|
|
|
193
|
-
|
|
329
|
+
try:
|
|
330
|
+
data = await self.request_async("post", f"{self.get_url()}/invoke", json=request, timeout=self.timeout)
|
|
331
|
+
result = data.json()
|
|
194
332
|
|
|
195
|
-
|
|
196
|
-
|
|
333
|
+
if result["exception"] is not None:
|
|
334
|
+
raise RemoteServiceException(f"server side exception {result['exception']}")
|
|
197
335
|
|
|
198
|
-
|
|
336
|
+
return self.get_deserializer(invocation.type, invocation.method)(result["result"])
|
|
337
|
+
|
|
338
|
+
except (ServiceCommunicationException, AuthorizationException, RemoteServiceException) as e:
|
|
199
339
|
raise
|
|
200
340
|
|
|
201
341
|
except Exception as e:
|
|
@@ -221,13 +361,19 @@ class DispatchMSPackChannel(HTTPXChannel):
|
|
|
221
361
|
|
|
222
362
|
def invoke(self, invocation: DynamicProxy.Invocation):
|
|
223
363
|
service_name = self.service_names[invocation.type] # map type to registered service name
|
|
224
|
-
request =
|
|
225
|
-
|
|
364
|
+
request: dict = {
|
|
365
|
+
"method": f"{self.component_descriptor.name}:{service_name}:{invocation.method.__name__}"
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if self.optimize_serialization:
|
|
369
|
+
request["args"] = self.serialize_args(invocation)
|
|
370
|
+
else:
|
|
371
|
+
request["args"] = self.to_dict(invocation.args)
|
|
226
372
|
|
|
227
373
|
try:
|
|
228
|
-
packed = msgpack.packb(
|
|
374
|
+
packed = msgpack.packb(request, use_bin_type=True)
|
|
229
375
|
|
|
230
|
-
response = self.
|
|
376
|
+
response = self.request("post",
|
|
231
377
|
f"{self.get_url()}/invoke",
|
|
232
378
|
content=packed,
|
|
233
379
|
headers={"Content-Type": "application/msgpack"},
|
|
@@ -241,6 +387,25 @@ class DispatchMSPackChannel(HTTPXChannel):
|
|
|
241
387
|
|
|
242
388
|
return self.get_deserializer(invocation.type, invocation.method)(result["result"])
|
|
243
389
|
|
|
390
|
+
|
|
391
|
+
except httpx.RequestError as e:
|
|
392
|
+
raise ServiceCommunicationException(str(e)) from e
|
|
393
|
+
|
|
394
|
+
except httpx.HTTPStatusError as e:
|
|
395
|
+
if e.response.status_code == 401:
|
|
396
|
+
www_auth = e.response.headers.get("www-authenticate", "")
|
|
397
|
+
if "invalid_token" in www_auth:
|
|
398
|
+
if 'expired' in www_auth:
|
|
399
|
+
raise TokenExpiredException() from e
|
|
400
|
+
elif 'missing' in www_auth:
|
|
401
|
+
raise MissingTokenException() from e
|
|
402
|
+
else:
|
|
403
|
+
raise InvalidTokenException() from e
|
|
404
|
+
|
|
405
|
+
raise RemoteServiceException(str(e)) from e
|
|
406
|
+
except httpx.HTTPError as e:
|
|
407
|
+
raise RemoteServiceException(str(e)) from e
|
|
408
|
+
|
|
244
409
|
except ServiceCommunicationException:
|
|
245
410
|
raise
|
|
246
411
|
|
|
@@ -252,13 +417,19 @@ class DispatchMSPackChannel(HTTPXChannel):
|
|
|
252
417
|
|
|
253
418
|
async def invoke_async(self, invocation: DynamicProxy.Invocation):
|
|
254
419
|
service_name = self.service_names[invocation.type] # map type to registered service name
|
|
255
|
-
request =
|
|
256
|
-
|
|
420
|
+
request: dict = {
|
|
421
|
+
"method": f"{self.component_descriptor.name}:{service_name}:{invocation.method.__name__}"
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
if self.optimize_serialization:
|
|
425
|
+
request["args"] = self.serialize_args(invocation)
|
|
426
|
+
else:
|
|
427
|
+
request["args"] = self.to_dict(invocation.args)
|
|
257
428
|
|
|
258
429
|
try:
|
|
259
|
-
packed = msgpack.packb(
|
|
430
|
+
packed = msgpack.packb(request, use_bin_type=True)
|
|
260
431
|
|
|
261
|
-
response = await self.
|
|
432
|
+
response = await self.request_async("post",
|
|
262
433
|
f"{self.get_url()}/invoke",
|
|
263
434
|
content=packed,
|
|
264
435
|
headers={"Content-Type": "application/msgpack"},
|
|
@@ -272,6 +443,26 @@ class DispatchMSPackChannel(HTTPXChannel):
|
|
|
272
443
|
|
|
273
444
|
return self.get_deserializer(invocation.type, invocation.method)(result["result"])
|
|
274
445
|
|
|
446
|
+
except httpx.RequestError as e:
|
|
447
|
+
raise ServiceCommunicationException(str(e)) from e
|
|
448
|
+
|
|
449
|
+
except httpx.HTTPStatusError as e:
|
|
450
|
+
if e.response.status_code == 401:
|
|
451
|
+
www_auth = e.response.headers.get("www-authenticate", "")
|
|
452
|
+
if "invalid_token" in www_auth:
|
|
453
|
+
if 'expired' in www_auth:
|
|
454
|
+
raise TokenExpiredException() from e
|
|
455
|
+
|
|
456
|
+
if 'missing' in www_auth:
|
|
457
|
+
raise MissingTokenException() from e
|
|
458
|
+
|
|
459
|
+
raise InvalidTokenException() from e
|
|
460
|
+
|
|
461
|
+
raise RemoteServiceException(str(e)) from e
|
|
462
|
+
|
|
463
|
+
except httpx.HTTPError as e:
|
|
464
|
+
raise RemoteServiceException(str(e)) from e
|
|
465
|
+
|
|
275
466
|
except ServiceCommunicationException:
|
|
276
467
|
raise
|
|
277
468
|
|
aspyx_service/healthcheck.py
CHANGED
aspyx_service/registries.py
CHANGED
|
@@ -42,19 +42,19 @@ class ConsulComponentRegistry(ComponentRegistry):
|
|
|
42
42
|
# injections
|
|
43
43
|
|
|
44
44
|
@inject_value("consul.watchdog.interval", default=5)
|
|
45
|
-
def
|
|
45
|
+
def set_watchdog_interval(self, interval):
|
|
46
46
|
self.watchdog_interval = interval
|
|
47
47
|
|
|
48
48
|
@inject_value("consul.healthcheck.interval", default="10s")
|
|
49
|
-
def
|
|
49
|
+
def set_healthcheck_interval(self, interval):
|
|
50
50
|
self.healthcheck_interval = interval
|
|
51
51
|
|
|
52
52
|
@inject_value("consul.healthcheck.timeout", default="3s")
|
|
53
|
-
def
|
|
53
|
+
def set_healthcheck_timeout(self, interval):
|
|
54
54
|
self.healthcheck_timeout = interval
|
|
55
55
|
|
|
56
56
|
@inject_value("consul.healthcheck.deregister", default="5m")
|
|
57
|
-
def
|
|
57
|
+
def set_healthcheck_deregister(self, interval):
|
|
58
58
|
self.healthcheck_deregister = interval
|
|
59
59
|
|
|
60
60
|
# lifecycle hooks
|
|
@@ -220,7 +220,7 @@ class ConsulComponentRegistry(ComponentRegistry):
|
|
|
220
220
|
|
|
221
221
|
# only cache if non-empty
|
|
222
222
|
|
|
223
|
-
if
|
|
223
|
+
if component_addresses:
|
|
224
224
|
self.component_addresses[descriptor.name] = component_addresses
|
|
225
225
|
|
|
226
226
|
return list(component_addresses.values())
|