aspyx-service 0.9.0__py3-none-any.whl → 0.10.1__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 +7 -2
- aspyx_service/channels.py +51 -14
- aspyx_service/healthcheck.py +20 -4
- aspyx_service/registries.py +24 -23
- aspyx_service/restchannel.py +54 -25
- aspyx_service/serialization.py +10 -1
- aspyx_service/server.py +28 -10
- aspyx_service/service.py +196 -53
- aspyx_service-0.10.1.dist-info/METADATA +505 -0
- aspyx_service-0.10.1.dist-info/RECORD +12 -0
- aspyx_service-0.9.0.dist-info/METADATA +0 -36
- aspyx_service-0.9.0.dist-info/RECORD +0 -12
- {aspyx_service-0.9.0.dist-info → aspyx_service-0.10.1.dist-info}/WHEEL +0 -0
- {aspyx_service-0.9.0.dist-info → aspyx_service-0.10.1.dist-info}/licenses/LICENSE +0 -0
aspyx_service/__init__.py
CHANGED
|
@@ -4,7 +4,7 @@ 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,
|
|
7
|
+
from .service import ServiceException, Server, Channel, ComponentDescriptor, inject_service, ChannelAddress, ChannelInstances, ServiceManager, Component, Service, AbstractComponent, ComponentStatus, ComponentRegistry, implementation, health, component, service
|
|
8
8
|
from .channels import HTTPXChannel, DispatchJSONChannel
|
|
9
9
|
from .registries import ConsulComponentRegistry
|
|
10
10
|
from .server import FastAPIServer
|
|
@@ -32,7 +32,7 @@ __all__ = [
|
|
|
32
32
|
"ComponentDescriptor",
|
|
33
33
|
"ComponentRegistry",
|
|
34
34
|
"ChannelAddress",
|
|
35
|
-
"
|
|
35
|
+
"ChannelInstances",
|
|
36
36
|
"health",
|
|
37
37
|
"component",
|
|
38
38
|
"service",
|
|
@@ -41,6 +41,11 @@ __all__ = [
|
|
|
41
41
|
|
|
42
42
|
# healthcheck
|
|
43
43
|
|
|
44
|
+
"health_checks",
|
|
45
|
+
"health_check",
|
|
46
|
+
"HealthStatus",
|
|
47
|
+
"HealthCheckManager",
|
|
48
|
+
|
|
44
49
|
# serialization
|
|
45
50
|
|
|
46
51
|
# "deserialize",
|
aspyx_service/channels.py
CHANGED
|
@@ -3,6 +3,8 @@ Service management and dependency injection framework.
|
|
|
3
3
|
"""
|
|
4
4
|
from __future__ import annotations
|
|
5
5
|
|
|
6
|
+
import json
|
|
7
|
+
from dataclasses import is_dataclass, asdict, fields
|
|
6
8
|
from typing import Type, Optional, Any, Callable
|
|
7
9
|
|
|
8
10
|
import msgpack
|
|
@@ -12,7 +14,7 @@ from pydantic import BaseModel
|
|
|
12
14
|
from aspyx.reflection import DynamicProxy, TypeDescriptor
|
|
13
15
|
from .service import ServiceManager
|
|
14
16
|
|
|
15
|
-
from .service import ComponentDescriptor,
|
|
17
|
+
from .service import ComponentDescriptor, ChannelInstances, ServiceException, channel, Channel, RemoteServiceException
|
|
16
18
|
from .serialization import get_deserializer
|
|
17
19
|
|
|
18
20
|
|
|
@@ -24,10 +26,37 @@ class HTTPXChannel(Channel):
|
|
|
24
26
|
"deserializers"
|
|
25
27
|
]
|
|
26
28
|
|
|
29
|
+
# class methods
|
|
30
|
+
|
|
31
|
+
@classmethod
|
|
32
|
+
def to_dict(cls, obj: Any) -> Any:
|
|
33
|
+
if isinstance(obj, BaseModel):
|
|
34
|
+
return obj.dict()
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
elif is_dataclass(obj):
|
|
38
|
+
return {
|
|
39
|
+
f.name: cls.to_dict(getattr(obj, f.name))
|
|
40
|
+
|
|
41
|
+
for f in fields(obj)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
elif isinstance(obj, (list, tuple)):
|
|
45
|
+
return [cls.to_dict(item) for item in obj]
|
|
46
|
+
|
|
47
|
+
elif isinstance(obj, dict):
|
|
48
|
+
return {key: cls.to_dict(value) for key, value in obj.items()}
|
|
49
|
+
|
|
50
|
+
return obj
|
|
51
|
+
|
|
52
|
+
@classmethod
|
|
53
|
+
def to_json(cls, obj) -> str:
|
|
54
|
+
return json.dumps(cls.to_dict(obj))
|
|
55
|
+
|
|
27
56
|
# constructor
|
|
28
57
|
|
|
29
|
-
def __init__(self
|
|
30
|
-
super().__init__(
|
|
58
|
+
def __init__(self):
|
|
59
|
+
super().__init__()
|
|
31
60
|
|
|
32
61
|
self.client: Optional[Client] = None
|
|
33
62
|
self.async_client: Optional[AsyncClient] = None
|
|
@@ -49,7 +78,7 @@ class HTTPXChannel(Channel):
|
|
|
49
78
|
|
|
50
79
|
# override
|
|
51
80
|
|
|
52
|
-
def setup(self, component_descriptor: ComponentDescriptor, address:
|
|
81
|
+
def setup(self, component_descriptor: ComponentDescriptor, address: ChannelInstances):
|
|
53
82
|
super().setup(component_descriptor, address)
|
|
54
83
|
|
|
55
84
|
# remember service names
|
|
@@ -92,30 +121,35 @@ class Response(BaseModel):
|
|
|
92
121
|
|
|
93
122
|
@channel("dispatch-json")
|
|
94
123
|
class DispatchJSONChannel(HTTPXChannel):
|
|
124
|
+
"""
|
|
125
|
+
A channel that calls a POST on th endpoint `ìnvoke` sending a request body containing information on the
|
|
126
|
+
called component, service and method and the arguments.
|
|
127
|
+
"""
|
|
95
128
|
# constructor
|
|
96
129
|
|
|
97
130
|
def __init__(self):
|
|
98
|
-
super().__init__(
|
|
131
|
+
super().__init__()
|
|
99
132
|
|
|
100
133
|
# internal
|
|
101
134
|
|
|
102
135
|
# implement Channel
|
|
103
136
|
|
|
104
|
-
def set_address(self, address: Optional[
|
|
137
|
+
def set_address(self, address: Optional[ChannelInstances]):
|
|
105
138
|
ServiceManager.logger.info("channel %s got an address %s", self.name, address)
|
|
106
139
|
|
|
107
140
|
super().set_address(address)
|
|
108
141
|
|
|
109
|
-
def setup(self, component_descriptor: ComponentDescriptor, address:
|
|
142
|
+
def setup(self, component_descriptor: ComponentDescriptor, address: ChannelInstances):
|
|
110
143
|
super().setup(component_descriptor, address)
|
|
111
144
|
|
|
112
145
|
def invoke(self, invocation: DynamicProxy.Invocation):
|
|
113
146
|
service_name = self.service_names[invocation.type] # map type to registered service name
|
|
114
147
|
request = Request(method=f"{self.component_descriptor.name}:{service_name}:{invocation.method.__name__}", args=invocation.args)
|
|
115
148
|
|
|
149
|
+
dict = self.to_dict(request)
|
|
116
150
|
try:
|
|
117
151
|
if self.client is not None:
|
|
118
|
-
result = Response(**self.get_client().post(f"{self.get_url()}/invoke", json=
|
|
152
|
+
result = Response(**self.get_client().post(f"{self.get_url()}/invoke", json=dict, timeout=30000.0).json())
|
|
119
153
|
if result.exception is not None:
|
|
120
154
|
raise RemoteServiceException(f"server side exception {result.exception}")
|
|
121
155
|
|
|
@@ -130,9 +164,10 @@ class DispatchJSONChannel(HTTPXChannel):
|
|
|
130
164
|
request = Request(method=f"{self.component_descriptor.name}:{service_name}:{invocation.method.__name__}",
|
|
131
165
|
args=invocation.args)
|
|
132
166
|
|
|
167
|
+
dict = self.to_dict(request)
|
|
133
168
|
try:
|
|
134
169
|
if self.async_client is not None:
|
|
135
|
-
data = await self.async_client.post(f"{self.get_url()}/invoke", json=
|
|
170
|
+
data = await self.async_client.post(f"{self.get_url()}/invoke", json=dict, timeout=30000.0)
|
|
136
171
|
result = Response(**data.json())
|
|
137
172
|
if result.exception is not None:
|
|
138
173
|
raise RemoteServiceException(f"server side exception {result.exception}")
|
|
@@ -146,14 +181,17 @@ class DispatchJSONChannel(HTTPXChannel):
|
|
|
146
181
|
|
|
147
182
|
@channel("dispatch-msgpack")
|
|
148
183
|
class DispatchMSPackChannel(HTTPXChannel):
|
|
184
|
+
"""
|
|
185
|
+
A channel that sends a POST on the ìnvoke `endpoint`with an msgpack encoded request body.
|
|
186
|
+
"""
|
|
149
187
|
# constructor
|
|
150
188
|
|
|
151
189
|
def __init__(self):
|
|
152
|
-
super().__init__(
|
|
190
|
+
super().__init__()
|
|
153
191
|
|
|
154
192
|
# override
|
|
155
193
|
|
|
156
|
-
def set_address(self, address: Optional[
|
|
194
|
+
def set_address(self, address: Optional[ChannelInstances]):
|
|
157
195
|
ServiceManager.logger.info("channel %s got an address %s", self.name, address)
|
|
158
196
|
|
|
159
197
|
super().set_address(address)
|
|
@@ -164,7 +202,7 @@ class DispatchMSPackChannel(HTTPXChannel):
|
|
|
164
202
|
args=invocation.args)
|
|
165
203
|
|
|
166
204
|
try:
|
|
167
|
-
packed = msgpack.packb(
|
|
205
|
+
packed = msgpack.packb(self.to_dict(request), use_bin_type=True)
|
|
168
206
|
|
|
169
207
|
response = self.get_client().post(
|
|
170
208
|
f"{self.get_url()}/invoke",
|
|
@@ -189,7 +227,7 @@ class DispatchMSPackChannel(HTTPXChannel):
|
|
|
189
227
|
args=invocation.args)
|
|
190
228
|
|
|
191
229
|
try:
|
|
192
|
-
packed = msgpack.packb(
|
|
230
|
+
packed = msgpack.packb(self.to_dict(request), use_bin_type=True)
|
|
193
231
|
|
|
194
232
|
response = await self.get_async_client().post(
|
|
195
233
|
f"{self.get_url()}/invoke",
|
|
@@ -207,4 +245,3 @@ class DispatchMSPackChannel(HTTPXChannel):
|
|
|
207
245
|
|
|
208
246
|
except Exception as e:
|
|
209
247
|
raise ServiceException(f"msgpack exception: {e}") from e
|
|
210
|
-
|
aspyx_service/healthcheck.py
CHANGED
|
@@ -9,7 +9,7 @@ import time
|
|
|
9
9
|
from enum import Enum
|
|
10
10
|
from typing import Any, Callable, Type, Optional
|
|
11
11
|
|
|
12
|
-
from aspyx.di import
|
|
12
|
+
from aspyx.di import injectable, Environment, inject_environment, on_init
|
|
13
13
|
from aspyx.reflection import Decorators, TypeDescriptor
|
|
14
14
|
|
|
15
15
|
|
|
@@ -31,7 +31,8 @@ def health_checks():
|
|
|
31
31
|
|
|
32
32
|
def health_check(name="", cache = 0, fail_if_slower_than = 0):
|
|
33
33
|
"""
|
|
34
|
-
Methods annotated with `@on_init` will be called when the instance is created.
|
|
34
|
+
Methods annotated with `@on_init` will be called when the instance is created.
|
|
35
|
+
"""
|
|
35
36
|
def decorator(func):
|
|
36
37
|
Decorators.add(func, health_check, name, cache, fail_if_slower_than)
|
|
37
38
|
return func
|
|
@@ -39,6 +40,13 @@ def health_check(name="", cache = 0, fail_if_slower_than = 0):
|
|
|
39
40
|
return decorator
|
|
40
41
|
|
|
41
42
|
class HealthStatus(Enum):
|
|
43
|
+
"""
|
|
44
|
+
A enum specifying the health status of a service. The values are:
|
|
45
|
+
|
|
46
|
+
- `OK` service is healthy
|
|
47
|
+
- `WARNING` service has some problems
|
|
48
|
+
- `CRITICAL` service is unhealthy
|
|
49
|
+
"""
|
|
42
50
|
OK = 1
|
|
43
51
|
WARNING = 2
|
|
44
52
|
ERROR = 3
|
|
@@ -49,6 +57,9 @@ class HealthStatus(Enum):
|
|
|
49
57
|
|
|
50
58
|
@injectable()
|
|
51
59
|
class HealthCheckManager:
|
|
60
|
+
"""
|
|
61
|
+
The health manager is able to run all registered health checks and is able to return an overall status.
|
|
62
|
+
"""
|
|
52
63
|
logger = logging.getLogger("aspyx.service.health")
|
|
53
64
|
|
|
54
65
|
# local classes
|
|
@@ -112,8 +123,8 @@ class HealthCheckManager:
|
|
|
112
123
|
return result
|
|
113
124
|
|
|
114
125
|
class Health:
|
|
115
|
-
def __init__(self):
|
|
116
|
-
self.status =
|
|
126
|
+
def __init__(self, status: HealthStatus = HealthStatus.OK):
|
|
127
|
+
self.status = status
|
|
117
128
|
self.results : list[HealthCheckManager.Result] = []
|
|
118
129
|
|
|
119
130
|
def to_dict(self):
|
|
@@ -135,6 +146,11 @@ class HealthCheckManager:
|
|
|
135
146
|
# check
|
|
136
147
|
|
|
137
148
|
async def check(self) -> HealthCheckManager.Health:
|
|
149
|
+
"""
|
|
150
|
+
run all registered health checks and return an overall result.
|
|
151
|
+
Returns: the overall result.
|
|
152
|
+
|
|
153
|
+
"""
|
|
138
154
|
self.logger.info("Checking health...")
|
|
139
155
|
|
|
140
156
|
health = HealthCheckManager.Health()
|
aspyx_service/registries.py
CHANGED
|
@@ -10,34 +10,38 @@ from urllib.parse import urlparse
|
|
|
10
10
|
|
|
11
11
|
import consul
|
|
12
12
|
|
|
13
|
+
from aspyx.di.configuration import inject_value
|
|
13
14
|
from aspyx.util import StringBuilder
|
|
14
15
|
from aspyx.di import on_init
|
|
15
16
|
from .healthcheck import HealthCheckManager, HealthStatus
|
|
16
17
|
|
|
17
18
|
from .server import Server
|
|
18
|
-
from .service import ComponentRegistry, Channel,
|
|
19
|
+
from .service import ComponentRegistry, Channel, ChannelInstances, ServiceManager, ComponentDescriptor, Component, ChannelAddress
|
|
19
20
|
|
|
20
21
|
class ConsulComponentRegistry(ComponentRegistry):
|
|
22
|
+
"""
|
|
23
|
+
A specialized registry using consul.
|
|
24
|
+
A polling mechanism is used to identify changes in the component health.
|
|
25
|
+
"""
|
|
21
26
|
# constructor
|
|
22
27
|
|
|
23
|
-
def __init__(self, port: int,
|
|
28
|
+
def __init__(self, port: int, consul: consul.Consul):
|
|
24
29
|
self.port = port
|
|
25
30
|
self.ip = Server.get_local_ip()
|
|
26
31
|
self.running = False
|
|
27
|
-
self.consul =
|
|
32
|
+
self.consul = consul
|
|
28
33
|
self.watchdog = None
|
|
29
34
|
self.interval = 5
|
|
30
35
|
self.last_index = {}
|
|
31
|
-
self.component_addresses : dict[str, dict[str,
|
|
36
|
+
self.component_addresses : dict[str, dict[str, ChannelInstances]] = {} # comp -> channel -> address
|
|
32
37
|
self.watch_channels : list[Channel] = []
|
|
38
|
+
self.watchdog_interval = 5
|
|
33
39
|
|
|
34
|
-
|
|
40
|
+
# injections
|
|
35
41
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
def make_consul(self, host: str, port: int) -> consul.Consul:
|
|
40
|
-
return consul.Consul(host=host, port=port)
|
|
42
|
+
@inject_value("consul.watchdog.interval", default=5)
|
|
43
|
+
def set_interval(self, interval):
|
|
44
|
+
self.watchdog_interval = interval
|
|
41
45
|
|
|
42
46
|
# lifecycle hooks
|
|
43
47
|
|
|
@@ -45,7 +49,6 @@ class ConsulComponentRegistry(ComponentRegistry):
|
|
|
45
49
|
def setup(self):
|
|
46
50
|
# create consul client
|
|
47
51
|
|
|
48
|
-
self.consul = self.make_consul(host=self.consul_host, port=self.consul_port)
|
|
49
52
|
self.running = True
|
|
50
53
|
|
|
51
54
|
# start thread
|
|
@@ -53,7 +56,7 @@ class ConsulComponentRegistry(ComponentRegistry):
|
|
|
53
56
|
self.watchdog = threading.Thread(target=self.watch_consul, daemon=True)
|
|
54
57
|
self.watchdog.start()
|
|
55
58
|
|
|
56
|
-
def inform_channels(self, old_address:
|
|
59
|
+
def inform_channels(self, old_address: ChannelInstances, new_address: Optional[ChannelInstances]):
|
|
57
60
|
for channel in self.watch_channels:
|
|
58
61
|
if channel.address is old_address:
|
|
59
62
|
channel.set_address(new_address)
|
|
@@ -107,19 +110,17 @@ class ConsulComponentRegistry(ComponentRegistry):
|
|
|
107
110
|
|
|
108
111
|
# time to sleep
|
|
109
112
|
|
|
110
|
-
time.sleep(
|
|
113
|
+
time.sleep(self.watchdog_interval)
|
|
111
114
|
|
|
112
115
|
@abstractmethod
|
|
113
|
-
def watch(self, channel: Channel):
|
|
116
|
+
def watch(self, channel: Channel) -> None:
|
|
114
117
|
self.watch_channels.append(channel)
|
|
115
118
|
|
|
116
119
|
#self.component_addresses[channel.component_descriptor.name] = {}
|
|
117
120
|
|
|
118
121
|
# public
|
|
119
122
|
|
|
120
|
-
def register_service(self, name, service_id, health: str, tags=None, meta=None):
|
|
121
|
-
ip = "host.docker.internal" # TODO
|
|
122
|
-
|
|
123
|
+
def register_service(self, name, service_id, health: str, tags=None, meta=None) -> None:
|
|
123
124
|
self.consul.agent.service.register(
|
|
124
125
|
name=name,
|
|
125
126
|
service_id=service_id,
|
|
@@ -128,13 +129,13 @@ class ConsulComponentRegistry(ComponentRegistry):
|
|
|
128
129
|
tags=tags or [],
|
|
129
130
|
meta=meta or {},
|
|
130
131
|
check=consul.Check().http(
|
|
131
|
-
url=f"http://{ip}:{self.port}{health}",
|
|
132
|
+
url=f"http://{self.ip}:{self.port}{health}",
|
|
132
133
|
interval="10s",
|
|
133
134
|
timeout="3s",
|
|
134
135
|
deregister="5m")
|
|
135
136
|
)
|
|
136
137
|
|
|
137
|
-
def deregister(self, descriptor: ComponentDescriptor[Component]):
|
|
138
|
+
def deregister(self, descriptor: ComponentDescriptor[Component]) -> None:
|
|
138
139
|
name = descriptor.name
|
|
139
140
|
|
|
140
141
|
service_id = f"{self.ip}:{self.port}:{name}"
|
|
@@ -149,8 +150,8 @@ class ConsulComponentRegistry(ComponentRegistry):
|
|
|
149
150
|
|
|
150
151
|
# private
|
|
151
152
|
|
|
152
|
-
def fetch_addresses(self, component: str, wait=None) -> dict[str,
|
|
153
|
-
addresses : dict[str,
|
|
153
|
+
def fetch_addresses(self, component: str, wait=None) -> dict[str, ChannelInstances]:
|
|
154
|
+
addresses : dict[str, ChannelInstances] = {} # channel name -> ServiceAddress
|
|
154
155
|
|
|
155
156
|
index, nodes = self.consul.health.service(component, passing=True, index=self.last_index.get(component, None), wait=wait)
|
|
156
157
|
self.last_index[component] = index
|
|
@@ -170,7 +171,7 @@ class ConsulComponentRegistry(ComponentRegistry):
|
|
|
170
171
|
|
|
171
172
|
address = addresses.get(channel, None)
|
|
172
173
|
if address is None:
|
|
173
|
-
address =
|
|
174
|
+
address = ChannelInstances(component=component, channel=channel_name)
|
|
174
175
|
addresses[channel] = address
|
|
175
176
|
|
|
176
177
|
address.urls.append(url)
|
|
@@ -200,7 +201,7 @@ class ConsulComponentRegistry(ComponentRegistry):
|
|
|
200
201
|
|
|
201
202
|
self.register_service(name, id, descriptor.health, tags =["component"], meta={"channels": addresses})
|
|
202
203
|
|
|
203
|
-
def get_addresses(self, descriptor: ComponentDescriptor) -> list[
|
|
204
|
+
def get_addresses(self, descriptor: ComponentDescriptor) -> list[ChannelInstances]:
|
|
204
205
|
component_addresses = self.component_addresses.get(descriptor.name, None)
|
|
205
206
|
if component_addresses is None:
|
|
206
207
|
component_addresses = self.fetch_addresses(descriptor.name)
|
aspyx_service/restchannel.py
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
"""
|
|
2
|
+
rest channel implementation
|
|
3
|
+
"""
|
|
1
4
|
import inspect
|
|
2
5
|
import re
|
|
3
6
|
from dataclasses import is_dataclass, asdict
|
|
@@ -7,7 +10,7 @@ from typing import get_type_hints, TypeVar, Annotated, Callable, get_origin, get
|
|
|
7
10
|
from pydantic import BaseModel
|
|
8
11
|
|
|
9
12
|
from aspyx.reflection import DynamicProxy, Decorators
|
|
10
|
-
from .service import
|
|
13
|
+
from .service import ServiceException, channel
|
|
11
14
|
|
|
12
15
|
T = TypeVar("T")
|
|
13
16
|
|
|
@@ -25,14 +28,26 @@ QueryParam = lambda t: Annotated[t, QueryParamMarker]
|
|
|
25
28
|
|
|
26
29
|
# decorators
|
|
27
30
|
|
|
28
|
-
def rest(url):
|
|
31
|
+
def rest(url=""):
|
|
32
|
+
"""
|
|
33
|
+
mark service interfaces to add a url prefix
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
url: prefix that will be added to all urls
|
|
37
|
+
"""
|
|
29
38
|
def decorator(cls):
|
|
30
39
|
Decorators.add(cls, rest, url)
|
|
31
40
|
|
|
32
41
|
return cls
|
|
33
42
|
return decorator
|
|
34
43
|
|
|
35
|
-
def get(url):
|
|
44
|
+
def get(url: str):
|
|
45
|
+
"""
|
|
46
|
+
methods marked with `get` will be executed by calling a http get request.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
url: the url
|
|
50
|
+
"""
|
|
36
51
|
def decorator(cls):
|
|
37
52
|
Decorators.add(cls, get, url)
|
|
38
53
|
|
|
@@ -40,7 +55,14 @@ def get(url):
|
|
|
40
55
|
return decorator
|
|
41
56
|
|
|
42
57
|
|
|
43
|
-
def post(url):
|
|
58
|
+
def post(url: str):
|
|
59
|
+
"""
|
|
60
|
+
methods marked with `post` will be executed by calling a http get request.
|
|
61
|
+
The body parameter should be marked with `Body(<param>)`
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
url: the url
|
|
65
|
+
"""
|
|
44
66
|
def decorator(cls):
|
|
45
67
|
Decorators.add(cls, post, url)
|
|
46
68
|
|
|
@@ -48,7 +70,13 @@ def post(url):
|
|
|
48
70
|
|
|
49
71
|
return decorator
|
|
50
72
|
|
|
51
|
-
def put(url):
|
|
73
|
+
def put(url: str):
|
|
74
|
+
"""
|
|
75
|
+
methods marked with `put` will be executed by calling a http put request.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
url: the url
|
|
79
|
+
"""
|
|
52
80
|
def decorator(cls):
|
|
53
81
|
Decorators.add(cls, put, url)
|
|
54
82
|
|
|
@@ -56,7 +84,13 @@ def put(url):
|
|
|
56
84
|
|
|
57
85
|
return decorator
|
|
58
86
|
|
|
59
|
-
def delete(url):
|
|
87
|
+
def delete(url: str):
|
|
88
|
+
"""
|
|
89
|
+
methods marked with `delete` will be executed by calling a http delete request.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
url: the url
|
|
93
|
+
"""
|
|
60
94
|
def decorator(cls):
|
|
61
95
|
Decorators.add(cls, delete, url)
|
|
62
96
|
|
|
@@ -64,7 +98,13 @@ def delete(url):
|
|
|
64
98
|
|
|
65
99
|
return decorator
|
|
66
100
|
|
|
67
|
-
def patch(url):
|
|
101
|
+
def patch(url: str):
|
|
102
|
+
"""
|
|
103
|
+
methods marked with `patch` will be executed by calling a http patch request.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
url: the url
|
|
107
|
+
"""
|
|
68
108
|
def decorator(cls):
|
|
69
109
|
Decorators.add(cls, patch, url)
|
|
70
110
|
|
|
@@ -72,20 +112,22 @@ def patch(url):
|
|
|
72
112
|
|
|
73
113
|
return decorator
|
|
74
114
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
115
|
@channel("rest")
|
|
79
116
|
class RestChannel(HTTPXChannel):
|
|
117
|
+
"""
|
|
118
|
+
A rest channel executes http requests as specified by the corresponding decorators and annotations,
|
|
119
|
+
"""
|
|
80
120
|
__slots__ = [
|
|
81
121
|
"signature",
|
|
82
122
|
"url_template",
|
|
83
123
|
"type",
|
|
124
|
+
"calls",
|
|
84
125
|
"return_type",
|
|
85
126
|
"path_param_names",
|
|
86
127
|
"query_param_names",
|
|
87
128
|
"body_param_name"
|
|
88
129
|
]
|
|
130
|
+
|
|
89
131
|
# local class
|
|
90
132
|
|
|
91
133
|
class Call:
|
|
@@ -133,7 +175,7 @@ class RestChannel(HTTPXChannel):
|
|
|
133
175
|
# constructor
|
|
134
176
|
|
|
135
177
|
def __init__(self):
|
|
136
|
-
super().__init__(
|
|
178
|
+
super().__init__()
|
|
137
179
|
|
|
138
180
|
self.calls : dict[Callable, RestChannel.Call] = {}
|
|
139
181
|
|
|
@@ -147,19 +189,6 @@ class RestChannel(HTTPXChannel):
|
|
|
147
189
|
|
|
148
190
|
return call
|
|
149
191
|
|
|
150
|
-
def to_dict(self, obj):
|
|
151
|
-
if obj is None:
|
|
152
|
-
return None
|
|
153
|
-
if is_dataclass(obj):
|
|
154
|
-
return asdict(obj)
|
|
155
|
-
elif isinstance(obj, BaseModel):
|
|
156
|
-
return obj.dict()
|
|
157
|
-
elif hasattr(obj, "__dict__"):
|
|
158
|
-
return vars(obj)
|
|
159
|
-
else:
|
|
160
|
-
# fallback for primitives etc.
|
|
161
|
-
return obj
|
|
162
|
-
|
|
163
192
|
# override
|
|
164
193
|
|
|
165
194
|
def invoke(self, invocation: DynamicProxy.Invocation):
|
|
@@ -196,4 +225,4 @@ class RestChannel(HTTPXChannel):
|
|
|
196
225
|
raise ServiceException(
|
|
197
226
|
f"no url for channel {self.name} for component {self.component_descriptor.name} registered")
|
|
198
227
|
except Exception as e:
|
|
199
|
-
raise ServiceException(f"communication exception {e}") from e
|
|
228
|
+
raise ServiceException(f"communication exception {e}") from e
|
aspyx_service/serialization.py
CHANGED
|
@@ -98,7 +98,7 @@ class TypeDeserializer:
|
|
|
98
98
|
return deser_union
|
|
99
99
|
|
|
100
100
|
if isinstance(typ, type) and issubclass(typ, BaseModel):
|
|
101
|
-
return
|
|
101
|
+
return typ.parse_obj
|
|
102
102
|
|
|
103
103
|
if is_dataclass(typ):
|
|
104
104
|
field_deserializers = {f.name: TypeDeserializer(f.type) for f in fields(typ)}
|
|
@@ -122,4 +122,13 @@ class TypeDeserializer:
|
|
|
122
122
|
|
|
123
123
|
@lru_cache(maxsize=512)
|
|
124
124
|
def get_deserializer(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
|
+
"""
|
|
125
134
|
return TypeDeserializer(typ)
|
aspyx_service/server.py
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
FastAPI server implementation for the aspyx service framework.
|
|
3
3
|
"""
|
|
4
4
|
import atexit
|
|
5
|
-
import functools
|
|
6
5
|
import inspect
|
|
7
6
|
import threading
|
|
8
7
|
from typing import Type, Optional, Callable
|
|
@@ -28,6 +27,9 @@ from .restchannel import get, post, put, delete, rest
|
|
|
28
27
|
|
|
29
28
|
|
|
30
29
|
class FastAPIServer(Server):
|
|
30
|
+
"""
|
|
31
|
+
A server utilizing fastapi framework.
|
|
32
|
+
"""
|
|
31
33
|
# constructor
|
|
32
34
|
|
|
33
35
|
def __init__(self, host="0.0.0.0", port=8000, **kwargs):
|
|
@@ -41,7 +43,7 @@ class FastAPIServer(Server):
|
|
|
41
43
|
self.component_registry: Optional[ComponentRegistry] = None
|
|
42
44
|
|
|
43
45
|
self.router = APIRouter()
|
|
44
|
-
self.fast_api = FastAPI(
|
|
46
|
+
self.fast_api = FastAPI()
|
|
45
47
|
|
|
46
48
|
# cache
|
|
47
49
|
|
|
@@ -82,14 +84,21 @@ class FastAPIServer(Server):
|
|
|
82
84
|
)
|
|
83
85
|
|
|
84
86
|
def start(self):
|
|
85
|
-
|
|
86
|
-
|
|
87
|
+
"""
|
|
88
|
+
start the fastapi server in a thread
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
config = uvicorn.Config(self.fast_api, host=self.host, port=self.port, log_level="debug")
|
|
92
|
+
server = uvicorn.Server(config)
|
|
87
93
|
|
|
88
|
-
|
|
89
|
-
|
|
94
|
+
thread = threading.Thread(target=server.run, daemon=True)
|
|
95
|
+
thread.start()
|
|
90
96
|
|
|
91
97
|
print(f"server started on {self.host}:{self.port}")
|
|
92
98
|
|
|
99
|
+
return thread
|
|
100
|
+
|
|
101
|
+
|
|
93
102
|
def get_deserializers(self, service: Type, method):
|
|
94
103
|
deserializers = self.deserializers.get(method, None)
|
|
95
104
|
if deserializers is None:
|
|
@@ -105,8 +114,8 @@ class FastAPIServer(Server):
|
|
|
105
114
|
|
|
106
115
|
deserializers = self.get_deserializers(type, method)
|
|
107
116
|
|
|
108
|
-
for i in
|
|
109
|
-
args[i] = deserializers[i](
|
|
117
|
+
for i, arg in enumerate(args):
|
|
118
|
+
args[i] = deserializers[i](arg)
|
|
110
119
|
|
|
111
120
|
return args
|
|
112
121
|
|
|
@@ -181,6 +190,15 @@ class FastAPIServer(Server):
|
|
|
181
190
|
self.router.get(url)(get_health_response)
|
|
182
191
|
|
|
183
192
|
def boot(self, module_type: Type) -> Environment:
|
|
193
|
+
"""
|
|
194
|
+
startup the service manager, DI framework and the fastapi server based on the supplied module
|
|
195
|
+
|
|
196
|
+
Args:
|
|
197
|
+
module_type: the module
|
|
198
|
+
|
|
199
|
+
Returns:
|
|
200
|
+
|
|
201
|
+
"""
|
|
184
202
|
# setup environment
|
|
185
203
|
|
|
186
204
|
self.environment = Environment(module_type)
|
|
@@ -194,8 +212,8 @@ class FastAPIServer(Server):
|
|
|
194
212
|
self.add_routes()
|
|
195
213
|
self.fast_api.include_router(self.router)
|
|
196
214
|
|
|
197
|
-
|
|
198
|
-
|
|
215
|
+
for route in self.fast_api.routes:
|
|
216
|
+
print(f"{route.name}: {route.path} [{route.methods}]")
|
|
199
217
|
|
|
200
218
|
# start server thread
|
|
201
219
|
|