aspyx-service 0.10.1__tar.gz → 0.10.2__tar.gz
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-0.10.1 → aspyx_service-0.10.2}/PKG-INFO +14 -2
- {aspyx_service-0.10.1 → aspyx_service-0.10.2}/README.md +12 -0
- {aspyx_service-0.10.1 → aspyx_service-0.10.2}/pyproject.toml +2 -2
- {aspyx_service-0.10.1 → aspyx_service-0.10.2}/src/aspyx_service/channels.py +60 -30
- {aspyx_service-0.10.1 → aspyx_service-0.10.2}/src/aspyx_service/registries.py +25 -12
- {aspyx_service-0.10.1 → aspyx_service-0.10.2}/src/aspyx_service/restchannel.py +90 -17
- {aspyx_service-0.10.1 → aspyx_service-0.10.2}/src/aspyx_service/serialization.py +61 -61
- {aspyx_service-0.10.1 → aspyx_service-0.10.2}/src/aspyx_service/server.py +5 -8
- {aspyx_service-0.10.1 → aspyx_service-0.10.2}/src/aspyx_service/service.py +23 -6
- aspyx_service-0.10.2/tests/__init__.py +1 -0
- aspyx_service-0.10.2/tests/common.py +300 -0
- aspyx_service-0.10.2/tests/config.yaml +13 -0
- aspyx_service-0.10.2/tests/test_async_service.py +58 -0
- aspyx_service-0.10.2/tests/test_service.py +82 -0
- aspyx_service-0.10.1/tests/config.yaml +0 -4
- aspyx_service-0.10.1/tests/test-service.py +0 -423
- {aspyx_service-0.10.1 → aspyx_service-0.10.2}/.gitignore +0 -0
- {aspyx_service-0.10.1 → aspyx_service-0.10.2}/LICENSE +0 -0
- {aspyx_service-0.10.1 → aspyx_service-0.10.2}/src/aspyx_service/__init__.py +0 -0
- {aspyx_service-0.10.1 → aspyx_service-0.10.2}/src/aspyx_service/healthcheck.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: aspyx_service
|
|
3
|
-
Version: 0.10.
|
|
3
|
+
Version: 0.10.2
|
|
4
4
|
Summary: Aspyx Service framework
|
|
5
5
|
Author-email: Andreas Ernst <andreas.ernst7@gmail.com>
|
|
6
6
|
License: MIT License
|
|
@@ -26,7 +26,7 @@ License: MIT License
|
|
|
26
26
|
SOFTWARE.
|
|
27
27
|
License-File: LICENSE
|
|
28
28
|
Requires-Python: >=3.9
|
|
29
|
-
Requires-Dist: aspyx>=1.5.
|
|
29
|
+
Requires-Dist: aspyx>=1.5.1
|
|
30
30
|
Requires-Dist: fastapi~=0.115.13
|
|
31
31
|
Requires-Dist: httpx~=0.28.1
|
|
32
32
|
Requires-Dist: msgpack~=1.1.1
|
|
@@ -34,6 +34,18 @@ Requires-Dist: python-consul2~=0.1.5
|
|
|
34
34
|
Requires-Dist: uvicorn[standard]
|
|
35
35
|
Description-Content-Type: text/markdown
|
|
36
36
|
|
|
37
|
+
# aspyx
|
|
38
|
+
|
|
39
|
+

|
|
40
|
+

|
|
41
|
+

|
|
42
|
+

|
|
43
|
+

|
|
44
|
+
[](https://pypi.org/project/aspyx/)
|
|
45
|
+
[](https://coolsamson7.github.io/aspyx/index/introduction)
|
|
46
|
+
|
|
47
|
+

|
|
48
|
+
|
|
37
49
|
# Service
|
|
38
50
|
|
|
39
51
|
- [Introduction](#introduction)
|
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
# aspyx
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+

|
|
5
|
+

|
|
6
|
+

|
|
7
|
+

|
|
8
|
+
[](https://pypi.org/project/aspyx/)
|
|
9
|
+
[](https://coolsamson7.github.io/aspyx/index/introduction)
|
|
10
|
+
|
|
11
|
+

|
|
12
|
+
|
|
1
13
|
# Service
|
|
2
14
|
|
|
3
15
|
- [Introduction](#introduction)
|
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
[project]
|
|
4
4
|
name = "aspyx_service"
|
|
5
|
-
version = "0.10.
|
|
5
|
+
version = "0.10.2"
|
|
6
6
|
description = "Aspyx Service framework"
|
|
7
7
|
authors = [{ name = "Andreas Ernst", email = "andreas.ernst7@gmail.com" }]
|
|
8
8
|
readme = "README.md"
|
|
9
9
|
license = { file = "LICENSE" }
|
|
10
10
|
requires-python = ">=3.9"
|
|
11
11
|
dependencies = [
|
|
12
|
-
"aspyx>=1.5.
|
|
12
|
+
"aspyx>=1.5.1",
|
|
13
13
|
"python-consul2~=0.1.5",
|
|
14
14
|
"fastapi~=0.115.13",
|
|
15
15
|
"httpx~=0.28.1",
|
|
@@ -11,8 +11,9 @@ import msgpack
|
|
|
11
11
|
from httpx import Client, AsyncClient
|
|
12
12
|
from pydantic import BaseModel
|
|
13
13
|
|
|
14
|
+
from aspyx.di.configuration import inject_value
|
|
14
15
|
from aspyx.reflection import DynamicProxy, TypeDescriptor
|
|
15
|
-
from .service import ServiceManager
|
|
16
|
+
from .service import ServiceManager, ServiceCommunicationException
|
|
16
17
|
|
|
17
18
|
from .service import ComponentDescriptor, ChannelInstances, ServiceException, channel, Channel, RemoteServiceException
|
|
18
19
|
from .serialization import get_deserializer
|
|
@@ -23,7 +24,8 @@ class HTTPXChannel(Channel):
|
|
|
23
24
|
"client",
|
|
24
25
|
"async_client",
|
|
25
26
|
"service_names",
|
|
26
|
-
"deserializers"
|
|
27
|
+
"deserializers",
|
|
28
|
+
"timeout"
|
|
27
29
|
]
|
|
28
30
|
|
|
29
31
|
# class methods
|
|
@@ -58,11 +60,18 @@ class HTTPXChannel(Channel):
|
|
|
58
60
|
def __init__(self):
|
|
59
61
|
super().__init__()
|
|
60
62
|
|
|
63
|
+
self.timeout = 1000.0
|
|
61
64
|
self.client: Optional[Client] = None
|
|
62
65
|
self.async_client: Optional[AsyncClient] = None
|
|
63
66
|
self.service_names: dict[Type, str] = {}
|
|
64
67
|
self.deserializers: dict[Callable, Callable] = {}
|
|
65
68
|
|
|
69
|
+
# inject
|
|
70
|
+
|
|
71
|
+
@inject_value("http.timeout", default=1000.0)
|
|
72
|
+
def set_timeout(self, timeout: float) -> None:
|
|
73
|
+
self.timeout = timeout
|
|
74
|
+
|
|
66
75
|
# protected
|
|
67
76
|
|
|
68
77
|
def get_deserializer(self, type: Type, method: Callable) -> Type:
|
|
@@ -99,16 +108,16 @@ class HTTPXChannel(Channel):
|
|
|
99
108
|
|
|
100
109
|
return self.client
|
|
101
110
|
|
|
102
|
-
def get_async_client(self) ->
|
|
111
|
+
def get_async_client(self) -> AsyncClient:
|
|
103
112
|
if self.async_client is None:
|
|
104
113
|
self.async_client = self.make_async_client()
|
|
105
114
|
|
|
106
115
|
return self.async_client
|
|
107
116
|
|
|
108
|
-
def make_client(self):
|
|
117
|
+
def make_client(self) -> Client:
|
|
109
118
|
return Client() # base_url=url
|
|
110
119
|
|
|
111
|
-
def make_async_client(self):
|
|
120
|
+
def make_async_client(self) -> AsyncClient:
|
|
112
121
|
return AsyncClient() # base_url=url
|
|
113
122
|
|
|
114
123
|
class Request(BaseModel):
|
|
@@ -122,7 +131,7 @@ class Response(BaseModel):
|
|
|
122
131
|
@channel("dispatch-json")
|
|
123
132
|
class DispatchJSONChannel(HTTPXChannel):
|
|
124
133
|
"""
|
|
125
|
-
A channel that calls a POST on
|
|
134
|
+
A channel that calls a POST on the endpoint `ìnvoke` sending a request body containing information on the
|
|
126
135
|
called component, service and method and the arguments.
|
|
127
136
|
"""
|
|
128
137
|
# constructor
|
|
@@ -144,40 +153,49 @@ class DispatchJSONChannel(HTTPXChannel):
|
|
|
144
153
|
|
|
145
154
|
def invoke(self, invocation: DynamicProxy.Invocation):
|
|
146
155
|
service_name = self.service_names[invocation.type] # map type to registered service name
|
|
147
|
-
request = Request(method=f"{self.component_descriptor.name}:{service_name}:{invocation.method.__name__}",
|
|
156
|
+
request = Request(method=f"{self.component_descriptor.name}:{service_name}:{invocation.method.__name__}",
|
|
157
|
+
args=invocation.args)
|
|
148
158
|
|
|
149
159
|
dict = self.to_dict(request)
|
|
150
160
|
try:
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
161
|
+
result = Response(**self.get_client().post(f"{self.get_url()}/invoke", json=dict, timeout=self.timeout).json())
|
|
162
|
+
if result.exception is not None:
|
|
163
|
+
raise RemoteServiceException(f"server side exception {result.exception}")
|
|
164
|
+
|
|
165
|
+
return self.get_deserializer(invocation.type, invocation.method)(result.result)
|
|
166
|
+
except ServiceCommunicationException:
|
|
167
|
+
raise
|
|
168
|
+
|
|
169
|
+
except RemoteServiceException:
|
|
170
|
+
raise
|
|
171
|
+
|
|
159
172
|
except Exception as e:
|
|
160
|
-
raise
|
|
173
|
+
raise ServiceCommunicationException(f"communication exception {e}") from e
|
|
174
|
+
|
|
161
175
|
|
|
162
176
|
async def invoke_async(self, invocation: DynamicProxy.Invocation):
|
|
163
177
|
service_name = self.service_names[invocation.type] # map type to registered service name
|
|
164
178
|
request = Request(method=f"{self.component_descriptor.name}:{service_name}:{invocation.method.__name__}",
|
|
165
179
|
args=invocation.args)
|
|
166
|
-
|
|
167
180
|
dict = self.to_dict(request)
|
|
168
181
|
try:
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
182
|
+
data = await self.get_async_client().post(f"{self.get_url()}/invoke", json=dict, timeout=self.timeout)
|
|
183
|
+
result = Response(**data.json())
|
|
184
|
+
|
|
185
|
+
if result.exception is not None:
|
|
186
|
+
raise RemoteServiceException(f"server side exception {result.exception}")
|
|
187
|
+
|
|
188
|
+
return self.get_deserializer(invocation.type, invocation.method)(result.result)
|
|
189
|
+
|
|
190
|
+
except ServiceCommunicationException:
|
|
191
|
+
raise
|
|
192
|
+
|
|
193
|
+
except RemoteServiceException:
|
|
194
|
+
raise
|
|
195
|
+
|
|
179
196
|
except Exception as e:
|
|
180
|
-
raise
|
|
197
|
+
raise ServiceCommunicationException(f"communication exception {e}") from e
|
|
198
|
+
|
|
181
199
|
|
|
182
200
|
@channel("dispatch-msgpack")
|
|
183
201
|
class DispatchMSPackChannel(HTTPXChannel):
|
|
@@ -208,7 +226,7 @@ class DispatchMSPackChannel(HTTPXChannel):
|
|
|
208
226
|
f"{self.get_url()}/invoke",
|
|
209
227
|
content=packed,
|
|
210
228
|
headers={"Content-Type": "application/msgpack"},
|
|
211
|
-
timeout=
|
|
229
|
+
timeout=self.timeout
|
|
212
230
|
)
|
|
213
231
|
|
|
214
232
|
result = msgpack.unpackb(response.content, raw=False)
|
|
@@ -218,6 +236,12 @@ class DispatchMSPackChannel(HTTPXChannel):
|
|
|
218
236
|
|
|
219
237
|
return self.get_deserializer(invocation.type, invocation.method)(result["result"])
|
|
220
238
|
|
|
239
|
+
except ServiceCommunicationException:
|
|
240
|
+
raise
|
|
241
|
+
|
|
242
|
+
except RemoteServiceException:
|
|
243
|
+
raise
|
|
244
|
+
|
|
221
245
|
except Exception as e:
|
|
222
246
|
raise ServiceException(f"msgpack exception: {e}") from e
|
|
223
247
|
|
|
@@ -233,7 +257,7 @@ class DispatchMSPackChannel(HTTPXChannel):
|
|
|
233
257
|
f"{self.get_url()}/invoke",
|
|
234
258
|
content=packed,
|
|
235
259
|
headers={"Content-Type": "application/msgpack"},
|
|
236
|
-
timeout=
|
|
260
|
+
timeout=self.timeout
|
|
237
261
|
)
|
|
238
262
|
|
|
239
263
|
result = msgpack.unpackb(response.content, raw=False)
|
|
@@ -243,5 +267,11 @@ class DispatchMSPackChannel(HTTPXChannel):
|
|
|
243
267
|
|
|
244
268
|
return self.get_deserializer(invocation.type, invocation.method)(result["result"])
|
|
245
269
|
|
|
270
|
+
except ServiceCommunicationException:
|
|
271
|
+
raise
|
|
272
|
+
|
|
273
|
+
except RemoteServiceException:
|
|
274
|
+
raise
|
|
275
|
+
|
|
246
276
|
except Exception as e:
|
|
247
277
|
raise ServiceException(f"msgpack exception: {e}") from e
|
|
@@ -6,7 +6,6 @@ import threading
|
|
|
6
6
|
from abc import abstractmethod
|
|
7
7
|
import time
|
|
8
8
|
from typing import Optional
|
|
9
|
-
from urllib.parse import urlparse
|
|
10
9
|
|
|
11
10
|
import consul
|
|
12
11
|
|
|
@@ -36,6 +35,9 @@ class ConsulComponentRegistry(ComponentRegistry):
|
|
|
36
35
|
self.component_addresses : dict[str, dict[str, ChannelInstances]] = {} # comp -> channel -> address
|
|
37
36
|
self.watch_channels : list[Channel] = []
|
|
38
37
|
self.watchdog_interval = 5
|
|
38
|
+
self.healthcheck_interval = "10s"
|
|
39
|
+
self.healthcheck_timeout= "5s"
|
|
40
|
+
self.healthcheck_deregister = "5m"
|
|
39
41
|
|
|
40
42
|
# injections
|
|
41
43
|
|
|
@@ -43,6 +45,18 @@ class ConsulComponentRegistry(ComponentRegistry):
|
|
|
43
45
|
def set_interval(self, interval):
|
|
44
46
|
self.watchdog_interval = interval
|
|
45
47
|
|
|
48
|
+
@inject_value("consul.healthcheck.interval", default="10s")
|
|
49
|
+
def set_interval(self, interval):
|
|
50
|
+
self.healthcheck_interval = interval
|
|
51
|
+
|
|
52
|
+
@inject_value("consul.healthcheck.timeout", default="3s")
|
|
53
|
+
def set_interval(self, interval):
|
|
54
|
+
self.healthcheck_timeout = interval
|
|
55
|
+
|
|
56
|
+
@inject_value("consul.healthcheck.deregister", default="5m")
|
|
57
|
+
def set_interval(self, interval):
|
|
58
|
+
self.healthcheck_deregister = interval
|
|
59
|
+
|
|
46
60
|
# lifecycle hooks
|
|
47
61
|
|
|
48
62
|
@on_init()
|
|
@@ -116,8 +130,6 @@ class ConsulComponentRegistry(ComponentRegistry):
|
|
|
116
130
|
def watch(self, channel: Channel) -> None:
|
|
117
131
|
self.watch_channels.append(channel)
|
|
118
132
|
|
|
119
|
-
#self.component_addresses[channel.component_descriptor.name] = {}
|
|
120
|
-
|
|
121
133
|
# public
|
|
122
134
|
|
|
123
135
|
def register_service(self, name, service_id, health: str, tags=None, meta=None) -> None:
|
|
@@ -130,9 +142,9 @@ class ConsulComponentRegistry(ComponentRegistry):
|
|
|
130
142
|
meta=meta or {},
|
|
131
143
|
check=consul.Check().http(
|
|
132
144
|
url=f"http://{self.ip}:{self.port}{health}",
|
|
133
|
-
interval=
|
|
134
|
-
timeout=
|
|
135
|
-
deregister=
|
|
145
|
+
interval=self.healthcheck_interval,
|
|
146
|
+
timeout=self.healthcheck_timeout,
|
|
147
|
+
deregister=self.healthcheck_deregister)
|
|
136
148
|
)
|
|
137
149
|
|
|
138
150
|
def deregister(self, descriptor: ComponentDescriptor[Component]) -> None:
|
|
@@ -213,12 +225,13 @@ class ConsulComponentRegistry(ComponentRegistry):
|
|
|
213
225
|
|
|
214
226
|
return list(component_addresses.values())
|
|
215
227
|
|
|
216
|
-
#200–299 passing Service is healthy (OK, Created, No Content…)
|
|
217
|
-
#429 warning Rate limited or degraded
|
|
218
|
-
#300–399 warning Redirects interpreted as potential issues
|
|
219
|
-
#400–499 critical Client errors (Bad Request, Unauthorized…)
|
|
220
|
-
#500–599 critical Server errors (Internal Error, Timeout…)
|
|
221
|
-
#Other / No response critical Timeout, connection refused, etc.
|
|
228
|
+
# 200–299 passing Service is healthy (OK, Created, No Content…)
|
|
229
|
+
# 429 warning Rate limited or degraded
|
|
230
|
+
# 300–399 warning Redirects interpreted as potential issues
|
|
231
|
+
# 400–499 critical Client errors (Bad Request, Unauthorized…)
|
|
232
|
+
# 500–599 critical Server errors (Internal Error, Timeout…)
|
|
233
|
+
# Other / No response critical Timeout, connection refused, etc.
|
|
234
|
+
|
|
222
235
|
def map_health(self, health: HealthCheckManager.Health) -> int:
|
|
223
236
|
if health.status is HealthStatus.OK:
|
|
224
237
|
return 200
|
|
@@ -10,7 +10,7 @@ from typing import get_type_hints, TypeVar, Annotated, Callable, get_origin, get
|
|
|
10
10
|
from pydantic import BaseModel
|
|
11
11
|
|
|
12
12
|
from aspyx.reflection import DynamicProxy, Decorators
|
|
13
|
-
from .service import
|
|
13
|
+
from .service import channel, ServiceCommunicationException
|
|
14
14
|
|
|
15
15
|
T = TypeVar("T")
|
|
16
16
|
|
|
@@ -134,6 +134,9 @@ class RestChannel(HTTPXChannel):
|
|
|
134
134
|
def __init__(self, type: Type, method : Callable):
|
|
135
135
|
self.signature = inspect.signature(method)
|
|
136
136
|
|
|
137
|
+
param_names = list(self.signature.parameters.keys())
|
|
138
|
+
param_names.remove("self")
|
|
139
|
+
|
|
137
140
|
prefix = ""
|
|
138
141
|
if Decorators.has_decorator(type, rest):
|
|
139
142
|
prefix = Decorators.get_decorator(type, rest).args[0]
|
|
@@ -155,6 +158,9 @@ class RestChannel(HTTPXChannel):
|
|
|
155
158
|
|
|
156
159
|
self.path_param_names = set(re.findall(r"{(.*?)}", self.url_template))
|
|
157
160
|
|
|
161
|
+
for param_name in self.path_param_names:
|
|
162
|
+
param_names.remove(param_name)
|
|
163
|
+
|
|
158
164
|
hints = get_type_hints(method, include_extras=True)
|
|
159
165
|
|
|
160
166
|
self.body_param_name = None
|
|
@@ -163,10 +169,42 @@ class RestChannel(HTTPXChannel):
|
|
|
163
169
|
for param_name, hint in hints.items():
|
|
164
170
|
if get_origin(hint) is Annotated:
|
|
165
171
|
metadata = get_args(hint)[1:]
|
|
172
|
+
|
|
166
173
|
if BodyMarker in metadata:
|
|
167
174
|
self.body_param_name = param_name
|
|
175
|
+
param_names.remove(param_name)
|
|
168
176
|
elif QueryParamMarker in metadata:
|
|
169
177
|
self.query_param_names.add(param_name)
|
|
178
|
+
param_names.remove(param_name)
|
|
179
|
+
|
|
180
|
+
# check if something is missing
|
|
181
|
+
|
|
182
|
+
if len(param_names) > 0:
|
|
183
|
+
# check body params
|
|
184
|
+
if self.type in ("post", "put", "patch"):
|
|
185
|
+
if self.body_param_name is None:
|
|
186
|
+
candidates = [
|
|
187
|
+
(name, hint)
|
|
188
|
+
for name, hint in hints.items()
|
|
189
|
+
if name not in self.path_param_names
|
|
190
|
+
]
|
|
191
|
+
# find first dataclass or pydantic argument
|
|
192
|
+
for name, hint in candidates:
|
|
193
|
+
typ = hint
|
|
194
|
+
if get_origin(typ) is Annotated:
|
|
195
|
+
typ = get_args(typ)[0]
|
|
196
|
+
if (
|
|
197
|
+
(isinstance(typ, type) and issubclass(typ, BaseModel))
|
|
198
|
+
or is_dataclass(typ)
|
|
199
|
+
):
|
|
200
|
+
self.body_param_name = name
|
|
201
|
+
param_names.remove(name)
|
|
202
|
+
break
|
|
203
|
+
|
|
204
|
+
# the rest are query params
|
|
205
|
+
|
|
206
|
+
for param in param_names:
|
|
207
|
+
self.query_param_names.add(param)
|
|
170
208
|
|
|
171
209
|
# return type
|
|
172
210
|
|
|
@@ -191,6 +229,42 @@ class RestChannel(HTTPXChannel):
|
|
|
191
229
|
|
|
192
230
|
# override
|
|
193
231
|
|
|
232
|
+
async def invoke_async(self, invocation: 'DynamicProxy.Invocation'):
|
|
233
|
+
call = self.get_call(invocation.type, invocation.method)
|
|
234
|
+
|
|
235
|
+
bound = call.signature.bind(self, *invocation.args, **invocation.kwargs)
|
|
236
|
+
bound.apply_defaults()
|
|
237
|
+
arguments = bound.arguments
|
|
238
|
+
|
|
239
|
+
# url
|
|
240
|
+
|
|
241
|
+
url = call.url_template.format(**arguments)
|
|
242
|
+
query_params = {k: arguments[k] for k in call.query_param_names if k in arguments}
|
|
243
|
+
body = {}
|
|
244
|
+
if call.body_param_name is not None:
|
|
245
|
+
body = self.to_dict(arguments.get(call.body_param_name))
|
|
246
|
+
|
|
247
|
+
# call
|
|
248
|
+
|
|
249
|
+
try:
|
|
250
|
+
result = None
|
|
251
|
+
if call.type == "get":
|
|
252
|
+
result = await self.get_client().get(self.get_url() + url, params=query_params, timeout=self.timeout).json()
|
|
253
|
+
elif call.type == "put":
|
|
254
|
+
result = await self.get_client().put(self.get_url() + url, params=query_params, timeout=self.timeout).json()
|
|
255
|
+
elif call.type == "delete":
|
|
256
|
+
result = await self.get_client().delete(self.get_url() + url, params=query_params, timeout=self.timeout).json()
|
|
257
|
+
elif call.type == "post":
|
|
258
|
+
result = await self.get_client().post(self.get_url() + url, params=query_params, json=body,
|
|
259
|
+
timeout=self.timeout).json()
|
|
260
|
+
|
|
261
|
+
return self.get_deserializer(invocation.type, invocation.method)(result)
|
|
262
|
+
except ServiceCommunicationException:
|
|
263
|
+
raise
|
|
264
|
+
|
|
265
|
+
except Exception as e:
|
|
266
|
+
raise ServiceCommunicationException(f"communication exception {e}") from e
|
|
267
|
+
|
|
194
268
|
def invoke(self, invocation: DynamicProxy.Invocation):
|
|
195
269
|
call = self.get_call(invocation.type, invocation.method)
|
|
196
270
|
|
|
@@ -209,20 +283,19 @@ class RestChannel(HTTPXChannel):
|
|
|
209
283
|
# call
|
|
210
284
|
|
|
211
285
|
try:
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
f"no url for channel {self.name} for component {self.component_descriptor.name} registered")
|
|
286
|
+
result = None
|
|
287
|
+
if call.type == "get":
|
|
288
|
+
result = self.get_client().get(self.get_url() + url, params=query_params, timeout=self.timeout).json()
|
|
289
|
+
elif call.type == "put":
|
|
290
|
+
result = self.get_client().put(self.get_url() + url, params=query_params, timeout=self.timeout).json()
|
|
291
|
+
elif call.type == "delete":
|
|
292
|
+
result = self.get_client().delete(self.get_url() + url, params=query_params, timeout=self.timeout).json()
|
|
293
|
+
elif call.type == "post":
|
|
294
|
+
result = self.get_client().post(self.get_url() + url, params=query_params, json=body, timeout=self.timeout).json()
|
|
295
|
+
|
|
296
|
+
return self.get_deserializer(invocation.type, invocation.method)(result)
|
|
297
|
+
except ServiceCommunicationException:
|
|
298
|
+
raise
|
|
299
|
+
|
|
227
300
|
except Exception as e:
|
|
228
|
-
raise
|
|
301
|
+
raise ServiceCommunicationException(f"communication exception {e}") from e
|
|
@@ -7,67 +7,6 @@ from typing import get_origin, get_args, Union
|
|
|
7
7
|
|
|
8
8
|
from pydantic import BaseModel
|
|
9
9
|
|
|
10
|
-
def deserialize(value, return_type):
|
|
11
|
-
if value is None:
|
|
12
|
-
return None
|
|
13
|
-
|
|
14
|
-
origin = get_origin(return_type)
|
|
15
|
-
args = get_args(return_type)
|
|
16
|
-
|
|
17
|
-
# Handle Optional / Union
|
|
18
|
-
if origin is Union and type(None) in args:
|
|
19
|
-
real_type = [arg for arg in args if arg is not type(None)][0]
|
|
20
|
-
return deserialize(value, real_type)
|
|
21
|
-
|
|
22
|
-
# Handle pydantic
|
|
23
|
-
if isinstance(return_type, type) and issubclass(return_type, BaseModel):
|
|
24
|
-
return return_type.parse_obj(value)
|
|
25
|
-
|
|
26
|
-
# Handle dataclass
|
|
27
|
-
if is_dataclass(return_type):
|
|
28
|
-
return from_dict(return_type, value)
|
|
29
|
-
|
|
30
|
-
# Handle List[T]
|
|
31
|
-
if origin is list:
|
|
32
|
-
item_type = args[0]
|
|
33
|
-
return [deserialize(v, item_type) for v in value]
|
|
34
|
-
|
|
35
|
-
# Fallback: primitive
|
|
36
|
-
return value
|
|
37
|
-
|
|
38
|
-
def from_dict(cls, data: dict):
|
|
39
|
-
if not is_dataclass(cls):
|
|
40
|
-
return data # primitive or unknown
|
|
41
|
-
|
|
42
|
-
kwargs = {}
|
|
43
|
-
for field in fields(cls):
|
|
44
|
-
name = field.name
|
|
45
|
-
field_type = field.type
|
|
46
|
-
value = data.get(name)
|
|
47
|
-
|
|
48
|
-
if value is None:
|
|
49
|
-
kwargs[name] = None
|
|
50
|
-
continue
|
|
51
|
-
|
|
52
|
-
origin = get_origin(field_type)
|
|
53
|
-
args = get_args(field_type)
|
|
54
|
-
|
|
55
|
-
if origin is Union and type(None) in args:
|
|
56
|
-
real_type = [arg for arg in args if arg is not type(None)][0]
|
|
57
|
-
kwargs[name] = deserialize(value, real_type)
|
|
58
|
-
|
|
59
|
-
elif is_dataclass(field_type):
|
|
60
|
-
kwargs[name] = from_dict(field_type, value)
|
|
61
|
-
|
|
62
|
-
elif origin is list:
|
|
63
|
-
item_type = args[0]
|
|
64
|
-
kwargs[name] = [deserialize(v, item_type) for v in value]
|
|
65
|
-
|
|
66
|
-
else:
|
|
67
|
-
kwargs[name] = value
|
|
68
|
-
|
|
69
|
-
return cls(**kwargs)
|
|
70
|
-
|
|
71
10
|
class TypeDeserializer:
|
|
72
11
|
# constructor
|
|
73
12
|
|
|
@@ -119,6 +58,54 @@ class TypeDeserializer:
|
|
|
119
58
|
|
|
120
59
|
# Fallback
|
|
121
60
|
return lambda v: v
|
|
61
|
+
|
|
62
|
+
class TypeSerializer:
|
|
63
|
+
def __init__(self, typ):
|
|
64
|
+
self.typ = typ
|
|
65
|
+
self.serializer = self._build_serializer(typ)
|
|
66
|
+
|
|
67
|
+
def __call__(self, value):
|
|
68
|
+
return self.serializer(value)
|
|
69
|
+
|
|
70
|
+
def _build_serializer(self, typ):
|
|
71
|
+
origin = get_origin(typ)
|
|
72
|
+
args = get_args(typ)
|
|
73
|
+
|
|
74
|
+
if origin is Union:
|
|
75
|
+
serializers = [TypeSerializer(arg) for arg in args if arg is not type(None)]
|
|
76
|
+
def ser_union(value):
|
|
77
|
+
if value is None:
|
|
78
|
+
return None
|
|
79
|
+
for s in serializers:
|
|
80
|
+
try:
|
|
81
|
+
return s(value)
|
|
82
|
+
except Exception:
|
|
83
|
+
continue
|
|
84
|
+
return value
|
|
85
|
+
return ser_union
|
|
86
|
+
|
|
87
|
+
if isinstance(typ, type) and issubclass(typ, BaseModel):
|
|
88
|
+
return lambda v: v.dict() if v is not None else None
|
|
89
|
+
|
|
90
|
+
if is_dataclass(typ):
|
|
91
|
+
field_serializers = {f.name: TypeSerializer(f.type) for f in fields(typ)}
|
|
92
|
+
def ser_dataclass(obj):
|
|
93
|
+
if obj is None:
|
|
94
|
+
return None
|
|
95
|
+
return {k: field_serializers[k](getattr(obj, k)) for k in field_serializers}
|
|
96
|
+
return ser_dataclass
|
|
97
|
+
|
|
98
|
+
if origin is list:
|
|
99
|
+
item_ser = TypeSerializer(args[0]) if args else lambda x: x
|
|
100
|
+
return lambda v: [item_ser(item) for item in v] if v is not None else None
|
|
101
|
+
|
|
102
|
+
if origin is dict:
|
|
103
|
+
key_ser = TypeSerializer(args[0]) if args else lambda x: x
|
|
104
|
+
val_ser = TypeSerializer(args[1]) if len(args) > 1 else lambda x: x
|
|
105
|
+
return lambda v: {key_ser(k): val_ser(val) for k, val in v.items()} if v is not None else None
|
|
106
|
+
|
|
107
|
+
# Fallback: primitive Typen oder unbekannt
|
|
108
|
+
return lambda v: v
|
|
122
109
|
|
|
123
110
|
@lru_cache(maxsize=512)
|
|
124
111
|
def get_deserializer(typ):
|
|
@@ -132,3 +119,16 @@ def get_deserializer(typ):
|
|
|
132
119
|
|
|
133
120
|
"""
|
|
134
121
|
return TypeDeserializer(typ)
|
|
122
|
+
|
|
123
|
+
@lru_cache(maxsize=512)
|
|
124
|
+
def get_serializer(typ):
|
|
125
|
+
"""
|
|
126
|
+
return a function that is able to deserialize a value of the specified type
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
typ: the type
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
|
|
133
|
+
"""
|
|
134
|
+
return TypeSerializer(typ)
|
|
@@ -38,7 +38,6 @@ class FastAPIServer(Server):
|
|
|
38
38
|
self.host = host
|
|
39
39
|
Server.port = port
|
|
40
40
|
self.server_thread = None
|
|
41
|
-
self.environment : Optional[Environment] = None
|
|
42
41
|
self.service_manager : Optional[ServiceManager] = None
|
|
43
42
|
self.component_registry: Optional[ComponentRegistry] = None
|
|
44
43
|
|
|
@@ -83,19 +82,17 @@ class FastAPIServer(Server):
|
|
|
83
82
|
response_model=method.return_type,
|
|
84
83
|
)
|
|
85
84
|
|
|
86
|
-
def
|
|
85
|
+
def start_fastapi_thread(self):
|
|
87
86
|
"""
|
|
88
87
|
start the fastapi server in a thread
|
|
89
88
|
"""
|
|
90
89
|
|
|
91
|
-
config = uvicorn.Config(self.fast_api, host=self.host, port=self.port, log_level="debug"
|
|
90
|
+
config = uvicorn.Config(self.fast_api, host=self.host, port=self.port, access_log=False) #log_level="debug"
|
|
92
91
|
server = uvicorn.Server(config)
|
|
93
92
|
|
|
94
93
|
thread = threading.Thread(target=server.run, daemon=True)
|
|
95
94
|
thread.start()
|
|
96
95
|
|
|
97
|
-
print(f"server started on {self.host}:{self.port}")
|
|
98
|
-
|
|
99
96
|
return thread
|
|
100
97
|
|
|
101
98
|
|
|
@@ -212,12 +209,12 @@ class FastAPIServer(Server):
|
|
|
212
209
|
self.add_routes()
|
|
213
210
|
self.fast_api.include_router(self.router)
|
|
214
211
|
|
|
215
|
-
for route in self.fast_api.routes:
|
|
216
|
-
|
|
212
|
+
#for route in self.fast_api.routes:
|
|
213
|
+
# print(f"{route.name}: {route.path} [{route.methods}]")
|
|
217
214
|
|
|
218
215
|
# start server thread
|
|
219
216
|
|
|
220
|
-
self.
|
|
217
|
+
self.start_fastapi_thread()
|
|
221
218
|
|
|
222
219
|
# shutdown
|
|
223
220
|
|