aspyx-service 0.9.0__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 +71 -0
- aspyx_service/channels.py +210 -0
- aspyx_service/healthcheck.py +178 -0
- aspyx_service/registries.py +227 -0
- aspyx_service/restchannel.py +199 -0
- aspyx_service/serialization.py +125 -0
- aspyx_service/server.py +213 -0
- aspyx_service/service.py +749 -0
- aspyx_service-0.9.0.dist-info/METADATA +36 -0
- aspyx_service-0.9.0.dist-info/RECORD +12 -0
- aspyx_service-0.9.0.dist-info/WHEEL +4 -0
- aspyx_service-0.9.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
import re
|
|
3
|
+
from dataclasses import is_dataclass, asdict
|
|
4
|
+
|
|
5
|
+
from typing import get_type_hints, TypeVar, Annotated, Callable, get_origin, get_args, Type
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel
|
|
8
|
+
|
|
9
|
+
from aspyx.reflection import DynamicProxy, Decorators
|
|
10
|
+
from .service import RemoteServiceException, ServiceException, channel
|
|
11
|
+
|
|
12
|
+
T = TypeVar("T")
|
|
13
|
+
|
|
14
|
+
from .channels import HTTPXChannel
|
|
15
|
+
|
|
16
|
+
class BodyMarker:
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
Body = lambda t: Annotated[t, BodyMarker]
|
|
20
|
+
|
|
21
|
+
class QueryParamMarker:
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
QueryParam = lambda t: Annotated[t, QueryParamMarker]
|
|
25
|
+
|
|
26
|
+
# decorators
|
|
27
|
+
|
|
28
|
+
def rest(url):
|
|
29
|
+
def decorator(cls):
|
|
30
|
+
Decorators.add(cls, rest, url)
|
|
31
|
+
|
|
32
|
+
return cls
|
|
33
|
+
return decorator
|
|
34
|
+
|
|
35
|
+
def get(url):
|
|
36
|
+
def decorator(cls):
|
|
37
|
+
Decorators.add(cls, get, url)
|
|
38
|
+
|
|
39
|
+
return cls
|
|
40
|
+
return decorator
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def post(url):
|
|
44
|
+
def decorator(cls):
|
|
45
|
+
Decorators.add(cls, post, url)
|
|
46
|
+
|
|
47
|
+
return cls
|
|
48
|
+
|
|
49
|
+
return decorator
|
|
50
|
+
|
|
51
|
+
def put(url):
|
|
52
|
+
def decorator(cls):
|
|
53
|
+
Decorators.add(cls, put, url)
|
|
54
|
+
|
|
55
|
+
return cls
|
|
56
|
+
|
|
57
|
+
return decorator
|
|
58
|
+
|
|
59
|
+
def delete(url):
|
|
60
|
+
def decorator(cls):
|
|
61
|
+
Decorators.add(cls, delete, url)
|
|
62
|
+
|
|
63
|
+
return cls
|
|
64
|
+
|
|
65
|
+
return decorator
|
|
66
|
+
|
|
67
|
+
def patch(url):
|
|
68
|
+
def decorator(cls):
|
|
69
|
+
Decorators.add(cls, patch, url)
|
|
70
|
+
|
|
71
|
+
return cls
|
|
72
|
+
|
|
73
|
+
return decorator
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@channel("rest")
|
|
79
|
+
class RestChannel(HTTPXChannel):
|
|
80
|
+
__slots__ = [
|
|
81
|
+
"signature",
|
|
82
|
+
"url_template",
|
|
83
|
+
"type",
|
|
84
|
+
"return_type",
|
|
85
|
+
"path_param_names",
|
|
86
|
+
"query_param_names",
|
|
87
|
+
"body_param_name"
|
|
88
|
+
]
|
|
89
|
+
# local class
|
|
90
|
+
|
|
91
|
+
class Call:
|
|
92
|
+
def __init__(self, type: Type, method : Callable):
|
|
93
|
+
self.signature = inspect.signature(method)
|
|
94
|
+
|
|
95
|
+
prefix = ""
|
|
96
|
+
if Decorators.has_decorator(type, rest):
|
|
97
|
+
prefix = Decorators.get_decorator(type, rest).args[0]
|
|
98
|
+
|
|
99
|
+
# find decorator
|
|
100
|
+
|
|
101
|
+
self.type = "get"
|
|
102
|
+
self.url_template = ""
|
|
103
|
+
|
|
104
|
+
decorators = Decorators.get_all(method)
|
|
105
|
+
|
|
106
|
+
for decorator in [get, post, put, delete, patch]:
|
|
107
|
+
descriptor = next((descriptor for descriptor in decorators if descriptor.decorator is decorator), None)
|
|
108
|
+
if descriptor is not None:
|
|
109
|
+
self.type = decorator.__name__
|
|
110
|
+
self.url_template = prefix + descriptor.args[0]
|
|
111
|
+
|
|
112
|
+
# parameters
|
|
113
|
+
|
|
114
|
+
self.path_param_names = set(re.findall(r"{(.*?)}", self.url_template))
|
|
115
|
+
|
|
116
|
+
hints = get_type_hints(method, include_extras=True)
|
|
117
|
+
|
|
118
|
+
self.body_param_name = None
|
|
119
|
+
self.query_param_names = set()
|
|
120
|
+
|
|
121
|
+
for param_name, hint in hints.items():
|
|
122
|
+
if get_origin(hint) is Annotated:
|
|
123
|
+
metadata = get_args(hint)[1:]
|
|
124
|
+
if BodyMarker in metadata:
|
|
125
|
+
self.body_param_name = param_name
|
|
126
|
+
elif QueryParamMarker in metadata:
|
|
127
|
+
self.query_param_names.add(param_name)
|
|
128
|
+
|
|
129
|
+
# return type
|
|
130
|
+
|
|
131
|
+
self.return_type = get_type_hints(method)['return']
|
|
132
|
+
|
|
133
|
+
# constructor
|
|
134
|
+
|
|
135
|
+
def __init__(self):
|
|
136
|
+
super().__init__("rest")
|
|
137
|
+
|
|
138
|
+
self.calls : dict[Callable, RestChannel.Call] = {}
|
|
139
|
+
|
|
140
|
+
# internal
|
|
141
|
+
|
|
142
|
+
def get_call(self, type: Type ,method: Callable):
|
|
143
|
+
call = self.calls.get(method, None)
|
|
144
|
+
if call is None:
|
|
145
|
+
call = RestChannel.Call(type, method)
|
|
146
|
+
self.calls[method] = call
|
|
147
|
+
|
|
148
|
+
return call
|
|
149
|
+
|
|
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
|
+
# override
|
|
164
|
+
|
|
165
|
+
def invoke(self, invocation: DynamicProxy.Invocation):
|
|
166
|
+
call = self.get_call(invocation.type, invocation.method)
|
|
167
|
+
|
|
168
|
+
bound = call.signature.bind(self,*invocation.args, **invocation.kwargs)
|
|
169
|
+
bound.apply_defaults()
|
|
170
|
+
arguments = bound.arguments
|
|
171
|
+
|
|
172
|
+
# url
|
|
173
|
+
|
|
174
|
+
url = call.url_template.format(**arguments)
|
|
175
|
+
query_params = {k: arguments[k] for k in call.query_param_names if k in arguments}
|
|
176
|
+
body = {}
|
|
177
|
+
if call.body_param_name is not None:
|
|
178
|
+
body = self.to_dict(arguments.get(call.body_param_name))
|
|
179
|
+
|
|
180
|
+
# call
|
|
181
|
+
|
|
182
|
+
try:
|
|
183
|
+
if self.client is not None:
|
|
184
|
+
result = None
|
|
185
|
+
if call.type == "get":
|
|
186
|
+
result = self.client.get(self.get_url() + url, params=query_params, timeout=30000.0).json()
|
|
187
|
+
elif call.type == "put":
|
|
188
|
+
result = self.client.put(self.get_url() + url, params=query_params, timeout=30000.0).json()
|
|
189
|
+
elif call.type == "delete":
|
|
190
|
+
result = self.client.delete(self.get_url() + url, params=query_params, timeout=30000.0).json()
|
|
191
|
+
elif call.type == "post":
|
|
192
|
+
result = self.client.post(self.get_url() + url, params=query_params, json=body, timeout=30000.0).json()
|
|
193
|
+
|
|
194
|
+
return self.get_deserializer(invocation.type, invocation.method)(result)
|
|
195
|
+
else:
|
|
196
|
+
raise ServiceException(
|
|
197
|
+
f"no url for channel {self.name} for component {self.component_descriptor.name} registered")
|
|
198
|
+
except Exception as e:
|
|
199
|
+
raise ServiceException(f"communication exception {e}") from e
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""
|
|
2
|
+
deserialization functions
|
|
3
|
+
"""
|
|
4
|
+
from dataclasses import is_dataclass, fields
|
|
5
|
+
from functools import lru_cache
|
|
6
|
+
from typing import get_origin, get_args, Union
|
|
7
|
+
|
|
8
|
+
from pydantic import BaseModel
|
|
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
|
+
class TypeDeserializer:
|
|
72
|
+
# constructor
|
|
73
|
+
|
|
74
|
+
def __init__(self, typ):
|
|
75
|
+
self.typ = typ
|
|
76
|
+
self.deserializer = self._build_deserializer(typ)
|
|
77
|
+
|
|
78
|
+
def __call__(self, value):
|
|
79
|
+
return self.deserializer(value)
|
|
80
|
+
|
|
81
|
+
# internal
|
|
82
|
+
|
|
83
|
+
def _build_deserializer(self, typ):
|
|
84
|
+
origin = get_origin(typ)
|
|
85
|
+
args = get_args(typ)
|
|
86
|
+
|
|
87
|
+
if origin is Union:
|
|
88
|
+
deserializers = [TypeDeserializer(arg) for arg in args if arg is not type(None)]
|
|
89
|
+
def deser_union(value):
|
|
90
|
+
if value is None:
|
|
91
|
+
return None
|
|
92
|
+
for d in deserializers:
|
|
93
|
+
try:
|
|
94
|
+
return d(value)
|
|
95
|
+
except Exception:
|
|
96
|
+
continue
|
|
97
|
+
return value
|
|
98
|
+
return deser_union
|
|
99
|
+
|
|
100
|
+
if isinstance(typ, type) and issubclass(typ, BaseModel):
|
|
101
|
+
return lambda v: typ.parse_obj(v)
|
|
102
|
+
|
|
103
|
+
if is_dataclass(typ):
|
|
104
|
+
field_deserializers = {f.name: TypeDeserializer(f.type) for f in fields(typ)}
|
|
105
|
+
def deser_dataclass(value):
|
|
106
|
+
return typ(**{
|
|
107
|
+
k: field_deserializers[k](v) for k, v in value.items()
|
|
108
|
+
})
|
|
109
|
+
return deser_dataclass
|
|
110
|
+
|
|
111
|
+
if origin is list:
|
|
112
|
+
item_deser = TypeDeserializer(args[0]) if args else lambda x: x
|
|
113
|
+
return lambda v: [item_deser(item) for item in v]
|
|
114
|
+
|
|
115
|
+
if origin is dict:
|
|
116
|
+
key_deser = TypeDeserializer(args[0]) if args else lambda x: x
|
|
117
|
+
val_deser = TypeDeserializer(args[1]) if len(args) > 1 else lambda x: x
|
|
118
|
+
return lambda v: {key_deser(k): val_deser(val) for k, val in v.items()}
|
|
119
|
+
|
|
120
|
+
# Fallback
|
|
121
|
+
return lambda v: v
|
|
122
|
+
|
|
123
|
+
@lru_cache(maxsize=512)
|
|
124
|
+
def get_deserializer(typ):
|
|
125
|
+
return TypeDeserializer(typ)
|
aspyx_service/server.py
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
"""
|
|
2
|
+
FastAPI server implementation for the aspyx service framework.
|
|
3
|
+
"""
|
|
4
|
+
import atexit
|
|
5
|
+
import functools
|
|
6
|
+
import inspect
|
|
7
|
+
import threading
|
|
8
|
+
from typing import Type, Optional, Callable
|
|
9
|
+
|
|
10
|
+
from fastapi.responses import JSONResponse
|
|
11
|
+
import msgpack
|
|
12
|
+
import uvicorn
|
|
13
|
+
|
|
14
|
+
from fastapi import FastAPI, APIRouter, Request as HttpRequest, Response as HttpResponse
|
|
15
|
+
|
|
16
|
+
from aspyx.di import Environment
|
|
17
|
+
from aspyx.reflection import TypeDescriptor, Decorators
|
|
18
|
+
|
|
19
|
+
from .service import ComponentRegistry
|
|
20
|
+
from .healthcheck import HealthCheckManager
|
|
21
|
+
|
|
22
|
+
from .serialization import get_deserializer
|
|
23
|
+
|
|
24
|
+
from .service import Server, ServiceManager
|
|
25
|
+
from .channels import Request, Response
|
|
26
|
+
|
|
27
|
+
from .restchannel import get, post, put, delete, rest
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class FastAPIServer(Server):
|
|
31
|
+
# constructor
|
|
32
|
+
|
|
33
|
+
def __init__(self, host="0.0.0.0", port=8000, **kwargs):
|
|
34
|
+
super().__init__()
|
|
35
|
+
|
|
36
|
+
self.host = host
|
|
37
|
+
Server.port = port
|
|
38
|
+
self.server_thread = None
|
|
39
|
+
self.environment : Optional[Environment] = None
|
|
40
|
+
self.service_manager : Optional[ServiceManager] = None
|
|
41
|
+
self.component_registry: Optional[ComponentRegistry] = None
|
|
42
|
+
|
|
43
|
+
self.router = APIRouter()
|
|
44
|
+
self.fast_api = FastAPI(host=self.host, port=Server.port, debug=True)
|
|
45
|
+
|
|
46
|
+
# cache
|
|
47
|
+
|
|
48
|
+
self.deserializers: dict[str, list[Callable]] = {}
|
|
49
|
+
|
|
50
|
+
# that's the overall dispatcher
|
|
51
|
+
|
|
52
|
+
self.router.post("/invoke")(self.invoke)
|
|
53
|
+
|
|
54
|
+
# private
|
|
55
|
+
|
|
56
|
+
def add_routes(self):
|
|
57
|
+
"""
|
|
58
|
+
add everything that looks like a http endpoint
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
# go
|
|
62
|
+
|
|
63
|
+
for descriptor in self.service_manager.descriptors.values():
|
|
64
|
+
if not descriptor.is_component() and descriptor.is_local():
|
|
65
|
+
prefix = ""
|
|
66
|
+
|
|
67
|
+
type_descriptor = TypeDescriptor.for_type(descriptor.type)
|
|
68
|
+
instance = self.environment.get(descriptor.implementation)
|
|
69
|
+
|
|
70
|
+
if type_descriptor.has_decorator(rest):
|
|
71
|
+
prefix = type_descriptor.get_decorator(rest).args[0]
|
|
72
|
+
|
|
73
|
+
for method in type_descriptor.get_methods():
|
|
74
|
+
decorator = next((decorator for decorator in Decorators.get(method.method) if decorator.decorator in [get, put, post, delete]), None)
|
|
75
|
+
if decorator is not None:
|
|
76
|
+
self.router.add_api_route(
|
|
77
|
+
path=prefix + decorator.args[0],
|
|
78
|
+
endpoint=getattr(instance, method.get_name()),
|
|
79
|
+
methods=[decorator.decorator.__name__],
|
|
80
|
+
name=f"{descriptor.get_component_descriptor().name}.{descriptor.name}.{method.get_name()}",
|
|
81
|
+
response_model=method.return_type,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
def start(self):
|
|
85
|
+
def run():
|
|
86
|
+
uvicorn.run(self.fast_api, host=self.host, port=self.port, access_log=False)
|
|
87
|
+
|
|
88
|
+
self.server_thread = threading.Thread(target=run, daemon=True)
|
|
89
|
+
self.server_thread.start()
|
|
90
|
+
|
|
91
|
+
print(f"server started on {self.host}:{self.port}")
|
|
92
|
+
|
|
93
|
+
def get_deserializers(self, service: Type, method):
|
|
94
|
+
deserializers = self.deserializers.get(method, None)
|
|
95
|
+
if deserializers is None:
|
|
96
|
+
descriptor = TypeDescriptor.for_type(service).get_method(method.__name__)
|
|
97
|
+
|
|
98
|
+
deserializers = [get_deserializer(type) for type in descriptor.param_types]
|
|
99
|
+
self.deserializers[method] = deserializers
|
|
100
|
+
|
|
101
|
+
return deserializers
|
|
102
|
+
|
|
103
|
+
def deserialize_args(self, request: Request, type: Type, method: Callable) -> list:
|
|
104
|
+
args = list(request.args)
|
|
105
|
+
|
|
106
|
+
deserializers = self.get_deserializers(type, method)
|
|
107
|
+
|
|
108
|
+
for i in range(0, len(args)):
|
|
109
|
+
args[i] = deserializers[i](args[i])
|
|
110
|
+
|
|
111
|
+
return args
|
|
112
|
+
|
|
113
|
+
async def invoke(self, http_request: HttpRequest):
|
|
114
|
+
content_type = http_request.headers.get("content-type", "")
|
|
115
|
+
|
|
116
|
+
content = "json"
|
|
117
|
+
if "application/msgpack" in content_type:
|
|
118
|
+
content = "msgpack"
|
|
119
|
+
raw_data = await http_request.body()
|
|
120
|
+
data = msgpack.unpackb(raw_data, raw=False)
|
|
121
|
+
elif "application/json" in content_type:
|
|
122
|
+
data = await http_request.json()
|
|
123
|
+
else:
|
|
124
|
+
return HttpResponse(
|
|
125
|
+
content="Unsupported Content-Type",
|
|
126
|
+
status_code=415,
|
|
127
|
+
media_type="text/plain"
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
request = Request(**data)
|
|
131
|
+
|
|
132
|
+
if content == "json":
|
|
133
|
+
return await self.dispatch(request)
|
|
134
|
+
else:
|
|
135
|
+
return HttpResponse(
|
|
136
|
+
content=msgpack.packb(await self.dispatch(request), use_bin_type=True),
|
|
137
|
+
media_type="application/msgpack"
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
async def dispatch(self, request: Request) :
|
|
141
|
+
ServiceManager.logger.debug("dispatch request %s", request.method)
|
|
142
|
+
|
|
143
|
+
# <comp>:<service>:<method>
|
|
144
|
+
|
|
145
|
+
parts = request.method.split(":")
|
|
146
|
+
|
|
147
|
+
#component = parts[0]
|
|
148
|
+
service_name = parts[1]
|
|
149
|
+
method_name = parts[2]
|
|
150
|
+
|
|
151
|
+
service_descriptor = ServiceManager.descriptors_by_name[service_name]
|
|
152
|
+
service = self.service_manager.get_service(service_descriptor.type, preferred_channel="local")
|
|
153
|
+
|
|
154
|
+
method = getattr(service, method_name)
|
|
155
|
+
|
|
156
|
+
args = self.deserialize_args(request, service_descriptor.type, method)
|
|
157
|
+
try:
|
|
158
|
+
if inspect.iscoroutinefunction(method):
|
|
159
|
+
result = await method(*args)
|
|
160
|
+
else:
|
|
161
|
+
result = method(*args)
|
|
162
|
+
|
|
163
|
+
return Response(result=result, exception=None).dict()
|
|
164
|
+
except Exception as e:
|
|
165
|
+
return Response(result=None, exception=str(e)).dict()
|
|
166
|
+
|
|
167
|
+
# override
|
|
168
|
+
|
|
169
|
+
def route(self, url: str, callable: Callable):
|
|
170
|
+
self.router.get(url)(callable)
|
|
171
|
+
|
|
172
|
+
def route_health(self, url: str, callable: Callable):
|
|
173
|
+
async def get_health_response():
|
|
174
|
+
health : HealthCheckManager.Health = await callable()
|
|
175
|
+
|
|
176
|
+
return JSONResponse(
|
|
177
|
+
status_code= self.component_registry.map_health(health),
|
|
178
|
+
content = health.to_dict()
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
self.router.get(url)(get_health_response)
|
|
182
|
+
|
|
183
|
+
def boot(self, module_type: Type) -> Environment:
|
|
184
|
+
# setup environment
|
|
185
|
+
|
|
186
|
+
self.environment = Environment(module_type)
|
|
187
|
+
self.service_manager = self.environment.get(ServiceManager)
|
|
188
|
+
self.component_registry = self.environment.get(ComponentRegistry)
|
|
189
|
+
|
|
190
|
+
self.service_manager.startup(self)
|
|
191
|
+
|
|
192
|
+
# add routes
|
|
193
|
+
|
|
194
|
+
self.add_routes()
|
|
195
|
+
self.fast_api.include_router(self.router)
|
|
196
|
+
|
|
197
|
+
#for route in self.fast_api.routes:
|
|
198
|
+
# print(f"{route.name}: {route.path} [{route.methods}]")
|
|
199
|
+
|
|
200
|
+
# start server thread
|
|
201
|
+
|
|
202
|
+
self.start()
|
|
203
|
+
|
|
204
|
+
# shutdown
|
|
205
|
+
|
|
206
|
+
def cleanup():
|
|
207
|
+
self.service_manager.shutdown()
|
|
208
|
+
|
|
209
|
+
atexit.register(cleanup)
|
|
210
|
+
|
|
211
|
+
# done
|
|
212
|
+
|
|
213
|
+
return self.environment
|