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/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 ij one of the following status:
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
- raise Exception("TODO")
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
- pass
105
+ def startup(self) -> None:
106
+ """
107
+ startup callback
108
+ """
103
109
 
104
110
  @abstractmethod
105
- def shutdown(self):
106
- pass
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
- pass
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
- pass
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
- pass
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
- Decorators.add(cls, component, name, description, services)
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
- Decorators.add(cls, service, name, description)
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
- def health(name = ""):
214
+ Args:
215
+ endpoint: the health endpoint
216
+ """
162
217
  def decorator(cls):
163
- Decorators.add(cls, health, name)
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 ServiceAddress:
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
- pass
380
+ """
381
+ base class for service exceptions
382
+ """
316
383
 
317
384
  class LocalServiceException(ServiceException):
318
- pass
385
+ """
386
+ base class for service exceptions occurring locally
387
+ """
319
388
 
320
389
  class ServiceCommunicationException(ServiceException):
321
- pass
390
+ """
391
+ base class for service exceptions thrown by remoting errors
392
+ """
322
393
 
323
394
  class RemoteServiceException(ServiceException):
324
- pass
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
- pass
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, name: str):
362
- self.name = 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[ServiceAddress] = None
365
- self.url_provider : Channel.URLSelector = Channel.FirstURLSelector()
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
- self.url_provider = Channel.RoundRobinURLSelector()
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
- self.url_provider = Channel.FirstURLSelector()
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.url_provider.get(self.address.urls)
478
+ return self.url_selector.get(self.address.urls)
380
479
 
381
- def set_address(self, address: Optional[ServiceAddress]):
480
+ def set_address(self, address: Optional[ChannelInstances]):
382
481
  self.address = address
383
482
 
384
- def setup(self, component_descriptor: ComponentDescriptor, address: ServiceAddress):
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
- pass
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
- pass
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
- pass
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[ServiceAddress]:
404
- pass
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: ServiceAddress) -> Channel:
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="") -> ServiceAddress:
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__("local")
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[ServiceAddress]:
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=""):