aspyx-service 0.10.2__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 +251 -57
- aspyx_service/healthcheck.py +1 -1
- aspyx_service/registries.py +5 -5
- aspyx_service/restchannel.py +17 -21
- aspyx_service/serialization.py +6 -3
- aspyx_service/server.py +139 -69
- aspyx_service/service.py +55 -19
- aspyx_service/session.py +97 -0
- {aspyx_service-0.10.2.dist-info → aspyx_service-0.10.4.dist-info}/METADATA +41 -10
- aspyx_service-0.10.4.dist-info/RECORD +14 -0
- aspyx_service-0.10.2.dist-info/RECORD +0 -12
- {aspyx_service-0.10.2.dist-info → aspyx_service-0.10.4.dist-info}/WHEEL +0 -0
- {aspyx_service-0.10.2.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,21 +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 .
|
|
18
|
+
from aspyx.threading import ThreadLocal, ContextLocal
|
|
19
|
+
from .service import ServiceManager, ServiceCommunicationException, TokenExpiredException, InvalidTokenException, \
|
|
20
|
+
AuthorizationException, MissingTokenException
|
|
17
21
|
|
|
18
22
|
from .service import ComponentDescriptor, ChannelInstances, ServiceException, channel, Channel, RemoteServiceException
|
|
19
|
-
from .serialization import get_deserializer
|
|
23
|
+
from .serialization import get_deserializer, TypeDeserializer, TypeSerializer, get_serializer
|
|
20
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)
|
|
21
62
|
|
|
22
63
|
class HTTPXChannel(Channel):
|
|
23
64
|
__slots__ = [
|
|
@@ -25,16 +66,21 @@ class HTTPXChannel(Channel):
|
|
|
25
66
|
"async_client",
|
|
26
67
|
"service_names",
|
|
27
68
|
"deserializers",
|
|
28
|
-
"timeout"
|
|
69
|
+
"timeout",
|
|
70
|
+
"optimize_serialization"
|
|
29
71
|
]
|
|
30
72
|
|
|
73
|
+
# class properties
|
|
74
|
+
|
|
75
|
+
client_local = ThreadLocal[Client]()
|
|
76
|
+
async_client_local = ThreadLocal[AsyncClient]()
|
|
77
|
+
|
|
31
78
|
# class methods
|
|
32
79
|
|
|
33
80
|
@classmethod
|
|
34
81
|
def to_dict(cls, obj: Any) -> Any:
|
|
35
82
|
if isinstance(obj, BaseModel):
|
|
36
|
-
return obj.
|
|
37
|
-
|
|
83
|
+
return obj.model_dump()
|
|
38
84
|
|
|
39
85
|
elif is_dataclass(obj):
|
|
40
86
|
return {
|
|
@@ -51,20 +97,16 @@ class HTTPXChannel(Channel):
|
|
|
51
97
|
|
|
52
98
|
return obj
|
|
53
99
|
|
|
54
|
-
@classmethod
|
|
55
|
-
def to_json(cls, obj) -> str:
|
|
56
|
-
return json.dumps(cls.to_dict(obj))
|
|
57
|
-
|
|
58
100
|
# constructor
|
|
59
101
|
|
|
60
102
|
def __init__(self):
|
|
61
103
|
super().__init__()
|
|
62
104
|
|
|
63
105
|
self.timeout = 1000.0
|
|
64
|
-
self.client: Optional[Client] = None
|
|
65
|
-
self.async_client: Optional[AsyncClient] = None
|
|
66
106
|
self.service_names: dict[Type, str] = {}
|
|
107
|
+
self.serializers: dict[Callable, list[Callable]] = {}
|
|
67
108
|
self.deserializers: dict[Callable, Callable] = {}
|
|
109
|
+
self.optimize_serialization = True
|
|
68
110
|
|
|
69
111
|
# inject
|
|
70
112
|
|
|
@@ -74,7 +116,27 @@ class HTTPXChannel(Channel):
|
|
|
74
116
|
|
|
75
117
|
# protected
|
|
76
118
|
|
|
77
|
-
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:
|
|
78
140
|
deserializer = self.deserializers.get(method, None)
|
|
79
141
|
if deserializer is None:
|
|
80
142
|
return_type = TypeDescriptor.for_type(type).get_method(method.__name__).return_type
|
|
@@ -95,24 +157,25 @@ class HTTPXChannel(Channel):
|
|
|
95
157
|
for service in component_descriptor.services:
|
|
96
158
|
self.service_names[service.type] = service.name
|
|
97
159
|
|
|
98
|
-
# make client
|
|
99
|
-
|
|
100
|
-
self.client = self.make_client()
|
|
101
|
-
self.async_client = self.make_async_client()
|
|
102
|
-
|
|
103
160
|
# public
|
|
104
161
|
|
|
105
162
|
def get_client(self) -> Client:
|
|
106
|
-
|
|
107
|
-
|
|
163
|
+
client = self.client_local.get()
|
|
164
|
+
|
|
165
|
+
if client is None:
|
|
166
|
+
client = self.make_client()
|
|
167
|
+
self.client_local.set(client)
|
|
108
168
|
|
|
109
|
-
return
|
|
169
|
+
return client
|
|
110
170
|
|
|
111
171
|
def get_async_client(self) -> AsyncClient:
|
|
112
|
-
|
|
113
|
-
self.async_client = self.make_async_client()
|
|
172
|
+
async_client = self.async_client_local.get()
|
|
114
173
|
|
|
115
|
-
|
|
174
|
+
if async_client is None:
|
|
175
|
+
async_client = self.make_async_client()
|
|
176
|
+
self.async_client_local.set(async_client)
|
|
177
|
+
|
|
178
|
+
return async_client
|
|
116
179
|
|
|
117
180
|
def make_client(self) -> Client:
|
|
118
181
|
return Client() # base_url=url
|
|
@@ -120,6 +183,79 @@ class HTTPXChannel(Channel):
|
|
|
120
183
|
def make_async_client(self) -> AsyncClient:
|
|
121
184
|
return AsyncClient() # base_url=url
|
|
122
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
|
+
|
|
123
259
|
class Request(BaseModel):
|
|
124
260
|
method: str # component:service:method
|
|
125
261
|
args: tuple[Any, ...]
|
|
@@ -153,20 +289,25 @@ class DispatchJSONChannel(HTTPXChannel):
|
|
|
153
289
|
|
|
154
290
|
def invoke(self, invocation: DynamicProxy.Invocation):
|
|
155
291
|
service_name = self.service_names[invocation.type] # map type to registered service name
|
|
156
|
-
request = Request(method=f"{self.component_descriptor.name}:{service_name}:{invocation.method.__name__}",
|
|
157
|
-
args=invocation.args)
|
|
158
292
|
|
|
159
|
-
dict =
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
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
|
+
}
|
|
164
297
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
298
|
+
if self.optimize_serialization:
|
|
299
|
+
request["args"] = self.serialize_args(invocation)
|
|
300
|
+
else:
|
|
301
|
+
request["args"] = self.to_dict(invocation.args)
|
|
168
302
|
|
|
169
|
-
|
|
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:
|
|
170
311
|
raise
|
|
171
312
|
|
|
172
313
|
except Exception as e:
|
|
@@ -175,22 +316,25 @@ class DispatchJSONChannel(HTTPXChannel):
|
|
|
175
316
|
|
|
176
317
|
async def invoke_async(self, invocation: DynamicProxy.Invocation):
|
|
177
318
|
service_name = self.service_names[invocation.type] # map type to registered service name
|
|
178
|
-
request =
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
try:
|
|
182
|
-
data = await self.get_async_client().post(f"{self.get_url()}/invoke", json=dict, timeout=self.timeout)
|
|
183
|
-
result = Response(**data.json())
|
|
319
|
+
request : dict = {
|
|
320
|
+
"method": f"{self.component_descriptor.name}:{service_name}:{invocation.method.__name__}"
|
|
321
|
+
}
|
|
184
322
|
|
|
185
|
-
|
|
186
|
-
|
|
323
|
+
if self.optimize_serialization:
|
|
324
|
+
request["args"] = self.serialize_args(invocation)
|
|
325
|
+
else:
|
|
326
|
+
request["args"] = self.to_dict(invocation.args)
|
|
187
327
|
|
|
188
|
-
|
|
328
|
+
try:
|
|
329
|
+
data = await self.request_async("post", f"{self.get_url()}/invoke", json=request, timeout=self.timeout)
|
|
330
|
+
result = data.json()
|
|
189
331
|
|
|
190
|
-
|
|
191
|
-
|
|
332
|
+
if result["exception"] is not None:
|
|
333
|
+
raise RemoteServiceException(f"server side exception {result['exception']}")
|
|
192
334
|
|
|
193
|
-
|
|
335
|
+
return self.get_deserializer(invocation.type, invocation.method)(result["result"])
|
|
336
|
+
|
|
337
|
+
except (ServiceCommunicationException, AuthorizationException, RemoteServiceException) as e:
|
|
194
338
|
raise
|
|
195
339
|
|
|
196
340
|
except Exception as e:
|
|
@@ -216,13 +360,19 @@ class DispatchMSPackChannel(HTTPXChannel):
|
|
|
216
360
|
|
|
217
361
|
def invoke(self, invocation: DynamicProxy.Invocation):
|
|
218
362
|
service_name = self.service_names[invocation.type] # map type to registered service name
|
|
219
|
-
request =
|
|
220
|
-
|
|
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)
|
|
221
371
|
|
|
222
372
|
try:
|
|
223
|
-
packed = msgpack.packb(
|
|
373
|
+
packed = msgpack.packb(request, use_bin_type=True)
|
|
224
374
|
|
|
225
|
-
response = self.
|
|
375
|
+
response = self.request("post",
|
|
226
376
|
f"{self.get_url()}/invoke",
|
|
227
377
|
content=packed,
|
|
228
378
|
headers={"Content-Type": "application/msgpack"},
|
|
@@ -236,6 +386,25 @@ class DispatchMSPackChannel(HTTPXChannel):
|
|
|
236
386
|
|
|
237
387
|
return self.get_deserializer(invocation.type, invocation.method)(result["result"])
|
|
238
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
|
+
|
|
239
408
|
except ServiceCommunicationException:
|
|
240
409
|
raise
|
|
241
410
|
|
|
@@ -247,13 +416,19 @@ class DispatchMSPackChannel(HTTPXChannel):
|
|
|
247
416
|
|
|
248
417
|
async def invoke_async(self, invocation: DynamicProxy.Invocation):
|
|
249
418
|
service_name = self.service_names[invocation.type] # map type to registered service name
|
|
250
|
-
request =
|
|
251
|
-
|
|
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)
|
|
252
427
|
|
|
253
428
|
try:
|
|
254
|
-
packed = msgpack.packb(
|
|
429
|
+
packed = msgpack.packb(request, use_bin_type=True)
|
|
255
430
|
|
|
256
|
-
response = await self.
|
|
431
|
+
response = await self.request_async("post",
|
|
257
432
|
f"{self.get_url()}/invoke",
|
|
258
433
|
content=packed,
|
|
259
434
|
headers={"Content-Type": "application/msgpack"},
|
|
@@ -267,6 +442,25 @@ class DispatchMSPackChannel(HTTPXChannel):
|
|
|
267
442
|
|
|
268
443
|
return self.get_deserializer(invocation.type, invocation.method)(result["result"])
|
|
269
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
|
+
|
|
270
464
|
except ServiceCommunicationException:
|
|
271
465
|
raise
|
|
272
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())
|