aspyx-service 0.11.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- aspyx_service/__init__.py +106 -0
- aspyx_service/authorization.py +126 -0
- aspyx_service/channels.py +445 -0
- aspyx_service/generator/__init__.py +16 -0
- aspyx_service/generator/json_schema_generator.py +197 -0
- aspyx_service/generator/openapi_generator.py +120 -0
- aspyx_service/healthcheck.py +194 -0
- aspyx_service/protobuf.py +1093 -0
- aspyx_service/registries.py +241 -0
- aspyx_service/restchannel.py +313 -0
- aspyx_service/server.py +576 -0
- aspyx_service/service.py +968 -0
- aspyx_service/session.py +136 -0
- aspyx_service-0.11.2.dist-info/METADATA +555 -0
- aspyx_service-0.11.2.dist-info/RECORD +17 -0
- aspyx_service-0.11.2.dist-info/WHEEL +4 -0
- aspyx_service-0.11.2.dist-info/licenses/LICENSE +21 -0
aspyx_service/service.py
ADDED
|
@@ -0,0 +1,968 @@
|
|
|
1
|
+
"""
|
|
2
|
+
service management framework allowing for service discovery and transparent remoting including multiple possible transport protocols.
|
|
3
|
+
"""
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import re
|
|
7
|
+
import socket
|
|
8
|
+
import logging
|
|
9
|
+
import threading
|
|
10
|
+
import typing
|
|
11
|
+
from abc import abstractmethod, ABC
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from enum import Enum, auto
|
|
14
|
+
|
|
15
|
+
from typing import Type, TypeVar, Generic, Callable, Optional, cast
|
|
16
|
+
|
|
17
|
+
from fastapi.datastructures import DefaultPlaceholder, Default
|
|
18
|
+
from httpx import Response
|
|
19
|
+
from starlette.responses import JSONResponse, PlainTextResponse
|
|
20
|
+
|
|
21
|
+
from aspyx.di import injectable, Environment, Providers, ClassInstanceProvider, inject_environment, order, \
|
|
22
|
+
Lifecycle, LifecycleCallable, InstanceProvider
|
|
23
|
+
from aspyx.di.aop.aop import ClassAspectTarget
|
|
24
|
+
from aspyx.reflection import Decorators, DynamicProxy, DecoratorDescriptor, TypeDescriptor
|
|
25
|
+
from aspyx.util import StringBuilder
|
|
26
|
+
|
|
27
|
+
from .healthcheck import HealthCheckManager, HealthStatus
|
|
28
|
+
|
|
29
|
+
T = TypeVar("T")
|
|
30
|
+
|
|
31
|
+
class Service:
|
|
32
|
+
"""
|
|
33
|
+
This is something like a 'tagging interface' for services.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
class ComponentStatus(Enum):
|
|
37
|
+
"""
|
|
38
|
+
A component is in one of the following statuses:
|
|
39
|
+
|
|
40
|
+
- VIRGIN: just constructed
|
|
41
|
+
- RUNNING: registered and up and running
|
|
42
|
+
- STOPPED: after shutdown
|
|
43
|
+
"""
|
|
44
|
+
VIRGIN = auto()
|
|
45
|
+
RUNNING = auto()
|
|
46
|
+
STOPPED = auto()
|
|
47
|
+
|
|
48
|
+
class Server(ABC):
|
|
49
|
+
"""
|
|
50
|
+
A server is a central class that boots a main module and initializes the ServiceManager.
|
|
51
|
+
It also is the place where http servers get initialized.
|
|
52
|
+
"""
|
|
53
|
+
port = 0
|
|
54
|
+
|
|
55
|
+
# constructor
|
|
56
|
+
|
|
57
|
+
def __init__(self):
|
|
58
|
+
self.environment : Optional[Environment] = None
|
|
59
|
+
self.instance = self
|
|
60
|
+
|
|
61
|
+
# public
|
|
62
|
+
|
|
63
|
+
def get(self, type: Type[T]) -> T:
|
|
64
|
+
return self.environment.get(type)
|
|
65
|
+
|
|
66
|
+
@abstractmethod
|
|
67
|
+
def add_route(self, path : str, endpoint : Callable, methods : list[str], response_class : typing.Union[Type[Response], DefaultPlaceholder] = Default(JSONResponse)):
|
|
68
|
+
pass
|
|
69
|
+
|
|
70
|
+
@abstractmethod
|
|
71
|
+
def route_health(self, url: str, callable: Callable):
|
|
72
|
+
pass
|
|
73
|
+
|
|
74
|
+
@classmethod
|
|
75
|
+
def get_local_ip(cls):
|
|
76
|
+
"""
|
|
77
|
+
return the local ip address
|
|
78
|
+
|
|
79
|
+
Returns: the local ip address
|
|
80
|
+
"""
|
|
81
|
+
try:
|
|
82
|
+
# create a dummy socket to an external address
|
|
83
|
+
|
|
84
|
+
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
85
|
+
s.connect(("8.8.8.8", 80)) # Doesn't actually send data
|
|
86
|
+
ip = s.getsockname()[0]
|
|
87
|
+
s.close()
|
|
88
|
+
|
|
89
|
+
return ip
|
|
90
|
+
except Exception:
|
|
91
|
+
return "127.0.0.1" # Fallback
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@dataclass
|
|
96
|
+
class ChannelAddress:
|
|
97
|
+
"""
|
|
98
|
+
A channel address is a combination of:
|
|
99
|
+
|
|
100
|
+
- channel: the channel name
|
|
101
|
+
- uri: uri of the appropriate endpoint
|
|
102
|
+
"""
|
|
103
|
+
channel : str
|
|
104
|
+
uri : str
|
|
105
|
+
|
|
106
|
+
def __str__(self):
|
|
107
|
+
return f"{self.channel}({self.uri})"
|
|
108
|
+
|
|
109
|
+
class Component(Service):
|
|
110
|
+
"""
|
|
111
|
+
This is the base class for components.
|
|
112
|
+
"""
|
|
113
|
+
@abstractmethod
|
|
114
|
+
def startup(self) -> None:
|
|
115
|
+
"""
|
|
116
|
+
startup callback
|
|
117
|
+
"""
|
|
118
|
+
|
|
119
|
+
@abstractmethod
|
|
120
|
+
def shutdown(self)-> None:
|
|
121
|
+
"""
|
|
122
|
+
shutdown callback
|
|
123
|
+
"""
|
|
124
|
+
|
|
125
|
+
@abstractmethod
|
|
126
|
+
def get_addresses(self, port: int) -> list[ChannelAddress]:
|
|
127
|
+
"""
|
|
128
|
+
returns a list of channel addresses that expose this component's services.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
port: the port of a server hosting this component
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
list of channel addresses
|
|
135
|
+
"""
|
|
136
|
+
|
|
137
|
+
@abstractmethod
|
|
138
|
+
def get_status(self) -> ComponentStatus:
|
|
139
|
+
"""
|
|
140
|
+
return the component status callback
|
|
141
|
+
|
|
142
|
+
Returns: the component status
|
|
143
|
+
"""
|
|
144
|
+
|
|
145
|
+
@abstractmethod
|
|
146
|
+
async def get_health(self) -> HealthCheckManager.Health:
|
|
147
|
+
"""
|
|
148
|
+
return the component health
|
|
149
|
+
|
|
150
|
+
Returns: the component health
|
|
151
|
+
"""
|
|
152
|
+
|
|
153
|
+
class AbstractComponent(Component, ABC):
|
|
154
|
+
"""
|
|
155
|
+
abstract base class for components
|
|
156
|
+
"""
|
|
157
|
+
# constructor
|
|
158
|
+
|
|
159
|
+
def __init__(self):
|
|
160
|
+
self.status = ComponentStatus.VIRGIN
|
|
161
|
+
|
|
162
|
+
def startup(self) -> None:
|
|
163
|
+
self.status = ComponentStatus.RUNNING
|
|
164
|
+
|
|
165
|
+
def shutdown(self) -> None:
|
|
166
|
+
self.status = ComponentStatus.STOPPED
|
|
167
|
+
|
|
168
|
+
def get_status(self) -> ComponentStatus:
|
|
169
|
+
return self.status
|
|
170
|
+
|
|
171
|
+
async def get_health(self) -> HealthCheckManager.Health:
|
|
172
|
+
return HealthCheckManager.Health(HealthStatus.OK)
|
|
173
|
+
|
|
174
|
+
def to_snake_case(name: str) -> str:
|
|
175
|
+
return re.sub(r'(?<!^)(?=[A-Z])', '-', name).lower()
|
|
176
|
+
|
|
177
|
+
def component(name = "", description="", services: list[Type] = []):
|
|
178
|
+
"""
|
|
179
|
+
decorates component interfaces
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
name: the component name. If empty the class name converted to snake-case is used
|
|
183
|
+
description: optional description
|
|
184
|
+
services: the list of hosted services
|
|
185
|
+
"""
|
|
186
|
+
def decorator(cls):
|
|
187
|
+
component_name = name
|
|
188
|
+
if component_name == "":
|
|
189
|
+
component_name = to_snake_case(cls.__name__)
|
|
190
|
+
|
|
191
|
+
Decorators.add(cls, component, component_name, description, services)
|
|
192
|
+
|
|
193
|
+
ServiceManager.register_component(cls, services)
|
|
194
|
+
|
|
195
|
+
#Providers.register(ServiceInstanceProvider(cls)) TODO why?
|
|
196
|
+
|
|
197
|
+
return cls
|
|
198
|
+
|
|
199
|
+
return decorator
|
|
200
|
+
|
|
201
|
+
def service(name = "", description = ""):
|
|
202
|
+
"""
|
|
203
|
+
decorates service interfaces
|
|
204
|
+
|
|
205
|
+
Args:
|
|
206
|
+
name: the service name. If empty the class name converted to snake case is used
|
|
207
|
+
description: optional description
|
|
208
|
+
"""
|
|
209
|
+
def decorator(cls):
|
|
210
|
+
service_name = name
|
|
211
|
+
if service_name == "":
|
|
212
|
+
service_name = to_snake_case(cls.__name__)
|
|
213
|
+
|
|
214
|
+
Decorators.add(cls, service, service_name, description)
|
|
215
|
+
|
|
216
|
+
Providers.register(ServiceInstanceProvider(cls))
|
|
217
|
+
|
|
218
|
+
return cls
|
|
219
|
+
|
|
220
|
+
return decorator
|
|
221
|
+
|
|
222
|
+
def health(endpoint = ""):
|
|
223
|
+
"""
|
|
224
|
+
specifies the health endpoint that will return the component health
|
|
225
|
+
|
|
226
|
+
Args:
|
|
227
|
+
endpoint: the health endpoint
|
|
228
|
+
"""
|
|
229
|
+
def decorator(cls):
|
|
230
|
+
Decorators.add(cls, health, endpoint)
|
|
231
|
+
|
|
232
|
+
return cls
|
|
233
|
+
|
|
234
|
+
return decorator
|
|
235
|
+
|
|
236
|
+
def implementation():
|
|
237
|
+
"""
|
|
238
|
+
decorates service or component implementations.
|
|
239
|
+
"""
|
|
240
|
+
def decorator(cls):
|
|
241
|
+
Decorators.add(cls, implementation)
|
|
242
|
+
|
|
243
|
+
Providers.register(ClassInstanceProvider(cls, True, "singleton"))
|
|
244
|
+
|
|
245
|
+
ServiceManager.register_implementation(cls)
|
|
246
|
+
|
|
247
|
+
return cls
|
|
248
|
+
|
|
249
|
+
return decorator
|
|
250
|
+
|
|
251
|
+
class BaseDescriptor(Generic[T]):
|
|
252
|
+
"""
|
|
253
|
+
the base class for the meta data of both services and components.
|
|
254
|
+
"""
|
|
255
|
+
__slots__ = [
|
|
256
|
+
"name",
|
|
257
|
+
"description",
|
|
258
|
+
"type",
|
|
259
|
+
"implementation"
|
|
260
|
+
]
|
|
261
|
+
|
|
262
|
+
# constructor
|
|
263
|
+
|
|
264
|
+
def __init__(self, type: Type[T], decorator: Callable):
|
|
265
|
+
self.name = type.__name__
|
|
266
|
+
self.description = ""
|
|
267
|
+
self.implementation : Type[T] = None
|
|
268
|
+
self.type : Type[T] = type
|
|
269
|
+
|
|
270
|
+
self.analyze_decorator(type, decorator)
|
|
271
|
+
|
|
272
|
+
def report(self, builder: StringBuilder):
|
|
273
|
+
pass
|
|
274
|
+
|
|
275
|
+
# internal
|
|
276
|
+
|
|
277
|
+
def analyze_decorator(self, type: Type, decorator: Callable):
|
|
278
|
+
descriptor = next((decorator_descriptor for decorator_descriptor in Decorators.get(type) if decorator_descriptor.decorator is decorator), None)
|
|
279
|
+
|
|
280
|
+
# name
|
|
281
|
+
|
|
282
|
+
name = descriptor.args[0]
|
|
283
|
+
if name is not None and name != "":
|
|
284
|
+
self.name = name
|
|
285
|
+
|
|
286
|
+
# description
|
|
287
|
+
|
|
288
|
+
description = descriptor.args[1]
|
|
289
|
+
if description is not None and description != "":
|
|
290
|
+
self.description = description
|
|
291
|
+
|
|
292
|
+
# public
|
|
293
|
+
|
|
294
|
+
@abstractmethod
|
|
295
|
+
def get_component_descriptor(self) -> ComponentDescriptor:
|
|
296
|
+
pass
|
|
297
|
+
|
|
298
|
+
def is_component(self) -> bool:
|
|
299
|
+
return False
|
|
300
|
+
|
|
301
|
+
def is_local(self):
|
|
302
|
+
return self.implementation is not None
|
|
303
|
+
|
|
304
|
+
class ServiceDescriptor(BaseDescriptor[T]):
|
|
305
|
+
"""
|
|
306
|
+
meta data for services
|
|
307
|
+
"""
|
|
308
|
+
__slots__ = [
|
|
309
|
+
"component_descriptor"
|
|
310
|
+
]
|
|
311
|
+
|
|
312
|
+
# constructor
|
|
313
|
+
|
|
314
|
+
def __init__(self, component_descriptor: ComponentDescriptor, service_type: Type[T]):
|
|
315
|
+
super().__init__(service_type, service)
|
|
316
|
+
|
|
317
|
+
self.component_descriptor = component_descriptor
|
|
318
|
+
|
|
319
|
+
# override
|
|
320
|
+
|
|
321
|
+
def report(self, builder: StringBuilder):
|
|
322
|
+
builder.append(self.name).append("(").append(self.type.__name__).append(")")
|
|
323
|
+
|
|
324
|
+
def get_component_descriptor(self) -> ComponentDescriptor:
|
|
325
|
+
return self.component_descriptor
|
|
326
|
+
|
|
327
|
+
class ComponentDescriptor(BaseDescriptor[T]):
|
|
328
|
+
"""
|
|
329
|
+
meta data for components
|
|
330
|
+
"""
|
|
331
|
+
|
|
332
|
+
__slots__ = [
|
|
333
|
+
"services",
|
|
334
|
+
"health",
|
|
335
|
+
"addresses"
|
|
336
|
+
]
|
|
337
|
+
|
|
338
|
+
# constructor
|
|
339
|
+
|
|
340
|
+
def __init__(self, component_type: Type[T], service_types: Type[T]):
|
|
341
|
+
super().__init__(component_type, component)
|
|
342
|
+
|
|
343
|
+
self.health = ""# Decorators.get_decorator(component_type, health).args[0]
|
|
344
|
+
self.services = [ServiceDescriptor(self, type) for type in service_types]
|
|
345
|
+
self.addresses : list[ChannelAddress] = []
|
|
346
|
+
|
|
347
|
+
# override
|
|
348
|
+
|
|
349
|
+
def report(self, builder: StringBuilder):
|
|
350
|
+
builder.append(self.name).append("(").append(self.type.__name__).append(")")
|
|
351
|
+
if self.is_local():
|
|
352
|
+
builder.append("\n\t").append("implementation: ").append(self.implementation.__name__)
|
|
353
|
+
builder.append("\n\t").append("health: ").append(self.health)
|
|
354
|
+
builder.append("\n\t").append("addresses: ").append(', '.join(map(str, self.addresses)))
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
builder.append("\n\tservices:\n")
|
|
358
|
+
for service in self.services:
|
|
359
|
+
builder.append("\t\t")
|
|
360
|
+
service.report(builder)
|
|
361
|
+
builder.append("\n")
|
|
362
|
+
|
|
363
|
+
def get_component_descriptor(self) -> ComponentDescriptor:
|
|
364
|
+
return self
|
|
365
|
+
|
|
366
|
+
def is_component(self) -> bool:
|
|
367
|
+
return True
|
|
368
|
+
|
|
369
|
+
# a resolved channel address
|
|
370
|
+
|
|
371
|
+
@dataclass()
|
|
372
|
+
class ChannelInstances:
|
|
373
|
+
"""
|
|
374
|
+
a resolved channel address containing:
|
|
375
|
+
|
|
376
|
+
- component: the component name
|
|
377
|
+
- channel: the channel name
|
|
378
|
+
- urls: list of URLs
|
|
379
|
+
"""
|
|
380
|
+
component: str
|
|
381
|
+
channel: str
|
|
382
|
+
urls: list[str]
|
|
383
|
+
|
|
384
|
+
# constructor
|
|
385
|
+
|
|
386
|
+
def __init__(self, component: str, channel: str, urls: list[str] = []):
|
|
387
|
+
self.component = component
|
|
388
|
+
self.channel : str = channel
|
|
389
|
+
self.urls : list[str] = sorted(urls)
|
|
390
|
+
|
|
391
|
+
class ServiceException(Exception):
|
|
392
|
+
"""
|
|
393
|
+
base class for service exceptions
|
|
394
|
+
"""
|
|
395
|
+
|
|
396
|
+
class LocalServiceException(ServiceException):
|
|
397
|
+
"""
|
|
398
|
+
base class for service exceptions occurring locally
|
|
399
|
+
"""
|
|
400
|
+
|
|
401
|
+
class ServiceCommunicationException(ServiceException):
|
|
402
|
+
"""
|
|
403
|
+
base class for service exceptions thrown by remoting errors
|
|
404
|
+
"""
|
|
405
|
+
|
|
406
|
+
class RemoteServiceException(ServiceException):
|
|
407
|
+
"""
|
|
408
|
+
base class for service exceptions occurring on the server side
|
|
409
|
+
"""
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
class AuthorizationException(ServiceException):
|
|
413
|
+
pass
|
|
414
|
+
|
|
415
|
+
class TokenException(AuthorizationException):
|
|
416
|
+
pass
|
|
417
|
+
|
|
418
|
+
class InvalidTokenException(TokenException):
|
|
419
|
+
pass
|
|
420
|
+
|
|
421
|
+
class MissingTokenException(TokenException):
|
|
422
|
+
pass
|
|
423
|
+
|
|
424
|
+
class TokenExpiredException(TokenException):
|
|
425
|
+
pass
|
|
426
|
+
|
|
427
|
+
class Channel(DynamicProxy.InvocationHandler, ABC):
|
|
428
|
+
"""
|
|
429
|
+
A channel is a dynamic proxy invocation handler and transparently takes care of remoting.
|
|
430
|
+
"""
|
|
431
|
+
__slots__ = [
|
|
432
|
+
"name",
|
|
433
|
+
"component_descriptor",
|
|
434
|
+
"address"
|
|
435
|
+
]
|
|
436
|
+
|
|
437
|
+
class URLSelector:
|
|
438
|
+
"""
|
|
439
|
+
a url selector retrieves a URL for the next remoting call.
|
|
440
|
+
"""
|
|
441
|
+
@abstractmethod
|
|
442
|
+
def get(self, urls: list[str]) -> str:
|
|
443
|
+
"""
|
|
444
|
+
return the next URL given a list of possible URLS
|
|
445
|
+
|
|
446
|
+
Args:
|
|
447
|
+
urls: list of possible URLS
|
|
448
|
+
|
|
449
|
+
Returns:
|
|
450
|
+
a URL
|
|
451
|
+
"""
|
|
452
|
+
|
|
453
|
+
class FirstURLSelector(URLSelector):
|
|
454
|
+
"""
|
|
455
|
+
a url selector always retrieving the first URL given a list of possible URLS
|
|
456
|
+
"""
|
|
457
|
+
def get(self, urls: list[str]) -> str:
|
|
458
|
+
if not urls:
|
|
459
|
+
raise ServiceCommunicationException("no known url")
|
|
460
|
+
|
|
461
|
+
return urls[0]
|
|
462
|
+
|
|
463
|
+
class RoundRobinURLSelector(URLSelector):
|
|
464
|
+
"""
|
|
465
|
+
a url selector that picks urls sequentially given a list of possible URLS
|
|
466
|
+
"""
|
|
467
|
+
def __init__(self):
|
|
468
|
+
self.index = 0
|
|
469
|
+
|
|
470
|
+
def get(self, urls: list[str]) -> str:
|
|
471
|
+
if urls:
|
|
472
|
+
try:
|
|
473
|
+
return urls[self.index]
|
|
474
|
+
finally:
|
|
475
|
+
self.index = (self.index + 1) % len(urls)
|
|
476
|
+
else:
|
|
477
|
+
raise ServiceCommunicationException("no known url")
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
# constructor
|
|
481
|
+
|
|
482
|
+
def __init__(self):
|
|
483
|
+
self.name = Decorators.get_decorator(type(self), channel).args[0]
|
|
484
|
+
self.component_descriptor : Optional[ComponentDescriptor] = None
|
|
485
|
+
self.address: Optional[ChannelInstances] = None
|
|
486
|
+
self.url_selector : Channel.URLSelector = Channel.FirstURLSelector()
|
|
487
|
+
|
|
488
|
+
self.select_round_robin()
|
|
489
|
+
|
|
490
|
+
# public
|
|
491
|
+
|
|
492
|
+
def customize(self):
|
|
493
|
+
pass
|
|
494
|
+
|
|
495
|
+
def select_round_robin(self) -> None:
|
|
496
|
+
"""
|
|
497
|
+
enable round robin
|
|
498
|
+
"""
|
|
499
|
+
self.url_selector = Channel.RoundRobinURLSelector()
|
|
500
|
+
|
|
501
|
+
def select_first_url(self):
|
|
502
|
+
"""
|
|
503
|
+
pick the first URL
|
|
504
|
+
"""
|
|
505
|
+
self.url_selector = Channel.FirstURLSelector()
|
|
506
|
+
|
|
507
|
+
def get_url(self) -> str:
|
|
508
|
+
if self.address is None:
|
|
509
|
+
raise ServiceCommunicationException(f"no url for channel {self.name} for component {self.component_descriptor.name} registered")
|
|
510
|
+
|
|
511
|
+
return self.url_selector.get(self.address.urls)
|
|
512
|
+
|
|
513
|
+
def set_address(self, address: Optional[ChannelInstances]):
|
|
514
|
+
self.address = address
|
|
515
|
+
|
|
516
|
+
def setup(self, component_descriptor: ComponentDescriptor, address: ChannelInstances):
|
|
517
|
+
self.component_descriptor = component_descriptor
|
|
518
|
+
self.address = address
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
class ComponentRegistry:
|
|
522
|
+
"""
|
|
523
|
+
A component registry keeps track of components including their health
|
|
524
|
+
"""
|
|
525
|
+
@abstractmethod
|
|
526
|
+
def register(self, descriptor: ComponentDescriptor[Component], addresses: list[ChannelAddress]) -> None:
|
|
527
|
+
"""
|
|
528
|
+
register a component to the registry
|
|
529
|
+
Args:
|
|
530
|
+
descriptor: the descriptor
|
|
531
|
+
addresses: list of addresses
|
|
532
|
+
"""
|
|
533
|
+
|
|
534
|
+
@abstractmethod
|
|
535
|
+
def deregister(self, descriptor: ComponentDescriptor[Component]) -> None:
|
|
536
|
+
"""
|
|
537
|
+
deregister a component from the registry
|
|
538
|
+
Args:
|
|
539
|
+
descriptor: the component descriptor
|
|
540
|
+
"""
|
|
541
|
+
|
|
542
|
+
@abstractmethod
|
|
543
|
+
def watch(self, channel: Channel) -> None:
|
|
544
|
+
"""
|
|
545
|
+
remember the passed channel and keep it informed about address changes
|
|
546
|
+
Args:
|
|
547
|
+
channel: a channel
|
|
548
|
+
"""
|
|
549
|
+
|
|
550
|
+
@abstractmethod
|
|
551
|
+
def get_addresses(self, descriptor: ComponentDescriptor) -> list[ChannelInstances]:
|
|
552
|
+
"""
|
|
553
|
+
return a list of addresses that can be used to call services belonging to this component
|
|
554
|
+
|
|
555
|
+
Args:
|
|
556
|
+
descriptor: the component descriptor
|
|
557
|
+
|
|
558
|
+
Returns:
|
|
559
|
+
list of channel instances
|
|
560
|
+
"""
|
|
561
|
+
|
|
562
|
+
def map_health(self, health: HealthCheckManager.Health) -> int:
|
|
563
|
+
return 200
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
@injectable()
|
|
567
|
+
class ChannelFactory:
|
|
568
|
+
"""
|
|
569
|
+
Internal factory for channels.
|
|
570
|
+
"""
|
|
571
|
+
factories: dict[str, Type] = {}
|
|
572
|
+
|
|
573
|
+
@classmethod
|
|
574
|
+
def register_channel(cls, channel: str, type: Type):
|
|
575
|
+
ServiceManager.logger.info("register channel %s", channel)
|
|
576
|
+
|
|
577
|
+
ChannelFactory.factories[channel] = type
|
|
578
|
+
|
|
579
|
+
# constructor
|
|
580
|
+
|
|
581
|
+
def __init__(self):
|
|
582
|
+
self.environment : Optional[Environment] = None
|
|
583
|
+
|
|
584
|
+
# lifecycle hooks
|
|
585
|
+
|
|
586
|
+
@inject_environment()
|
|
587
|
+
def set_environment(self, environment: Environment):
|
|
588
|
+
self.environment = environment
|
|
589
|
+
|
|
590
|
+
# public
|
|
591
|
+
|
|
592
|
+
def prepare_channel(self, server: Server, channel: str, component_descriptor: ComponentDescriptor):
|
|
593
|
+
type = self.factories[channel]
|
|
594
|
+
|
|
595
|
+
if getattr(type, "prepare", None) is not None:
|
|
596
|
+
getattr(type, "prepare", None)(server, component_descriptor)
|
|
597
|
+
|
|
598
|
+
def make(self, name: str, descriptor: ComponentDescriptor, address: ChannelInstances) -> Channel:
|
|
599
|
+
ServiceManager.logger.info("create channel %s: %s", name, self.factories.get(name).__name__)
|
|
600
|
+
|
|
601
|
+
result = self.environment.get(self.factories.get(name))
|
|
602
|
+
|
|
603
|
+
result.setup(descriptor, address)
|
|
604
|
+
|
|
605
|
+
return result
|
|
606
|
+
|
|
607
|
+
def channel(name: str):
|
|
608
|
+
"""
|
|
609
|
+
this decorator is used to mark channel implementations.
|
|
610
|
+
|
|
611
|
+
Args:
|
|
612
|
+
name: the channel name
|
|
613
|
+
"""
|
|
614
|
+
def decorator(cls):
|
|
615
|
+
Decorators.add(cls, channel, name)
|
|
616
|
+
|
|
617
|
+
Providers.register(ClassInstanceProvider(cls, False, "request"))
|
|
618
|
+
|
|
619
|
+
ChannelFactory.register_channel(name, cls)
|
|
620
|
+
|
|
621
|
+
return cls
|
|
622
|
+
|
|
623
|
+
return decorator
|
|
624
|
+
|
|
625
|
+
@dataclass(frozen=True)
|
|
626
|
+
class TypeAndChannel:
|
|
627
|
+
type: Type
|
|
628
|
+
channel: str
|
|
629
|
+
|
|
630
|
+
@injectable()
|
|
631
|
+
class ServiceManager:
|
|
632
|
+
"""
|
|
633
|
+
Central class that manages services and components and is able to return proxies.
|
|
634
|
+
"""
|
|
635
|
+
# class property
|
|
636
|
+
|
|
637
|
+
logger = logging.getLogger("aspyx.service") # __name__ = module name
|
|
638
|
+
|
|
639
|
+
descriptors_by_name: dict[str, BaseDescriptor] = {}
|
|
640
|
+
descriptors: dict[Type, BaseDescriptor] = {}
|
|
641
|
+
channel_cache : dict[TypeAndChannel, Channel] = {}
|
|
642
|
+
proxy_cache: dict[TypeAndChannel, DynamicProxy[T]] = {}
|
|
643
|
+
lock: threading.Lock = threading.Lock()
|
|
644
|
+
|
|
645
|
+
instances : dict[Type, BaseDescriptor] = {}
|
|
646
|
+
|
|
647
|
+
# class methods
|
|
648
|
+
|
|
649
|
+
@classmethod
|
|
650
|
+
def register_implementation(cls, type: Type):
|
|
651
|
+
cls.logger.info("register implementation %s", type.__name__)
|
|
652
|
+
for base in type.mro():
|
|
653
|
+
if Decorators.has_decorator(base, service):
|
|
654
|
+
ServiceManager.descriptors[base].implementation = type
|
|
655
|
+
return
|
|
656
|
+
|
|
657
|
+
elif Decorators.has_decorator(base, component):
|
|
658
|
+
ServiceManager.descriptors[base].implementation = type
|
|
659
|
+
return
|
|
660
|
+
|
|
661
|
+
@classmethod
|
|
662
|
+
def register_component(cls, component_type: Type, services: list[Type]):
|
|
663
|
+
component_descriptor = ComponentDescriptor(component_type, services)
|
|
664
|
+
|
|
665
|
+
setattr(component_type, "__descriptor__", component_descriptor)
|
|
666
|
+
|
|
667
|
+
cls.logger.info("register component %s", component_descriptor.name)
|
|
668
|
+
|
|
669
|
+
ServiceManager.descriptors[component_type] = component_descriptor
|
|
670
|
+
ServiceManager.descriptors_by_name[component_descriptor.name] = component_descriptor
|
|
671
|
+
|
|
672
|
+
for component_service in component_descriptor.services:
|
|
673
|
+
setattr(component_service.type, "__descriptor__", component_service)
|
|
674
|
+
|
|
675
|
+
ServiceManager.descriptors[component_service.type] = component_service
|
|
676
|
+
ServiceManager.descriptors_by_name[component_service.name] = component_service
|
|
677
|
+
|
|
678
|
+
# constructor
|
|
679
|
+
|
|
680
|
+
def __init__(self, component_registry: ComponentRegistry, channel_factory: ChannelFactory):
|
|
681
|
+
self.component_registry = component_registry
|
|
682
|
+
self.channel_factory = channel_factory
|
|
683
|
+
self.environment : Optional[Environment] = None
|
|
684
|
+
self.preferred_channel = ""
|
|
685
|
+
|
|
686
|
+
self.ip = Server.get_local_ip()
|
|
687
|
+
|
|
688
|
+
# internal
|
|
689
|
+
|
|
690
|
+
def report(self) -> str:
|
|
691
|
+
builder = StringBuilder()
|
|
692
|
+
|
|
693
|
+
for descriptor in self.descriptors.values():
|
|
694
|
+
if descriptor.is_component():
|
|
695
|
+
descriptor.report(builder)
|
|
696
|
+
|
|
697
|
+
return str(builder)
|
|
698
|
+
|
|
699
|
+
@classmethod
|
|
700
|
+
def get_descriptor(cls, type: Type) -> BaseDescriptor[BaseDescriptor[Component]]:
|
|
701
|
+
return cls.descriptors.get(type)
|
|
702
|
+
|
|
703
|
+
def get_instance(self, type: Type[T]) -> T:
|
|
704
|
+
instance = self.instances.get(type)
|
|
705
|
+
if instance is None:
|
|
706
|
+
ServiceManager.logger.debug("create implementation %s", type.__name__)
|
|
707
|
+
|
|
708
|
+
instance = self.environment.get(type)
|
|
709
|
+
self.instances[type] = instance
|
|
710
|
+
|
|
711
|
+
return instance
|
|
712
|
+
|
|
713
|
+
# lifecycle
|
|
714
|
+
|
|
715
|
+
|
|
716
|
+
def startup(self, server: Server) -> None:
|
|
717
|
+
self.logger.info("startup on port %s", server.port)
|
|
718
|
+
|
|
719
|
+
# add some introspection endpoints
|
|
720
|
+
|
|
721
|
+
server.add_route(path="/report", endpoint=lambda: self.report(), methods=["GET"], response_class=PlainTextResponse)
|
|
722
|
+
|
|
723
|
+
# boot components
|
|
724
|
+
|
|
725
|
+
for descriptor in self.descriptors.values():
|
|
726
|
+
if descriptor.is_component():
|
|
727
|
+
# register local address
|
|
728
|
+
|
|
729
|
+
if descriptor.is_local():
|
|
730
|
+
# create
|
|
731
|
+
|
|
732
|
+
instance = self.get_instance(descriptor.type)
|
|
733
|
+
descriptor.addresses = instance.get_addresses(server.port)
|
|
734
|
+
|
|
735
|
+
# fetch health
|
|
736
|
+
|
|
737
|
+
health_name = None
|
|
738
|
+
health_descriptor = Decorators.get_decorator(descriptor.implementation, health)
|
|
739
|
+
|
|
740
|
+
if health_descriptor is not None:
|
|
741
|
+
health_name = health_descriptor.args[0]
|
|
742
|
+
|
|
743
|
+
descriptor.health = health_name
|
|
744
|
+
|
|
745
|
+
self.component_registry.register(descriptor.get_component_descriptor(), [ChannelAddress("local", "")])
|
|
746
|
+
|
|
747
|
+
# startup
|
|
748
|
+
|
|
749
|
+
instance.startup()
|
|
750
|
+
|
|
751
|
+
# add health route
|
|
752
|
+
|
|
753
|
+
if health_name is not None:
|
|
754
|
+
server.route_health(health_name, instance.get_health)
|
|
755
|
+
|
|
756
|
+
# register addresses
|
|
757
|
+
|
|
758
|
+
for address in descriptor.addresses:
|
|
759
|
+
self.channel_factory.prepare_channel(server, address.channel, descriptor.get_component_descriptor())
|
|
760
|
+
|
|
761
|
+
self.component_registry.register(descriptor.get_component_descriptor(), descriptor.addresses)
|
|
762
|
+
|
|
763
|
+
def shutdown(self):
|
|
764
|
+
self.logger.info("shutdown")
|
|
765
|
+
|
|
766
|
+
for descriptor in self.descriptors.values():
|
|
767
|
+
if descriptor.is_component():
|
|
768
|
+
if descriptor.is_local():
|
|
769
|
+
self.get_instance(descriptor.type).shutdown()
|
|
770
|
+
|
|
771
|
+
self.component_registry.deregister(cast(ComponentDescriptor, descriptor))
|
|
772
|
+
|
|
773
|
+
|
|
774
|
+
@inject_environment()
|
|
775
|
+
def set_environment(self, environment: Environment):
|
|
776
|
+
self.environment = environment
|
|
777
|
+
|
|
778
|
+
# public
|
|
779
|
+
|
|
780
|
+
def find_service_address(self, component_descriptor: ComponentDescriptor, preferred_channel="") -> ChannelInstances:
|
|
781
|
+
addresses = self.component_registry.get_addresses(component_descriptor) # component, channel + urls
|
|
782
|
+
address = next((address for address in addresses if address.channel == preferred_channel), None)
|
|
783
|
+
if address is None:
|
|
784
|
+
if addresses:
|
|
785
|
+
# return the first match
|
|
786
|
+
address = addresses[0]
|
|
787
|
+
else:
|
|
788
|
+
raise ServiceException(f"no matching channel found for component {component_descriptor.name}")
|
|
789
|
+
|
|
790
|
+
return address
|
|
791
|
+
|
|
792
|
+
def set_preferred_channel(self, preferred_channel: str):
|
|
793
|
+
self.preferred_channel = preferred_channel
|
|
794
|
+
|
|
795
|
+
def get_service(self, service_type: Type[T], preferred_channel="") -> T:
|
|
796
|
+
"""
|
|
797
|
+
return a service proxy given a service type and preferred channel name
|
|
798
|
+
|
|
799
|
+
Args:
|
|
800
|
+
service_type: the service type
|
|
801
|
+
preferred_channel: the preferred channel name
|
|
802
|
+
|
|
803
|
+
Returns:
|
|
804
|
+
the proxy
|
|
805
|
+
"""
|
|
806
|
+
|
|
807
|
+
if len(preferred_channel) == 0:
|
|
808
|
+
preferred_channel = self.preferred_channel
|
|
809
|
+
|
|
810
|
+
service_descriptor = ServiceManager.get_descriptor(service_type)
|
|
811
|
+
component_descriptor = service_descriptor.get_component_descriptor()
|
|
812
|
+
|
|
813
|
+
## shortcut for local implementation
|
|
814
|
+
|
|
815
|
+
if preferred_channel == "local" and service_descriptor.is_local():
|
|
816
|
+
return self.get_instance(service_descriptor.implementation)
|
|
817
|
+
|
|
818
|
+
# check proxy
|
|
819
|
+
|
|
820
|
+
channel_key = TypeAndChannel(type=component_descriptor.type, channel=preferred_channel)
|
|
821
|
+
proxy_key = TypeAndChannel(type=service_type, channel=preferred_channel)
|
|
822
|
+
|
|
823
|
+
proxy = self.proxy_cache.get(proxy_key, None)
|
|
824
|
+
if proxy is None:
|
|
825
|
+
channel_instance = self.channel_cache.get(channel_key, None)
|
|
826
|
+
|
|
827
|
+
if channel_instance is None:
|
|
828
|
+
address = self.find_service_address(component_descriptor, preferred_channel)
|
|
829
|
+
|
|
830
|
+
# again shortcut
|
|
831
|
+
|
|
832
|
+
if address.channel == "local":
|
|
833
|
+
return self.get_instance(service_descriptor.type)
|
|
834
|
+
|
|
835
|
+
# channel may have changed
|
|
836
|
+
|
|
837
|
+
if address.channel != preferred_channel:
|
|
838
|
+
channel_key = TypeAndChannel(type=component_descriptor.type, channel=address.channel)
|
|
839
|
+
|
|
840
|
+
channel_instance = self.channel_cache.get(channel_key, None)
|
|
841
|
+
if channel_instance is None:
|
|
842
|
+
# create channel
|
|
843
|
+
|
|
844
|
+
channel_instance = self.channel_factory.make(address.channel, component_descriptor, address)
|
|
845
|
+
|
|
846
|
+
# cache
|
|
847
|
+
|
|
848
|
+
self.channel_cache[channel_key] = channel_instance
|
|
849
|
+
|
|
850
|
+
# and watch for changes in the addresses
|
|
851
|
+
|
|
852
|
+
self.component_registry.watch(channel_instance)
|
|
853
|
+
|
|
854
|
+
# create proxy
|
|
855
|
+
|
|
856
|
+
proxy = DynamicProxy.create(service_type, channel_instance)
|
|
857
|
+
self.proxy_cache[proxy_key] = proxy
|
|
858
|
+
|
|
859
|
+
return proxy
|
|
860
|
+
|
|
861
|
+
class ServiceInstanceProvider(InstanceProvider):
|
|
862
|
+
"""
|
|
863
|
+
A ServiceInstanceProvider is able to create instances of services.
|
|
864
|
+
"""
|
|
865
|
+
|
|
866
|
+
# constructor
|
|
867
|
+
|
|
868
|
+
def __init__(self, clazz : Type[T]):
|
|
869
|
+
super().__init__(clazz, clazz, False, "singleton")
|
|
870
|
+
|
|
871
|
+
self.service_manager = None
|
|
872
|
+
|
|
873
|
+
# implement
|
|
874
|
+
|
|
875
|
+
def get_dependencies(self) -> (list[Type],int):
|
|
876
|
+
return [ServiceManager], 1
|
|
877
|
+
|
|
878
|
+
def create(self, environment: Environment, *args):
|
|
879
|
+
if self.service_manager is None:
|
|
880
|
+
self.service_manager = environment.get(ServiceManager)
|
|
881
|
+
|
|
882
|
+
Environment.logger.debug("%s create service %s", self, self.type.__qualname__)
|
|
883
|
+
|
|
884
|
+
return self.service_manager.get_service(self.get_type())
|
|
885
|
+
|
|
886
|
+
def report(self) -> str:
|
|
887
|
+
return f"service {self.host.__name__}"
|
|
888
|
+
|
|
889
|
+
def __str__(self):
|
|
890
|
+
return f"ServiceInstanceProvider({self.host.__name__} -> {self.type.__name__})"
|
|
891
|
+
|
|
892
|
+
@channel("local")
|
|
893
|
+
class LocalChannel(Channel):
|
|
894
|
+
# properties
|
|
895
|
+
|
|
896
|
+
# constructor
|
|
897
|
+
|
|
898
|
+
def __init__(self, manager: ServiceManager):
|
|
899
|
+
super().__init__()
|
|
900
|
+
|
|
901
|
+
self.manager = manager
|
|
902
|
+
self.component = component
|
|
903
|
+
self.environment = None
|
|
904
|
+
|
|
905
|
+
# lifecycle hooks
|
|
906
|
+
|
|
907
|
+
@inject_environment()
|
|
908
|
+
def set_environment(self, environment: Environment):
|
|
909
|
+
self.environment = environment
|
|
910
|
+
|
|
911
|
+
# implement
|
|
912
|
+
|
|
913
|
+
def invoke(self, invocation: DynamicProxy.Invocation):
|
|
914
|
+
instance = self.manager.get_instance(invocation.type)
|
|
915
|
+
|
|
916
|
+
return getattr(instance, invocation.method.__name__)(*invocation.args, **invocation.kwargs)
|
|
917
|
+
|
|
918
|
+
class LocalComponentRegistry(ComponentRegistry):
|
|
919
|
+
# constructor
|
|
920
|
+
|
|
921
|
+
def __init__(self):
|
|
922
|
+
self.component_channels : dict[ComponentDescriptor, list[ChannelInstances]] = {}
|
|
923
|
+
|
|
924
|
+
# implement
|
|
925
|
+
|
|
926
|
+
def register(self, descriptor: ComponentDescriptor[Component], addresses: list[ChannelAddress]) -> None:
|
|
927
|
+
if self.component_channels.get(descriptor, None) is None:
|
|
928
|
+
self.component_channels[descriptor] = []
|
|
929
|
+
|
|
930
|
+
self.component_channels[descriptor].extend([ChannelInstances(descriptor.name, address.channel, [address.uri]) for address in addresses])
|
|
931
|
+
|
|
932
|
+
def deregister(self, descriptor: ComponentDescriptor[Component]) -> None:
|
|
933
|
+
pass
|
|
934
|
+
|
|
935
|
+
def watch(self, channel: Channel) -> None:
|
|
936
|
+
pass
|
|
937
|
+
|
|
938
|
+
def get_addresses(self, descriptor: ComponentDescriptor) -> list[ChannelInstances]:
|
|
939
|
+
return self.component_channels.get(descriptor, [])
|
|
940
|
+
|
|
941
|
+
def inject_service(preferred_channel=""):
|
|
942
|
+
def decorator(func):
|
|
943
|
+
Decorators.add(func, inject_service, preferred_channel)
|
|
944
|
+
|
|
945
|
+
return func
|
|
946
|
+
|
|
947
|
+
return decorator
|
|
948
|
+
|
|
949
|
+
@injectable()
|
|
950
|
+
@order(9)
|
|
951
|
+
class ServiceLifecycleCallable(LifecycleCallable):
|
|
952
|
+
def __init__(self, manager: ServiceManager):
|
|
953
|
+
super().__init__(inject_service, Lifecycle.ON_INJECT)
|
|
954
|
+
|
|
955
|
+
self.manager = manager
|
|
956
|
+
|
|
957
|
+
def args(self, decorator: DecoratorDescriptor, method: TypeDescriptor.MethodDescriptor, environment: Environment):
|
|
958
|
+
return [self.manager.get_service(method.param_types[0], preferred_channel=decorator.args[0])]
|
|
959
|
+
|
|
960
|
+
def component_services(component_type: Type) -> ClassAspectTarget:
|
|
961
|
+
target = ClassAspectTarget()
|
|
962
|
+
|
|
963
|
+
descriptor = TypeDescriptor.for_type(component_type)
|
|
964
|
+
|
|
965
|
+
for service_type in descriptor.get_decorator(component).args[2]:
|
|
966
|
+
target.of_type(service_type)
|
|
967
|
+
|
|
968
|
+
return target
|