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/service.py
CHANGED
|
@@ -3,6 +3,7 @@ service management framework allowing for service discovery and transparent remo
|
|
|
3
3
|
"""
|
|
4
4
|
from __future__ import annotations
|
|
5
5
|
|
|
6
|
+
import re
|
|
6
7
|
import socket
|
|
7
8
|
import logging
|
|
8
9
|
import threading
|
|
@@ -24,11 +25,10 @@ class Service:
|
|
|
24
25
|
"""
|
|
25
26
|
This is something like a 'tagging interface' for services.
|
|
26
27
|
"""
|
|
27
|
-
pass
|
|
28
28
|
|
|
29
29
|
class ComponentStatus(Enum):
|
|
30
30
|
"""
|
|
31
|
-
A component is
|
|
31
|
+
A component is in one of the following statuses:
|
|
32
32
|
|
|
33
33
|
- VIRGIN: just constructed
|
|
34
34
|
- RUNNING: registered and up and running
|
|
@@ -41,7 +41,7 @@ class ComponentStatus(Enum):
|
|
|
41
41
|
class Server(ABC):
|
|
42
42
|
"""
|
|
43
43
|
A server is a central entity that boots a main module and initializes the ServiceManager.
|
|
44
|
-
It also is the place where http servers get initialized
|
|
44
|
+
It also is the place where http servers get initialized.
|
|
45
45
|
"""
|
|
46
46
|
port = 0
|
|
47
47
|
|
|
@@ -64,6 +64,11 @@ class Server(ABC):
|
|
|
64
64
|
|
|
65
65
|
@classmethod
|
|
66
66
|
def get_local_ip(cls):
|
|
67
|
+
"""
|
|
68
|
+
return the local ip address
|
|
69
|
+
|
|
70
|
+
Returns: the local ip address
|
|
71
|
+
"""
|
|
67
72
|
try:
|
|
68
73
|
# create a dummy socket to an external address
|
|
69
74
|
|
|
@@ -72,8 +77,7 @@ class Server(ABC):
|
|
|
72
77
|
ip = s.getsockname()[0]
|
|
73
78
|
s.close()
|
|
74
79
|
|
|
75
|
-
|
|
76
|
-
#return ip
|
|
80
|
+
return ip
|
|
77
81
|
except Exception:
|
|
78
82
|
return "127.0.0.1" # Fallback
|
|
79
83
|
|
|
@@ -98,24 +102,44 @@ class Component(Service):
|
|
|
98
102
|
This is the base class for components.
|
|
99
103
|
"""
|
|
100
104
|
@abstractmethod
|
|
101
|
-
def startup(self):
|
|
102
|
-
|
|
105
|
+
def startup(self) -> None:
|
|
106
|
+
"""
|
|
107
|
+
startup callback
|
|
108
|
+
"""
|
|
103
109
|
|
|
104
110
|
@abstractmethod
|
|
105
|
-
def shutdown(self):
|
|
106
|
-
|
|
111
|
+
def shutdown(self)-> None:
|
|
112
|
+
"""
|
|
113
|
+
shutdown callback
|
|
114
|
+
"""
|
|
107
115
|
|
|
108
116
|
@abstractmethod
|
|
109
117
|
def get_addresses(self, port: int) -> list[ChannelAddress]:
|
|
110
|
-
|
|
118
|
+
"""
|
|
119
|
+
returns a list of channel addresses that expose this components services.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
port: the port of a server hosting this component
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
list of channel addresses
|
|
126
|
+
"""
|
|
111
127
|
|
|
112
128
|
@abstractmethod
|
|
113
129
|
def get_status(self) -> ComponentStatus:
|
|
114
|
-
|
|
130
|
+
"""
|
|
131
|
+
return the component status callback
|
|
132
|
+
|
|
133
|
+
Returns: the component status
|
|
134
|
+
"""
|
|
115
135
|
|
|
116
136
|
@abstractmethod
|
|
117
137
|
async def get_health(self) -> HealthCheckManager.Health:
|
|
118
|
-
|
|
138
|
+
"""
|
|
139
|
+
return the component health
|
|
140
|
+
|
|
141
|
+
Returns: the component health
|
|
142
|
+
"""
|
|
119
143
|
|
|
120
144
|
class AbstractComponent(Component, ABC):
|
|
121
145
|
"""
|
|
@@ -126,18 +150,33 @@ class AbstractComponent(Component, ABC):
|
|
|
126
150
|
def __init__(self):
|
|
127
151
|
self.status = ComponentStatus.VIRGIN
|
|
128
152
|
|
|
129
|
-
def startup(self):
|
|
153
|
+
def startup(self) -> None:
|
|
130
154
|
self.status = ComponentStatus.RUNNING
|
|
131
155
|
|
|
132
|
-
def shutdown(self):
|
|
156
|
+
def shutdown(self) -> None:
|
|
133
157
|
self.status = ComponentStatus.STOPPED
|
|
134
158
|
|
|
135
|
-
def get_status(self):
|
|
159
|
+
def get_status(self) -> ComponentStatus:
|
|
136
160
|
return self.status
|
|
137
161
|
|
|
162
|
+
def to_snake_case(name: str) -> str:
|
|
163
|
+
return re.sub(r'(?<!^)(?=[A-Z])', '-', name).lower()
|
|
164
|
+
|
|
138
165
|
def component(name = "", description="", services: list[Type] = []):
|
|
166
|
+
"""
|
|
167
|
+
decorates component interfaces
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
name: the component name. If empty the class name converted to snake-case is used
|
|
171
|
+
description: optional description
|
|
172
|
+
services: the list of hosted services
|
|
173
|
+
"""
|
|
139
174
|
def decorator(cls):
|
|
140
|
-
|
|
175
|
+
component_name = name
|
|
176
|
+
if component_name == "":
|
|
177
|
+
component_name = to_snake_case(cls.__name__)
|
|
178
|
+
|
|
179
|
+
Decorators.add(cls, component, component_name, description, services)
|
|
141
180
|
|
|
142
181
|
ServiceManager.register_component(cls, services)
|
|
143
182
|
|
|
@@ -148,8 +187,19 @@ def component(name = "", description="", services: list[Type] = []):
|
|
|
148
187
|
return decorator
|
|
149
188
|
|
|
150
189
|
def service(name = "", description = ""):
|
|
190
|
+
"""
|
|
191
|
+
decorates service interfaces
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
name: the service name. If empty the class name converted to snake case is used
|
|
195
|
+
description: optional description
|
|
196
|
+
"""
|
|
151
197
|
def decorator(cls):
|
|
152
|
-
|
|
198
|
+
service_name = name
|
|
199
|
+
if service_name == "":
|
|
200
|
+
service_name = to_snake_case(cls.__name__)
|
|
201
|
+
|
|
202
|
+
Decorators.add(cls, service, service_name, description)
|
|
153
203
|
|
|
154
204
|
Providers.register(ServiceInstanceProvider(cls))
|
|
155
205
|
|
|
@@ -157,16 +207,24 @@ def service(name = "", description = ""):
|
|
|
157
207
|
|
|
158
208
|
return decorator
|
|
159
209
|
|
|
210
|
+
def health(endpoint = ""):
|
|
211
|
+
"""
|
|
212
|
+
specifies the health endpoint that will return the component health
|
|
160
213
|
|
|
161
|
-
|
|
214
|
+
Args:
|
|
215
|
+
endpoint: the health endpoint
|
|
216
|
+
"""
|
|
162
217
|
def decorator(cls):
|
|
163
|
-
Decorators.add(cls, health,
|
|
218
|
+
Decorators.add(cls, health, endpoint)
|
|
164
219
|
|
|
165
220
|
return cls
|
|
166
221
|
|
|
167
222
|
return decorator
|
|
168
223
|
|
|
169
224
|
def implementation():
|
|
225
|
+
"""
|
|
226
|
+
decorates service or component implementations.
|
|
227
|
+
"""
|
|
170
228
|
def decorator(cls):
|
|
171
229
|
Decorators.add(cls, implementation)
|
|
172
230
|
|
|
@@ -299,7 +357,14 @@ class ComponentDescriptor(BaseDescriptor[T]):
|
|
|
299
357
|
# a resolved channel address
|
|
300
358
|
|
|
301
359
|
@dataclass()
|
|
302
|
-
class
|
|
360
|
+
class ChannelInstances:
|
|
361
|
+
"""
|
|
362
|
+
a resolved channel address containing:
|
|
363
|
+
|
|
364
|
+
- component: the component name
|
|
365
|
+
- channel: the channel name
|
|
366
|
+
- urls: list of URLs
|
|
367
|
+
"""
|
|
303
368
|
component: str
|
|
304
369
|
channel: str
|
|
305
370
|
urls: list[str]
|
|
@@ -312,18 +377,29 @@ class ServiceAddress:
|
|
|
312
377
|
self.urls : list[str] = sorted(urls)
|
|
313
378
|
|
|
314
379
|
class ServiceException(Exception):
|
|
315
|
-
|
|
380
|
+
"""
|
|
381
|
+
base class for service exceptions
|
|
382
|
+
"""
|
|
316
383
|
|
|
317
384
|
class LocalServiceException(ServiceException):
|
|
318
|
-
|
|
385
|
+
"""
|
|
386
|
+
base class for service exceptions occurring locally
|
|
387
|
+
"""
|
|
319
388
|
|
|
320
389
|
class ServiceCommunicationException(ServiceException):
|
|
321
|
-
|
|
390
|
+
"""
|
|
391
|
+
base class for service exceptions thrown by remoting errors
|
|
392
|
+
"""
|
|
322
393
|
|
|
323
394
|
class RemoteServiceException(ServiceException):
|
|
324
|
-
|
|
395
|
+
"""
|
|
396
|
+
base class for service exceptions occurring on the server side
|
|
397
|
+
"""
|
|
325
398
|
|
|
326
399
|
class Channel(DynamicProxy.InvocationHandler, ABC):
|
|
400
|
+
"""
|
|
401
|
+
A channel is a dynamic proxy invocation handler and transparently takes care of remoting.
|
|
402
|
+
"""
|
|
327
403
|
__slots__ = [
|
|
328
404
|
"name",
|
|
329
405
|
"component_descriptor",
|
|
@@ -331,11 +407,25 @@ class Channel(DynamicProxy.InvocationHandler, ABC):
|
|
|
331
407
|
]
|
|
332
408
|
|
|
333
409
|
class URLSelector:
|
|
410
|
+
"""
|
|
411
|
+
a url selector retrieves a URL for the next remoting call.
|
|
412
|
+
"""
|
|
334
413
|
@abstractmethod
|
|
335
414
|
def get(self, urls: list[str]) -> str:
|
|
336
|
-
|
|
415
|
+
"""
|
|
416
|
+
return the next URL given a list of possible URLS
|
|
417
|
+
|
|
418
|
+
Args:
|
|
419
|
+
urls: list of possible URLS
|
|
420
|
+
|
|
421
|
+
Returns:
|
|
422
|
+
a URL
|
|
423
|
+
"""
|
|
337
424
|
|
|
338
425
|
class FirstURLSelector(URLSelector):
|
|
426
|
+
"""
|
|
427
|
+
a url selector always retrieving the first URL given a list of possible URLS
|
|
428
|
+
"""
|
|
339
429
|
def get(self, urls: list[str]) -> str:
|
|
340
430
|
if len(urls) == 0:
|
|
341
431
|
raise ServiceCommunicationException("no known url")
|
|
@@ -343,6 +433,9 @@ class Channel(DynamicProxy.InvocationHandler, ABC):
|
|
|
343
433
|
return urls[0]
|
|
344
434
|
|
|
345
435
|
class RoundRobinURLSelector(URLSelector):
|
|
436
|
+
"""
|
|
437
|
+
a url selector that picks urls sequentially given a list of possible URLS
|
|
438
|
+
"""
|
|
346
439
|
def __init__(self):
|
|
347
440
|
self.index = 0
|
|
348
441
|
|
|
@@ -358,59 +451,90 @@ class Channel(DynamicProxy.InvocationHandler, ABC):
|
|
|
358
451
|
|
|
359
452
|
# constructor
|
|
360
453
|
|
|
361
|
-
def __init__(self
|
|
362
|
-
self.name =
|
|
454
|
+
def __init__(self):
|
|
455
|
+
self.name = Decorators.get_decorator(type(self), channel).args[0]
|
|
363
456
|
self.component_descriptor : Optional[ComponentDescriptor] = None
|
|
364
|
-
self.address: Optional[
|
|
365
|
-
self.
|
|
457
|
+
self.address: Optional[ChannelInstances] = None
|
|
458
|
+
self.url_selector : Channel.URLSelector = Channel.FirstURLSelector()
|
|
366
459
|
|
|
367
460
|
# public
|
|
368
461
|
|
|
369
462
|
def customize(self):
|
|
370
463
|
pass
|
|
371
464
|
|
|
372
|
-
def select_round_robin(self):
|
|
373
|
-
|
|
465
|
+
def select_round_robin(self) -> None:
|
|
466
|
+
"""
|
|
467
|
+
enable round robin
|
|
468
|
+
"""
|
|
469
|
+
self.url_selector = Channel.RoundRobinURLSelector()
|
|
374
470
|
|
|
375
471
|
def select_first_url(self):
|
|
376
|
-
|
|
472
|
+
"""
|
|
473
|
+
pick the first URL
|
|
474
|
+
"""
|
|
475
|
+
self.url_selector = Channel.FirstURLSelector()
|
|
377
476
|
|
|
378
477
|
def get_url(self) -> str:
|
|
379
|
-
return self.
|
|
478
|
+
return self.url_selector.get(self.address.urls)
|
|
380
479
|
|
|
381
|
-
def set_address(self, address: Optional[
|
|
480
|
+
def set_address(self, address: Optional[ChannelInstances]):
|
|
382
481
|
self.address = address
|
|
383
482
|
|
|
384
|
-
def setup(self, component_descriptor: ComponentDescriptor, address:
|
|
483
|
+
def setup(self, component_descriptor: ComponentDescriptor, address: ChannelInstances):
|
|
385
484
|
self.component_descriptor = component_descriptor
|
|
386
485
|
self.address = address
|
|
387
486
|
|
|
388
487
|
|
|
389
488
|
class ComponentRegistry:
|
|
489
|
+
"""
|
|
490
|
+
A component registry keeps track of components including their health
|
|
491
|
+
"""
|
|
390
492
|
@abstractmethod
|
|
391
|
-
def register(self, descriptor: ComponentDescriptor[Component], addresses: list[ChannelAddress]):
|
|
392
|
-
|
|
493
|
+
def register(self, descriptor: ComponentDescriptor[Component], addresses: list[ChannelAddress]) -> None:
|
|
494
|
+
"""
|
|
495
|
+
register a component to the registry
|
|
496
|
+
Args:
|
|
497
|
+
descriptor: the descriptor
|
|
498
|
+
addresses: list of addresses
|
|
499
|
+
"""
|
|
393
500
|
|
|
394
501
|
@abstractmethod
|
|
395
|
-
def deregister(self, descriptor: ComponentDescriptor[Component]):
|
|
396
|
-
|
|
502
|
+
def deregister(self, descriptor: ComponentDescriptor[Component]) -> None:
|
|
503
|
+
"""
|
|
504
|
+
deregister a component from the registry
|
|
505
|
+
Args:
|
|
506
|
+
descriptor: the component descriptor
|
|
507
|
+
"""
|
|
397
508
|
|
|
398
509
|
@abstractmethod
|
|
399
|
-
def watch(self, channel: Channel):
|
|
400
|
-
|
|
510
|
+
def watch(self, channel: Channel) -> None:
|
|
511
|
+
"""
|
|
512
|
+
remember the passed channel and keep it informed about address changes
|
|
513
|
+
Args:
|
|
514
|
+
channel: a channel
|
|
515
|
+
"""
|
|
401
516
|
|
|
402
517
|
@abstractmethod
|
|
403
|
-
def get_addresses(self, descriptor: ComponentDescriptor) -> list[
|
|
404
|
-
|
|
518
|
+
def get_addresses(self, descriptor: ComponentDescriptor) -> list[ChannelInstances]:
|
|
519
|
+
"""
|
|
520
|
+
return a list of addresses that can be used to call services belonging to this component
|
|
521
|
+
|
|
522
|
+
Args:
|
|
523
|
+
descriptor: the component descriptor
|
|
524
|
+
|
|
525
|
+
Returns:
|
|
526
|
+
list of channel instances
|
|
527
|
+
"""
|
|
405
528
|
|
|
406
529
|
def map_health(self, health: HealthCheckManager.Health) -> int:
|
|
407
530
|
return 200
|
|
408
531
|
|
|
409
|
-
def shutdown(self):
|
|
410
|
-
pass
|
|
411
532
|
|
|
412
533
|
@injectable()
|
|
413
534
|
class ChannelManager:
|
|
535
|
+
"""
|
|
536
|
+
Internal factory for channels.
|
|
537
|
+
"""
|
|
414
538
|
factories: dict[str, Type] = {}
|
|
415
539
|
|
|
416
540
|
@classmethod
|
|
@@ -432,7 +556,7 @@ class ChannelManager:
|
|
|
432
556
|
|
|
433
557
|
# public
|
|
434
558
|
|
|
435
|
-
def make(self, name: str, descriptor: ComponentDescriptor, address:
|
|
559
|
+
def make(self, name: str, descriptor: ComponentDescriptor, address: ChannelInstances) -> Channel:
|
|
436
560
|
ServiceManager.logger.info("create channel %s: %s", name, self.factories.get(name).__name__)
|
|
437
561
|
|
|
438
562
|
result = self.environment.get(self.factories.get(name))
|
|
@@ -441,7 +565,13 @@ class ChannelManager:
|
|
|
441
565
|
|
|
442
566
|
return result
|
|
443
567
|
|
|
444
|
-
def channel(name):
|
|
568
|
+
def channel(name: str):
|
|
569
|
+
"""
|
|
570
|
+
this decorator is used to mark channel implementations.
|
|
571
|
+
|
|
572
|
+
Args:
|
|
573
|
+
name: the channel name
|
|
574
|
+
"""
|
|
445
575
|
def decorator(cls):
|
|
446
576
|
Decorators.add(cls, channel, name)
|
|
447
577
|
|
|
@@ -460,6 +590,9 @@ class TypeAndChannel:
|
|
|
460
590
|
|
|
461
591
|
@injectable()
|
|
462
592
|
class ServiceManager:
|
|
593
|
+
"""
|
|
594
|
+
Central class that manages services and components and is able to return proxies.
|
|
595
|
+
"""
|
|
463
596
|
# class property
|
|
464
597
|
|
|
465
598
|
logger = logging.getLogger("aspyx.service") # __name__ = module name
|
|
@@ -587,7 +720,7 @@ class ServiceManager:
|
|
|
587
720
|
|
|
588
721
|
# public
|
|
589
722
|
|
|
590
|
-
def find_service_address(self, component_descriptor: ComponentDescriptor, preferred_channel="") ->
|
|
723
|
+
def find_service_address(self, component_descriptor: ComponentDescriptor, preferred_channel="") -> ChannelInstances:
|
|
591
724
|
addresses = self.component_registry.get_addresses(component_descriptor) # component, channel + urls
|
|
592
725
|
address = next((address for address in addresses if address.channel == preferred_channel), None)
|
|
593
726
|
if address is None:
|
|
@@ -600,6 +733,16 @@ class ServiceManager:
|
|
|
600
733
|
return address
|
|
601
734
|
|
|
602
735
|
def get_service(self, service_type: Type[T], preferred_channel="") -> T:
|
|
736
|
+
"""
|
|
737
|
+
return a service proxy given a service type and preferred channel name
|
|
738
|
+
|
|
739
|
+
Args:
|
|
740
|
+
service_type: the service type
|
|
741
|
+
preferred_channel: the preferred channel name
|
|
742
|
+
|
|
743
|
+
Returns:
|
|
744
|
+
the proxy
|
|
745
|
+
"""
|
|
603
746
|
service_descriptor = ServiceManager.get_descriptor(service_type)
|
|
604
747
|
component_descriptor = service_descriptor.get_component_descriptor()
|
|
605
748
|
|
|
@@ -688,7 +831,7 @@ class LocalChannel(Channel):
|
|
|
688
831
|
# constructor
|
|
689
832
|
|
|
690
833
|
def __init__(self, manager: ServiceManager):
|
|
691
|
-
super().__init__(
|
|
834
|
+
super().__init__()
|
|
692
835
|
|
|
693
836
|
self.manager = manager
|
|
694
837
|
self.component = component
|
|
@@ -717,16 +860,16 @@ class LocalComponentRegistry(ComponentRegistry):
|
|
|
717
860
|
|
|
718
861
|
# implement
|
|
719
862
|
|
|
720
|
-
def register(self, descriptor: ComponentDescriptor[Component], addresses: list[ChannelAddress]):
|
|
863
|
+
def register(self, descriptor: ComponentDescriptor[Component], addresses: list[ChannelAddress]) -> None:
|
|
721
864
|
self.component_channels[descriptor] = addresses
|
|
722
865
|
|
|
723
|
-
def deregister(self, descriptor: ComponentDescriptor[Component]):
|
|
866
|
+
def deregister(self, descriptor: ComponentDescriptor[Component]) -> None:
|
|
724
867
|
pass
|
|
725
868
|
|
|
726
|
-
def watch(self, channel: Channel):
|
|
869
|
+
def watch(self, channel: Channel) -> None:
|
|
727
870
|
pass
|
|
728
871
|
|
|
729
|
-
def get_addresses(self, descriptor: ComponentDescriptor) -> list[
|
|
872
|
+
def get_addresses(self, descriptor: ComponentDescriptor) -> list[ChannelInstances]:
|
|
730
873
|
return self.component_channels.get(descriptor, [])
|
|
731
874
|
|
|
732
875
|
def inject_service(preferred_channel=""):
|