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.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aspyx_service
3
- Version: 0.10.1
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.0
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
+ ![Pylint](https://github.com/coolsamson7/aspyx/actions/workflows/pylint.yml/badge.svg)
40
+ ![Build Status](https://github.com/coolsamson7/aspyx/actions/workflows/ci.yml/badge.svg)
41
+ ![Python Versions](https://img.shields.io/badge/python-3.9%20|%203.10%20|%203.11%20|%203.12-blue)
42
+ ![License](https://img.shields.io/github/license/coolsamson7/aspyx)
43
+ ![coverage](https://img.shields.io/badge/coverage-94%25-brightgreen)
44
+ [![PyPI](https://img.shields.io/pypi/v/aspyx)](https://pypi.org/project/aspyx/)
45
+ [![Docs](https://img.shields.io/badge/docs-online-blue?logo=github)](https://coolsamson7.github.io/aspyx/index/introduction)
46
+
47
+ ![image](https://github.com/user-attachments/assets/e808210a-b1a4-4fd0-93f1-b5f9845fa520)
48
+
37
49
  # Service
38
50
 
39
51
  - [Introduction](#introduction)
@@ -1,3 +1,15 @@
1
+ # aspyx
2
+
3
+ ![Pylint](https://github.com/coolsamson7/aspyx/actions/workflows/pylint.yml/badge.svg)
4
+ ![Build Status](https://github.com/coolsamson7/aspyx/actions/workflows/ci.yml/badge.svg)
5
+ ![Python Versions](https://img.shields.io/badge/python-3.9%20|%203.10%20|%203.11%20|%203.12-blue)
6
+ ![License](https://img.shields.io/github/license/coolsamson7/aspyx)
7
+ ![coverage](https://img.shields.io/badge/coverage-94%25-brightgreen)
8
+ [![PyPI](https://img.shields.io/pypi/v/aspyx)](https://pypi.org/project/aspyx/)
9
+ [![Docs](https://img.shields.io/badge/docs-online-blue?logo=github)](https://coolsamson7.github.io/aspyx/index/introduction)
10
+
11
+ ![image](https://github.com/user-attachments/assets/e808210a-b1a4-4fd0-93f1-b5f9845fa520)
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.1"
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.0",
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) -> Client:
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 th endpoint `ìnvoke` sending a request body containing information on the
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__}", args=invocation.args)
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
- if self.client is not None:
152
- result = Response(**self.get_client().post(f"{self.get_url()}/invoke", json=dict, timeout=30000.0).json())
153
- if result.exception is not None:
154
- raise RemoteServiceException(f"server side exception {result.exception}")
155
-
156
- return self.get_deserializer(invocation.type, invocation.method)(result.result)
157
- else:
158
- raise ServiceException(f"no url for channel {self.name} for component {self.component_descriptor.name} registered")
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 ServiceException(f"communication exception {e}") from e
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
- if self.async_client is not None:
170
- data = await self.async_client.post(f"{self.get_url()}/invoke", json=dict, timeout=30000.0)
171
- result = Response(**data.json())
172
- if result.exception is not None:
173
- raise RemoteServiceException(f"server side exception {result.exception}")
174
-
175
- return self.get_deserializer(invocation.type, invocation.method)(result.result)
176
- else:
177
- raise ServiceException(
178
- f"no url for channel {self.name} for component {self.component_descriptor.name} registered")
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 ServiceException(f"communication exception {e}") from e
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=30.0
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=30.0
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="10s",
134
- timeout="3s",
135
- deregister="5m")
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 ServiceException, channel
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
- if self.client is not None:
213
- result = None
214
- if call.type == "get":
215
- result = self.client.get(self.get_url() + url, params=query_params, timeout=30000.0).json()
216
- elif call.type == "put":
217
- result = self.client.put(self.get_url() + url, params=query_params, timeout=30000.0).json()
218
- elif call.type == "delete":
219
- result = self.client.delete(self.get_url() + url, params=query_params, timeout=30000.0).json()
220
- elif call.type == "post":
221
- result = self.client.post(self.get_url() + url, params=query_params, json=body, timeout=30000.0).json()
222
-
223
- return self.get_deserializer(invocation.type, invocation.method)(result)
224
- else:
225
- raise ServiceException(
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 ServiceException(f"communication exception {e}") from e
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 start(self):
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
- print(f"{route.name}: {route.path} [{route.methods}]")
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.start()
217
+ self.start_fastapi_thread()
221
218
 
222
219
  # shutdown
223
220