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/__init__.py
CHANGED
|
@@ -4,12 +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
|
|
11
11
|
from .healthcheck import health_checks, health_check, HealthCheckManager, HealthStatus
|
|
12
12
|
from .restchannel import RestChannel, post, get, put, delete, QueryParam, Body, rest
|
|
13
|
+
from .session import Session, SessionManager
|
|
14
|
+
from .authorization import AuthorizationManager, AbstractAuthorizationFactory, AuthorizationException
|
|
13
15
|
|
|
14
16
|
|
|
15
17
|
@module()
|
|
@@ -38,6 +40,25 @@ __all__ = [
|
|
|
38
40
|
"service",
|
|
39
41
|
"implementation",
|
|
40
42
|
"inject_service",
|
|
43
|
+
"component_services",
|
|
44
|
+
"RemoteServiceException",
|
|
45
|
+
"ServiceCommunicationException",
|
|
46
|
+
"TokenException",
|
|
47
|
+
"TokenExpiredException",
|
|
48
|
+
"InvalidTokenException",
|
|
49
|
+
"MissingTokenException",
|
|
50
|
+
"AuthorizationException",
|
|
51
|
+
|
|
52
|
+
# authorization
|
|
53
|
+
|
|
54
|
+
"AuthorizationManager",
|
|
55
|
+
"AbstractAuthorizationFactory",
|
|
56
|
+
"AuthorizationException",
|
|
57
|
+
|
|
58
|
+
# session
|
|
59
|
+
|
|
60
|
+
"Session",
|
|
61
|
+
"SessionManager",
|
|
41
62
|
|
|
42
63
|
# healthcheck
|
|
43
64
|
|
|
@@ -54,6 +75,7 @@ __all__ = [
|
|
|
54
75
|
|
|
55
76
|
"HTTPXChannel",
|
|
56
77
|
"DispatchJSONChannel",
|
|
78
|
+
"TokenContext",
|
|
57
79
|
|
|
58
80
|
# rest
|
|
59
81
|
|
|
@@ -69,8 +91,10 @@ __all__ = [
|
|
|
69
91
|
# registries
|
|
70
92
|
|
|
71
93
|
"ConsulComponentRegistry",
|
|
94
|
+
"RequestContext",
|
|
72
95
|
|
|
73
96
|
# server
|
|
74
97
|
|
|
75
|
-
"FastAPIServer"
|
|
98
|
+
"FastAPIServer",
|
|
99
|
+
"RequestContext"
|
|
76
100
|
]
|
|
@@ -0,0 +1,135 @@
|
|
|
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
|
+
|
|
13
|
+
class AuthorizationException(Exception):
|
|
14
|
+
"""
|
|
15
|
+
Any authorization exception
|
|
16
|
+
"""
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
def get_method_class(method):
|
|
20
|
+
if inspect.ismethod(method) or inspect.isfunction(method):
|
|
21
|
+
qualname = method.__qualname__
|
|
22
|
+
parts = qualname.split('.')
|
|
23
|
+
if len(parts) > 1:
|
|
24
|
+
cls_name = parts[-2]
|
|
25
|
+
module = inspect.getmodule(method)
|
|
26
|
+
if module:
|
|
27
|
+
for name, obj in inspect.getmembers(module, inspect.isclass):
|
|
28
|
+
if name == cls_name and hasattr(obj, method.__name__):
|
|
29
|
+
return obj
|
|
30
|
+
return None
|
|
31
|
+
|
|
32
|
+
@injectable()
|
|
33
|
+
class AuthorizationManager:
|
|
34
|
+
"""
|
|
35
|
+
The authorization manager is used to remember and execute pluggable authorization checks.
|
|
36
|
+
"""
|
|
37
|
+
class Authorization():
|
|
38
|
+
"""
|
|
39
|
+
Base class for authorization checks
|
|
40
|
+
"""
|
|
41
|
+
def check(self, invocation: Invocation):
|
|
42
|
+
"""
|
|
43
|
+
execute the authorization check. Throws an exception in case of violations,
|
|
44
|
+
"""
|
|
45
|
+
pass
|
|
46
|
+
|
|
47
|
+
class AuthorizationFactory(ABC):
|
|
48
|
+
"""
|
|
49
|
+
An authorization factory is used to create possible authorization checks given a method descriptor
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
def __init__(self, order = 0):
|
|
53
|
+
self.order = order
|
|
54
|
+
|
|
55
|
+
@abstractmethod
|
|
56
|
+
def compute_authorization(self, method_descriptor: TypeDescriptor.MethodDescriptor) -> Optional['AuthorizationManager.Authorization']:
|
|
57
|
+
"""
|
|
58
|
+
return a possible authorization check given a method descriptor
|
|
59
|
+
Args:
|
|
60
|
+
method_descriptor: the corresponding method descriptor
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
an authorization check or None
|
|
64
|
+
"""
|
|
65
|
+
pass
|
|
66
|
+
|
|
67
|
+
# constructor
|
|
68
|
+
|
|
69
|
+
def __init__(self):
|
|
70
|
+
self.factories : list[AuthorizationManager.AuthorizationFactory] = []
|
|
71
|
+
self.checks : dict[Callable, list[AuthorizationManager.Authorization]] = {}
|
|
72
|
+
|
|
73
|
+
# public
|
|
74
|
+
|
|
75
|
+
def register_factory(self, factory: 'AuthorizationManager.AuthorizationFactory'):
|
|
76
|
+
self.factories.append(factory)
|
|
77
|
+
|
|
78
|
+
self.factories.sort(key=lambda factory: factory.order)
|
|
79
|
+
|
|
80
|
+
# internal
|
|
81
|
+
|
|
82
|
+
def compute_checks(self, func: Callable) -> list[Authorization]:
|
|
83
|
+
checks = []
|
|
84
|
+
|
|
85
|
+
clazz = get_method_class(func)
|
|
86
|
+
|
|
87
|
+
descriptor = TypeDescriptor.for_type(clazz).get_method(func.__name__)
|
|
88
|
+
|
|
89
|
+
for factory in self.factories:
|
|
90
|
+
check = factory.compute_authorization(descriptor)
|
|
91
|
+
if check is not None:
|
|
92
|
+
checks.append(check)
|
|
93
|
+
|
|
94
|
+
return checks
|
|
95
|
+
|
|
96
|
+
def get_checks(self, func: Callable) -> list[Authorization]:
|
|
97
|
+
"""
|
|
98
|
+
return a list of authorization checks given a function.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
func: the corresponding function.
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
list of authorization checks
|
|
105
|
+
"""
|
|
106
|
+
checks = self.checks.get(func, None)
|
|
107
|
+
if checks is None:
|
|
108
|
+
checks = self.compute_checks(func)
|
|
109
|
+
self.checks[func] = checks
|
|
110
|
+
print(checks)
|
|
111
|
+
|
|
112
|
+
return checks
|
|
113
|
+
|
|
114
|
+
def check(self, invocation: Invocation) -> Optional[Authorization]:
|
|
115
|
+
for check in self.get_checks(invocation.func):
|
|
116
|
+
check.check(invocation)
|
|
117
|
+
|
|
118
|
+
class AbstractAuthorizationFactory(AuthorizationManager.AuthorizationFactory):
|
|
119
|
+
"""
|
|
120
|
+
Abstract base class for authorization factories
|
|
121
|
+
"""
|
|
122
|
+
|
|
123
|
+
# constructor
|
|
124
|
+
|
|
125
|
+
def __init__(self):
|
|
126
|
+
super().__init__(0)
|
|
127
|
+
|
|
128
|
+
if Decorators.has_decorator(type(self), order):
|
|
129
|
+
self.order = Decorators.get_decorator(type(self), order).args[0]
|
|
130
|
+
|
|
131
|
+
# inject
|
|
132
|
+
|
|
133
|
+
@inject()
|
|
134
|
+
def set_authorization_manager(self, authorization_manager: AuthorizationManager):
|
|
135
|
+
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 .service import ServiceManager, ServiceCommunicationException
|
|
18
|
+
from aspyx.threading import ThreadLocal, ContextLocal
|
|
19
|
+
from .service import ServiceManager, ServiceCommunicationException, TokenExpiredException, InvalidTokenException, \
|
|
20
|
+
AuthorizationException, MissingTokenException
|
|
18
21
|
|
|
19
22
|
from .service import ComponentDescriptor, ChannelInstances, ServiceException, channel, Channel, RemoteServiceException
|
|
20
|
-
from .serialization import get_deserializer
|
|
23
|
+
from .serialization import get_deserializer, TypeDeserializer, TypeSerializer, get_serializer
|
|
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,79 @@ 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
|
+
elif 'missing' in www_auth:
|
|
212
|
+
raise MissingTokenException() from e
|
|
213
|
+
else:
|
|
214
|
+
raise InvalidTokenException() from e
|
|
215
|
+
|
|
216
|
+
raise AuthorizationException(str(e)) from e
|
|
217
|
+
except httpx.HTTPError as e:
|
|
218
|
+
raise RemoteServiceException(str(e)) from e
|
|
219
|
+
|
|
220
|
+
return response
|
|
221
|
+
|
|
222
|
+
async def request_async(self, http_method: str, url: str, json: Optional[typing.Any] = None,
|
|
223
|
+
params: Optional[Any] = None, headers: Optional[Any] = None,
|
|
224
|
+
timeout: Any = USE_CLIENT_DEFAULT, content: Optional[Any] = None) -> httpx.Response:
|
|
225
|
+
|
|
226
|
+
token = TokenContext.get_access_token()
|
|
227
|
+
if token is not None:
|
|
228
|
+
if headers is None: # None is also valid!
|
|
229
|
+
headers = {}
|
|
230
|
+
|
|
231
|
+
## add bearer token
|
|
232
|
+
|
|
233
|
+
headers["Authorization"] = f"Bearer {token}"
|
|
234
|
+
|
|
235
|
+
try:
|
|
236
|
+
response = await self.get_async_client().request(http_method, url, params=params, json=json, headers=headers,
|
|
237
|
+
timeout=timeout, content=content)
|
|
238
|
+
response.raise_for_status()
|
|
239
|
+
except httpx.RequestError as e:
|
|
240
|
+
raise ServiceCommunicationException(str(e)) from e
|
|
241
|
+
|
|
242
|
+
except httpx.HTTPStatusError as e:
|
|
243
|
+
if e.response.status_code == 401:
|
|
244
|
+
www_auth = e.response.headers.get("www-authenticate", "")
|
|
245
|
+
if "invalid_token" in www_auth:
|
|
246
|
+
if 'expired' in www_auth:
|
|
247
|
+
raise TokenExpiredException() from e
|
|
248
|
+
elif 'missing' in www_auth:
|
|
249
|
+
raise MissingTokenException() from e
|
|
250
|
+
else:
|
|
251
|
+
raise InvalidTokenException() from e
|
|
252
|
+
|
|
253
|
+
raise RemoteServiceException(str(e)) from e
|
|
254
|
+
except httpx.HTTPError as e:
|
|
255
|
+
raise RemoteServiceException(str(e)) from e
|
|
256
|
+
|
|
257
|
+
return response
|
|
258
|
+
|
|
128
259
|
class Request(BaseModel):
|
|
129
260
|
method: str # component:service:method
|
|
130
261
|
args: tuple[Any, ...]
|
|
@@ -158,20 +289,25 @@ class DispatchJSONChannel(HTTPXChannel):
|
|
|
158
289
|
|
|
159
290
|
def invoke(self, invocation: DynamicProxy.Invocation):
|
|
160
291
|
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
292
|
|
|
164
|
-
dict =
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
raise RemoteServiceException(f"server side exception {result.exception}")
|
|
293
|
+
request : dict = {
|
|
294
|
+
"method": f"{self.component_descriptor.name}:{service_name}:{invocation.method.__name__}"
|
|
295
|
+
#"args": invocation.args
|
|
296
|
+
}
|
|
169
297
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
298
|
+
if self.optimize_serialization:
|
|
299
|
+
request["args"] = self.serialize_args(invocation)
|
|
300
|
+
else:
|
|
301
|
+
request["args"] = self.to_dict(invocation.args)
|
|
173
302
|
|
|
174
|
-
|
|
303
|
+
try:
|
|
304
|
+
http_result = self.request( "post", f"{self.get_url()}/invoke", json=request, timeout=self.timeout)
|
|
305
|
+
result = http_result.json()
|
|
306
|
+
if result["exception"] is not None:
|
|
307
|
+
raise RemoteServiceException(f"server side exception {result['exception']}")
|
|
308
|
+
|
|
309
|
+
return self.get_deserializer(invocation.type, invocation.method)(result["result"])
|
|
310
|
+
except (ServiceCommunicationException, AuthorizationException, RemoteServiceException) as e:
|
|
175
311
|
raise
|
|
176
312
|
|
|
177
313
|
except Exception as e:
|
|
@@ -180,22 +316,25 @@ class DispatchJSONChannel(HTTPXChannel):
|
|
|
180
316
|
|
|
181
317
|
async def invoke_async(self, invocation: DynamicProxy.Invocation):
|
|
182
318
|
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())
|
|
319
|
+
request : dict = {
|
|
320
|
+
"method": f"{self.component_descriptor.name}:{service_name}:{invocation.method.__name__}"
|
|
321
|
+
}
|
|
189
322
|
|
|
190
|
-
|
|
191
|
-
|
|
323
|
+
if self.optimize_serialization:
|
|
324
|
+
request["args"] = self.serialize_args(invocation)
|
|
325
|
+
else:
|
|
326
|
+
request["args"] = self.to_dict(invocation.args)
|
|
192
327
|
|
|
193
|
-
|
|
328
|
+
try:
|
|
329
|
+
data = await self.request_async("post", f"{self.get_url()}/invoke", json=request, timeout=self.timeout)
|
|
330
|
+
result = data.json()
|
|
194
331
|
|
|
195
|
-
|
|
196
|
-
|
|
332
|
+
if result["exception"] is not None:
|
|
333
|
+
raise RemoteServiceException(f"server side exception {result['exception']}")
|
|
197
334
|
|
|
198
|
-
|
|
335
|
+
return self.get_deserializer(invocation.type, invocation.method)(result["result"])
|
|
336
|
+
|
|
337
|
+
except (ServiceCommunicationException, AuthorizationException, RemoteServiceException) as e:
|
|
199
338
|
raise
|
|
200
339
|
|
|
201
340
|
except Exception as e:
|
|
@@ -221,13 +360,19 @@ class DispatchMSPackChannel(HTTPXChannel):
|
|
|
221
360
|
|
|
222
361
|
def invoke(self, invocation: DynamicProxy.Invocation):
|
|
223
362
|
service_name = self.service_names[invocation.type] # map type to registered service name
|
|
224
|
-
request =
|
|
225
|
-
|
|
363
|
+
request: dict = {
|
|
364
|
+
"method": f"{self.component_descriptor.name}:{service_name}:{invocation.method.__name__}"
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if self.optimize_serialization:
|
|
368
|
+
request["args"] = self.serialize_args(invocation)
|
|
369
|
+
else:
|
|
370
|
+
request["args"] = self.to_dict(invocation.args)
|
|
226
371
|
|
|
227
372
|
try:
|
|
228
|
-
packed = msgpack.packb(
|
|
373
|
+
packed = msgpack.packb(request, use_bin_type=True)
|
|
229
374
|
|
|
230
|
-
response = self.
|
|
375
|
+
response = self.request("post",
|
|
231
376
|
f"{self.get_url()}/invoke",
|
|
232
377
|
content=packed,
|
|
233
378
|
headers={"Content-Type": "application/msgpack"},
|
|
@@ -241,6 +386,25 @@ class DispatchMSPackChannel(HTTPXChannel):
|
|
|
241
386
|
|
|
242
387
|
return self.get_deserializer(invocation.type, invocation.method)(result["result"])
|
|
243
388
|
|
|
389
|
+
|
|
390
|
+
except httpx.RequestError as e:
|
|
391
|
+
raise ServiceCommunicationException(str(e)) from e
|
|
392
|
+
|
|
393
|
+
except httpx.HTTPStatusError as e:
|
|
394
|
+
if e.response.status_code == 401:
|
|
395
|
+
www_auth = e.response.headers.get("www-authenticate", "")
|
|
396
|
+
if "invalid_token" in www_auth:
|
|
397
|
+
if 'expired' in www_auth:
|
|
398
|
+
raise TokenExpiredException() from e
|
|
399
|
+
elif 'missing' in www_auth:
|
|
400
|
+
raise MissingTokenException() from e
|
|
401
|
+
else:
|
|
402
|
+
raise InvalidTokenException() from e
|
|
403
|
+
|
|
404
|
+
raise RemoteServiceException(str(e)) from e
|
|
405
|
+
except httpx.HTTPError as e:
|
|
406
|
+
raise RemoteServiceException(str(e)) from e
|
|
407
|
+
|
|
244
408
|
except ServiceCommunicationException:
|
|
245
409
|
raise
|
|
246
410
|
|
|
@@ -252,13 +416,19 @@ class DispatchMSPackChannel(HTTPXChannel):
|
|
|
252
416
|
|
|
253
417
|
async def invoke_async(self, invocation: DynamicProxy.Invocation):
|
|
254
418
|
service_name = self.service_names[invocation.type] # map type to registered service name
|
|
255
|
-
request =
|
|
256
|
-
|
|
419
|
+
request: dict = {
|
|
420
|
+
"method": f"{self.component_descriptor.name}:{service_name}:{invocation.method.__name__}"
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
if self.optimize_serialization:
|
|
424
|
+
request["args"] = self.serialize_args(invocation)
|
|
425
|
+
else:
|
|
426
|
+
request["args"] = self.to_dict(invocation.args)
|
|
257
427
|
|
|
258
428
|
try:
|
|
259
|
-
packed = msgpack.packb(
|
|
429
|
+
packed = msgpack.packb(request, use_bin_type=True)
|
|
260
430
|
|
|
261
|
-
response = await self.
|
|
431
|
+
response = await self.request_async("post",
|
|
262
432
|
f"{self.get_url()}/invoke",
|
|
263
433
|
content=packed,
|
|
264
434
|
headers={"Content-Type": "application/msgpack"},
|
|
@@ -272,6 +442,25 @@ class DispatchMSPackChannel(HTTPXChannel):
|
|
|
272
442
|
|
|
273
443
|
return self.get_deserializer(invocation.type, invocation.method)(result["result"])
|
|
274
444
|
|
|
445
|
+
except httpx.RequestError as e:
|
|
446
|
+
raise ServiceCommunicationException(str(e)) from e
|
|
447
|
+
|
|
448
|
+
except httpx.HTTPStatusError as e:
|
|
449
|
+
if e.response.status_code == 401:
|
|
450
|
+
www_auth = e.response.headers.get("www-authenticate", "")
|
|
451
|
+
if "invalid_token" in www_auth:
|
|
452
|
+
if 'expired' in www_auth:
|
|
453
|
+
raise TokenExpiredException() from e
|
|
454
|
+
elif 'missing' in www_auth:
|
|
455
|
+
raise MissingTokenException() from e
|
|
456
|
+
else:
|
|
457
|
+
raise InvalidTokenException() from e
|
|
458
|
+
|
|
459
|
+
raise RemoteServiceException(str(e)) from e
|
|
460
|
+
|
|
461
|
+
except httpx.HTTPError as e:
|
|
462
|
+
raise RemoteServiceException(str(e)) from e
|
|
463
|
+
|
|
275
464
|
except ServiceCommunicationException:
|
|
276
465
|
raise
|
|
277
466
|
|
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())
|