aspyx-service 0.11.2__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.
- aspyx_service/__init__.py +106 -0
- aspyx_service/authorization.py +126 -0
- aspyx_service/channels.py +445 -0
- aspyx_service/generator/__init__.py +16 -0
- aspyx_service/generator/json_schema_generator.py +197 -0
- aspyx_service/generator/openapi_generator.py +120 -0
- aspyx_service/healthcheck.py +194 -0
- aspyx_service/protobuf.py +1093 -0
- aspyx_service/registries.py +241 -0
- aspyx_service/restchannel.py +313 -0
- aspyx_service/server.py +576 -0
- aspyx_service/service.py +968 -0
- aspyx_service/session.py +136 -0
- aspyx_service-0.11.2.dist-info/METADATA +555 -0
- aspyx_service-0.11.2.dist-info/RECORD +17 -0
- aspyx_service-0.11.2.dist-info/WHEEL +4 -0
- aspyx_service-0.11.2.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module provides the core Aspyx service management framework allowing for service discovery and transparent remoting including multiple possible transport protocols.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from aspyx.di import module
|
|
6
|
+
|
|
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
|
+
from .registries import ConsulComponentRegistry
|
|
10
|
+
from .server import FastAPIServer, RequestContext, ResponseContext, TokenContextMiddleware
|
|
11
|
+
from .healthcheck import health_checks, health_check, HealthCheckManager, HealthStatus
|
|
12
|
+
from .restchannel import RestChannel, post, get, put, delete, QueryParam, Body, rest
|
|
13
|
+
from .session import Session, SessionManager, SessionContext
|
|
14
|
+
from .authorization import AuthorizationManager, AbstractAuthorizationFactory
|
|
15
|
+
from .protobuf import ProtobufManager
|
|
16
|
+
|
|
17
|
+
@module()
|
|
18
|
+
class ServiceModule:
|
|
19
|
+
def __init__(self):
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
# service
|
|
24
|
+
|
|
25
|
+
"ServiceManager",
|
|
26
|
+
"ServiceModule",
|
|
27
|
+
"ServiceException",
|
|
28
|
+
"Server",
|
|
29
|
+
"Component",
|
|
30
|
+
"Service",
|
|
31
|
+
"Channel",
|
|
32
|
+
"AbstractComponent",
|
|
33
|
+
"ComponentStatus",
|
|
34
|
+
"ComponentDescriptor",
|
|
35
|
+
"ComponentRegistry",
|
|
36
|
+
"ChannelAddress",
|
|
37
|
+
"ChannelInstances",
|
|
38
|
+
"health",
|
|
39
|
+
"component",
|
|
40
|
+
"service",
|
|
41
|
+
"implementation",
|
|
42
|
+
"inject_service",
|
|
43
|
+
"component_services",
|
|
44
|
+
"RemoteServiceException",
|
|
45
|
+
"ServiceCommunicationException",
|
|
46
|
+
"TokenException",
|
|
47
|
+
"TokenExpiredException",
|
|
48
|
+
"InvalidTokenException",
|
|
49
|
+
"MissingTokenException",
|
|
50
|
+
"AuthorizationException",
|
|
51
|
+
|
|
52
|
+
# protobuf
|
|
53
|
+
|
|
54
|
+
"ProtobufManager",
|
|
55
|
+
|
|
56
|
+
# authorization
|
|
57
|
+
|
|
58
|
+
"AuthorizationManager",
|
|
59
|
+
"AbstractAuthorizationFactory",
|
|
60
|
+
|
|
61
|
+
# session
|
|
62
|
+
|
|
63
|
+
"Session",
|
|
64
|
+
"SessionManager",
|
|
65
|
+
"SessionContext",
|
|
66
|
+
|
|
67
|
+
# healthcheck
|
|
68
|
+
|
|
69
|
+
"health_checks",
|
|
70
|
+
"health_check",
|
|
71
|
+
"HealthStatus",
|
|
72
|
+
"HealthCheckManager",
|
|
73
|
+
|
|
74
|
+
# serialization
|
|
75
|
+
|
|
76
|
+
# "deserialize",
|
|
77
|
+
|
|
78
|
+
# channel
|
|
79
|
+
|
|
80
|
+
"HTTPXChannel",
|
|
81
|
+
"DispatchJSONChannel",
|
|
82
|
+
"TokenContext",
|
|
83
|
+
|
|
84
|
+
# rest
|
|
85
|
+
|
|
86
|
+
"RestChannel",
|
|
87
|
+
"post",
|
|
88
|
+
"get",
|
|
89
|
+
"put",
|
|
90
|
+
"delete",
|
|
91
|
+
"rest",
|
|
92
|
+
"QueryParam",
|
|
93
|
+
"Body",
|
|
94
|
+
|
|
95
|
+
# registries
|
|
96
|
+
|
|
97
|
+
"ConsulComponentRegistry",
|
|
98
|
+
|
|
99
|
+
# server
|
|
100
|
+
|
|
101
|
+
"FastAPIServer",
|
|
102
|
+
"RequestContext",
|
|
103
|
+
"ResponseContext",
|
|
104
|
+
"TokenContext",
|
|
105
|
+
"TokenContextMiddleware",
|
|
106
|
+
]
|
|
@@ -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)
|
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Service management and dependency injection framework.
|
|
3
|
+
"""
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import typing
|
|
7
|
+
from contextlib import contextmanager
|
|
8
|
+
from dataclasses import is_dataclass, fields
|
|
9
|
+
from typing import Type, Optional, Any, Callable
|
|
10
|
+
|
|
11
|
+
import httpx
|
|
12
|
+
import msgpack
|
|
13
|
+
from httpx import Client, AsyncClient, USE_CLIENT_DEFAULT
|
|
14
|
+
from pydantic import BaseModel
|
|
15
|
+
|
|
16
|
+
from aspyx.di.configuration import inject_value
|
|
17
|
+
from aspyx.reflection import DynamicProxy, TypeDescriptor
|
|
18
|
+
from aspyx.threading import ThreadLocal, ContextLocal
|
|
19
|
+
from aspyx.util import get_deserializer, TypeDeserializer, TypeSerializer, get_serializer, CopyOnWriteCache
|
|
20
|
+
from .service import ServiceManager, ServiceCommunicationException, TokenExpiredException, InvalidTokenException, \
|
|
21
|
+
AuthorizationException, MissingTokenException
|
|
22
|
+
|
|
23
|
+
from .service import ComponentDescriptor, ChannelInstances, ServiceException, channel, Channel, RemoteServiceException
|
|
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)
|
|
62
|
+
|
|
63
|
+
class HTTPXChannel(Channel):
|
|
64
|
+
"""
|
|
65
|
+
A channel using the httpx clients.
|
|
66
|
+
"""
|
|
67
|
+
__slots__ = [
|
|
68
|
+
"client",
|
|
69
|
+
"async_client",
|
|
70
|
+
"service_names",
|
|
71
|
+
"deserializers",
|
|
72
|
+
"timeout",
|
|
73
|
+
"optimize_serialization"
|
|
74
|
+
]
|
|
75
|
+
|
|
76
|
+
# class properties
|
|
77
|
+
|
|
78
|
+
client_local = ThreadLocal[Client]()
|
|
79
|
+
async_client_local = ThreadLocal[AsyncClient]()
|
|
80
|
+
|
|
81
|
+
# constructor
|
|
82
|
+
|
|
83
|
+
def __init__(self):
|
|
84
|
+
super().__init__()
|
|
85
|
+
|
|
86
|
+
self.timeout = 1000.0
|
|
87
|
+
self.service_names: dict[Type, str] = {}
|
|
88
|
+
self.serializers = CopyOnWriteCache[Callable, list[Callable]]()
|
|
89
|
+
self.deserializers = CopyOnWriteCache[Callable, Callable]()
|
|
90
|
+
|
|
91
|
+
# inject
|
|
92
|
+
|
|
93
|
+
@inject_value("http.timeout", default=1000.0)
|
|
94
|
+
def set_timeout(self, timeout: float) -> None:
|
|
95
|
+
self.timeout = timeout
|
|
96
|
+
|
|
97
|
+
# protected
|
|
98
|
+
|
|
99
|
+
def serialize_args(self, invocation: DynamicProxy.Invocation) -> list[Any]:
|
|
100
|
+
deserializers = self.get_serializers(invocation.type, invocation.method)
|
|
101
|
+
|
|
102
|
+
args = list(invocation.args)
|
|
103
|
+
for index, deserializer in enumerate(deserializers):
|
|
104
|
+
args[index] = deserializer(args[index])
|
|
105
|
+
|
|
106
|
+
return args
|
|
107
|
+
|
|
108
|
+
def get_serializers(self, type: Type, method: Callable) -> list[TypeSerializer]:
|
|
109
|
+
serializers = self.serializers.get(method, None)
|
|
110
|
+
if serializers is None:
|
|
111
|
+
param_types = TypeDescriptor.for_type(type).get_method(method.__name__).param_types
|
|
112
|
+
|
|
113
|
+
serializers = [get_serializer(type) for type in param_types]
|
|
114
|
+
|
|
115
|
+
self.serializers.put(method, serializers)
|
|
116
|
+
|
|
117
|
+
return serializers
|
|
118
|
+
|
|
119
|
+
def get_deserializer(self, type: Type, method: Callable) -> TypeDeserializer:
|
|
120
|
+
deserializer = self.deserializers.get(method, None)
|
|
121
|
+
if deserializer is None:
|
|
122
|
+
return_type = TypeDescriptor.for_type(type).get_method(method.__name__).return_type
|
|
123
|
+
|
|
124
|
+
deserializer = get_deserializer(return_type)
|
|
125
|
+
|
|
126
|
+
self.deserializers.put(method, deserializer)
|
|
127
|
+
|
|
128
|
+
return deserializer
|
|
129
|
+
|
|
130
|
+
# override
|
|
131
|
+
|
|
132
|
+
def setup(self, component_descriptor: ComponentDescriptor, address: ChannelInstances):
|
|
133
|
+
super().setup(component_descriptor, address)
|
|
134
|
+
|
|
135
|
+
# remember service names
|
|
136
|
+
|
|
137
|
+
for service in component_descriptor.services:
|
|
138
|
+
self.service_names[service.type] = service.name
|
|
139
|
+
|
|
140
|
+
# public
|
|
141
|
+
|
|
142
|
+
def get_client(self) -> Client:
|
|
143
|
+
client = self.client_local.get()
|
|
144
|
+
|
|
145
|
+
if client is None:
|
|
146
|
+
client = self.make_client()
|
|
147
|
+
self.client_local.set(client)
|
|
148
|
+
|
|
149
|
+
return client
|
|
150
|
+
|
|
151
|
+
def get_async_client(self) -> AsyncClient:
|
|
152
|
+
async_client = self.async_client_local.get()
|
|
153
|
+
|
|
154
|
+
if async_client is None:
|
|
155
|
+
async_client = self.make_async_client()
|
|
156
|
+
self.async_client_local.set(async_client)
|
|
157
|
+
|
|
158
|
+
return async_client
|
|
159
|
+
|
|
160
|
+
def make_client(self) -> Client:
|
|
161
|
+
return Client() # base_url=url
|
|
162
|
+
|
|
163
|
+
def make_async_client(self) -> AsyncClient:
|
|
164
|
+
return AsyncClient() # base_url=url
|
|
165
|
+
|
|
166
|
+
def request(self, http_method: str, url: str, json: Optional[typing.Any] = None,
|
|
167
|
+
params: Optional[Any] = None, headers: Optional[Any] = None,
|
|
168
|
+
timeout: Any = USE_CLIENT_DEFAULT, content: Optional[Any] = None) -> httpx.Response:
|
|
169
|
+
|
|
170
|
+
token = TokenContext.get_access_token()
|
|
171
|
+
if token is not None:
|
|
172
|
+
if headers is None: # None is also valid!
|
|
173
|
+
headers = {}
|
|
174
|
+
|
|
175
|
+
## add bearer token
|
|
176
|
+
|
|
177
|
+
headers["Authorization"] = f"Bearer {token}"
|
|
178
|
+
|
|
179
|
+
try:
|
|
180
|
+
response = self.get_client().request(http_method, url, params=params, json=json, headers=headers, timeout=timeout, content=content)
|
|
181
|
+
|
|
182
|
+
#print("\n=== Response ===")
|
|
183
|
+
#print("Status Code:", response.status_code)
|
|
184
|
+
#try:
|
|
185
|
+
# print("Body:", json.dumps(response.json(), indent=2))
|
|
186
|
+
#except Exception:
|
|
187
|
+
# print("Body (raw):", response.text)
|
|
188
|
+
|
|
189
|
+
response.raise_for_status()
|
|
190
|
+
except httpx.RequestError as e:
|
|
191
|
+
raise ServiceCommunicationException(str(e)) from e
|
|
192
|
+
|
|
193
|
+
except httpx.HTTPStatusError as e:
|
|
194
|
+
if e.response.status_code == 401:
|
|
195
|
+
www_auth = e.response.headers.get("www-authenticate", "")
|
|
196
|
+
if "invalid_token" in www_auth:
|
|
197
|
+
if 'expired' in www_auth:
|
|
198
|
+
raise TokenExpiredException() from e
|
|
199
|
+
|
|
200
|
+
if 'missing' in www_auth:
|
|
201
|
+
raise MissingTokenException() from e
|
|
202
|
+
|
|
203
|
+
raise InvalidTokenException() from e
|
|
204
|
+
|
|
205
|
+
raise AuthorizationException(str(e)) from e
|
|
206
|
+
except httpx.HTTPError as e:
|
|
207
|
+
raise RemoteServiceException(str(e)) from e
|
|
208
|
+
|
|
209
|
+
return response
|
|
210
|
+
|
|
211
|
+
async def request_async(self, http_method: str, url: str, json: Optional[typing.Any] = None,
|
|
212
|
+
params: Optional[Any] = None, headers: Optional[Any] = None,
|
|
213
|
+
timeout: Any = USE_CLIENT_DEFAULT, content: Optional[Any] = None) -> httpx.Response:
|
|
214
|
+
|
|
215
|
+
token = TokenContext.get_access_token()
|
|
216
|
+
if token is not None:
|
|
217
|
+
if headers is None: # None is also valid!
|
|
218
|
+
headers = {}
|
|
219
|
+
|
|
220
|
+
## add bearer token
|
|
221
|
+
|
|
222
|
+
headers["Authorization"] = f"Bearer {token}"
|
|
223
|
+
|
|
224
|
+
try:
|
|
225
|
+
response = await self.get_async_client().request(http_method, url, params=params, json=json, headers=headers,
|
|
226
|
+
timeout=timeout, content=content)
|
|
227
|
+
response.raise_for_status()
|
|
228
|
+
except httpx.RequestError as e:
|
|
229
|
+
raise ServiceCommunicationException(str(e)) from e
|
|
230
|
+
|
|
231
|
+
except httpx.HTTPStatusError as e:
|
|
232
|
+
if e.response.status_code == 401:
|
|
233
|
+
www_auth = e.response.headers.get("www-authenticate", "")
|
|
234
|
+
if "invalid_token" in www_auth:
|
|
235
|
+
if 'expired' in www_auth:
|
|
236
|
+
raise TokenExpiredException() from e
|
|
237
|
+
elif 'missing' in www_auth:
|
|
238
|
+
raise MissingTokenException() from e
|
|
239
|
+
else:
|
|
240
|
+
raise InvalidTokenException() from e
|
|
241
|
+
|
|
242
|
+
raise RemoteServiceException(str(e)) from e
|
|
243
|
+
except httpx.HTTPError as e:
|
|
244
|
+
raise RemoteServiceException(str(e)) from e
|
|
245
|
+
|
|
246
|
+
return response
|
|
247
|
+
|
|
248
|
+
class Request(BaseModel):
|
|
249
|
+
method: str # component:service:method
|
|
250
|
+
args: tuple[Any, ...]
|
|
251
|
+
|
|
252
|
+
class Response(BaseModel):
|
|
253
|
+
result: Optional[Any]
|
|
254
|
+
exception: Optional[Any]
|
|
255
|
+
|
|
256
|
+
@channel("dispatch-json")
|
|
257
|
+
class DispatchJSONChannel(HTTPXChannel):
|
|
258
|
+
"""
|
|
259
|
+
A channel that calls a POST on the endpoint `ìnvoke` sending a request body containing information on the
|
|
260
|
+
called component, service and method and the arguments.
|
|
261
|
+
"""
|
|
262
|
+
# constructor
|
|
263
|
+
|
|
264
|
+
def __init__(self):
|
|
265
|
+
super().__init__()
|
|
266
|
+
|
|
267
|
+
# internal
|
|
268
|
+
|
|
269
|
+
# implement Channel
|
|
270
|
+
|
|
271
|
+
def set_address(self, address: Optional[ChannelInstances]):
|
|
272
|
+
ServiceManager.logger.info("channel %s got an address %s", self.name, address)
|
|
273
|
+
|
|
274
|
+
super().set_address(address)
|
|
275
|
+
|
|
276
|
+
def setup(self, component_descriptor: ComponentDescriptor, address: ChannelInstances):
|
|
277
|
+
super().setup(component_descriptor, address)
|
|
278
|
+
|
|
279
|
+
def invoke(self, invocation: DynamicProxy.Invocation):
|
|
280
|
+
service_name = self.service_names[invocation.type] # map type to registered service name
|
|
281
|
+
|
|
282
|
+
request = {
|
|
283
|
+
"method": f"{self.component_descriptor.name}:{service_name}:{invocation.method.__name__}",
|
|
284
|
+
"args": self.serialize_args(invocation)
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
try:
|
|
288
|
+
http_result = self.request( "post", f"{self.get_url()}/invoke", json=request, timeout=self.timeout)
|
|
289
|
+
result = http_result.json()
|
|
290
|
+
if result["exception"] is not None:
|
|
291
|
+
raise RemoteServiceException(f"server side exception {result['exception']}")
|
|
292
|
+
|
|
293
|
+
return self.get_deserializer(invocation.type, invocation.method)(result["result"])
|
|
294
|
+
except (ServiceCommunicationException, AuthorizationException, RemoteServiceException) as e:
|
|
295
|
+
raise
|
|
296
|
+
|
|
297
|
+
except Exception as e:
|
|
298
|
+
raise ServiceCommunicationException(f"communication exception {e}") from e
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
async def invoke_async(self, invocation: DynamicProxy.Invocation):
|
|
302
|
+
service_name = self.service_names[invocation.type] # map type to registered service name
|
|
303
|
+
request = {
|
|
304
|
+
"method": f"{self.component_descriptor.name}:{service_name}:{invocation.method.__name__}",
|
|
305
|
+
"args": self.serialize_args(invocation)
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
try:
|
|
309
|
+
data = await self.request_async("post", f"{self.get_url()}/invoke", json=request, timeout=self.timeout)
|
|
310
|
+
result = data.json()
|
|
311
|
+
|
|
312
|
+
if result["exception"] is not None:
|
|
313
|
+
raise RemoteServiceException(f"server side exception {result['exception']}")
|
|
314
|
+
|
|
315
|
+
return self.get_deserializer(invocation.type, invocation.method)(result["result"])
|
|
316
|
+
|
|
317
|
+
except (ServiceCommunicationException, AuthorizationException, RemoteServiceException) as e:
|
|
318
|
+
raise
|
|
319
|
+
|
|
320
|
+
except Exception as e:
|
|
321
|
+
raise ServiceCommunicationException(f"communication exception {e}") from e
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
@channel("dispatch-msgpack")
|
|
325
|
+
class DispatchMSPackChannel(HTTPXChannel):
|
|
326
|
+
"""
|
|
327
|
+
A channel that sends a POST on the ìnvoke `endpoint`with an msgpack encoded request body.
|
|
328
|
+
"""
|
|
329
|
+
# constructor
|
|
330
|
+
|
|
331
|
+
def __init__(self):
|
|
332
|
+
super().__init__()
|
|
333
|
+
|
|
334
|
+
# override
|
|
335
|
+
|
|
336
|
+
def set_address(self, address: Optional[ChannelInstances]):
|
|
337
|
+
ServiceManager.logger.info("channel %s got an address %s", self.name, address)
|
|
338
|
+
|
|
339
|
+
super().set_address(address)
|
|
340
|
+
|
|
341
|
+
def invoke(self, invocation: DynamicProxy.Invocation):
|
|
342
|
+
service_name = self.service_names[invocation.type] # map type to registered service name
|
|
343
|
+
request = {
|
|
344
|
+
"method": f"{self.component_descriptor.name}:{service_name}:{invocation.method.__name__}",
|
|
345
|
+
"args": self.serialize_args(invocation)
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
try:
|
|
349
|
+
packed = msgpack.packb(request, use_bin_type=True)
|
|
350
|
+
|
|
351
|
+
response = self.request("post",
|
|
352
|
+
f"{self.get_url()}/invoke",
|
|
353
|
+
content=packed,
|
|
354
|
+
headers={"Content-Type": "application/msgpack"},
|
|
355
|
+
timeout=self.timeout
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
result = msgpack.unpackb(response.content, raw=False)
|
|
359
|
+
|
|
360
|
+
if result.get("exception", None):
|
|
361
|
+
raise RemoteServiceException(f"server-side: {result['exception']}")
|
|
362
|
+
|
|
363
|
+
return self.get_deserializer(invocation.type, invocation.method)(result["result"])
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
except httpx.RequestError as e:
|
|
367
|
+
raise ServiceCommunicationException(str(e)) from e
|
|
368
|
+
|
|
369
|
+
except httpx.HTTPStatusError as e:
|
|
370
|
+
if e.response.status_code == 401:
|
|
371
|
+
www_auth = e.response.headers.get("www-authenticate", "")
|
|
372
|
+
if "invalid_token" in www_auth:
|
|
373
|
+
if 'expired' in www_auth:
|
|
374
|
+
raise TokenExpiredException() from e
|
|
375
|
+
|
|
376
|
+
if 'missing' in www_auth:
|
|
377
|
+
raise MissingTokenException() from e
|
|
378
|
+
|
|
379
|
+
raise InvalidTokenException() from e
|
|
380
|
+
|
|
381
|
+
raise RemoteServiceException(str(e)) from e
|
|
382
|
+
except httpx.HTTPError as e:
|
|
383
|
+
raise RemoteServiceException(str(e)) from e
|
|
384
|
+
|
|
385
|
+
except ServiceCommunicationException:
|
|
386
|
+
raise
|
|
387
|
+
|
|
388
|
+
except RemoteServiceException:
|
|
389
|
+
raise
|
|
390
|
+
|
|
391
|
+
except Exception as e:
|
|
392
|
+
raise ServiceException(f"msgpack exception: {e}") from e
|
|
393
|
+
|
|
394
|
+
async def invoke_async(self, invocation: DynamicProxy.Invocation):
|
|
395
|
+
service_name = self.service_names[invocation.type] # map type to registered service name
|
|
396
|
+
request = {
|
|
397
|
+
"method": f"{self.component_descriptor.name}:{service_name}:{invocation.method.__name__}",
|
|
398
|
+
"args": self.serialize_args(invocation)
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
try:
|
|
402
|
+
packed = msgpack.packb(request, use_bin_type=True)
|
|
403
|
+
|
|
404
|
+
response = await self.request_async("post",
|
|
405
|
+
f"{self.get_url()}/invoke",
|
|
406
|
+
content=packed,
|
|
407
|
+
headers={"Content-Type": "application/msgpack"},
|
|
408
|
+
timeout=self.timeout
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
result = msgpack.unpackb(response.content, raw=False)
|
|
412
|
+
|
|
413
|
+
if result.get("exception", None):
|
|
414
|
+
raise RemoteServiceException(f"server-side: {result['exception']}")
|
|
415
|
+
|
|
416
|
+
return self.get_deserializer(invocation.type, invocation.method)(result["result"])
|
|
417
|
+
|
|
418
|
+
except httpx.RequestError as e:
|
|
419
|
+
raise ServiceCommunicationException(str(e)) from e
|
|
420
|
+
|
|
421
|
+
except httpx.HTTPStatusError as e:
|
|
422
|
+
if e.response.status_code == 401:
|
|
423
|
+
www_auth = e.response.headers.get("www-authenticate", "")
|
|
424
|
+
if "invalid_token" in www_auth:
|
|
425
|
+
if 'expired' in www_auth:
|
|
426
|
+
raise TokenExpiredException() from e
|
|
427
|
+
|
|
428
|
+
if 'missing' in www_auth:
|
|
429
|
+
raise MissingTokenException() from e
|
|
430
|
+
|
|
431
|
+
raise InvalidTokenException() from e
|
|
432
|
+
|
|
433
|
+
raise RemoteServiceException(str(e)) from e
|
|
434
|
+
|
|
435
|
+
except httpx.HTTPError as e:
|
|
436
|
+
raise RemoteServiceException(str(e)) from e
|
|
437
|
+
|
|
438
|
+
except ServiceCommunicationException:
|
|
439
|
+
raise
|
|
440
|
+
|
|
441
|
+
except RemoteServiceException:
|
|
442
|
+
raise
|
|
443
|
+
|
|
444
|
+
except Exception as e:
|
|
445
|
+
raise ServiceException(f"msgpack exception: {e}") from e
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module provides the core Aspyx event management framework .
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from .json_schema_generator import JSONSchemaGenerator
|
|
6
|
+
from .openapi_generator import OpenAPIGenerator
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
# json_schema_generator
|
|
10
|
+
|
|
11
|
+
"JSONSchemaGenerator",
|
|
12
|
+
|
|
13
|
+
# openapi_generator
|
|
14
|
+
|
|
15
|
+
"OpenAPIGenerator"
|
|
16
|
+
]
|