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.

@@ -0,0 +1,749 @@
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 socket
7
+ import logging
8
+ import threading
9
+ from abc import abstractmethod, ABC
10
+ from dataclasses import dataclass
11
+ from enum import Enum, auto
12
+
13
+ from typing import Type, TypeVar, Generic, Callable, Optional, cast
14
+
15
+ from aspyx.di import injectable, Environment, Providers, ClassInstanceProvider, inject_environment, order, \
16
+ Lifecycle, LifecycleCallable, InstanceProvider
17
+ from aspyx.reflection import Decorators, DynamicProxy, DecoratorDescriptor, TypeDescriptor
18
+ from aspyx.util import StringBuilder
19
+ from .healthcheck import HealthCheckManager
20
+
21
+ T = TypeVar("T")
22
+
23
+ class Service:
24
+ """
25
+ This is something like a 'tagging interface' for services.
26
+ """
27
+ pass
28
+
29
+ class ComponentStatus(Enum):
30
+ """
31
+ A component is ij one of the following status:
32
+
33
+ - VIRGIN: just constructed
34
+ - RUNNING: registered and up and running
35
+ - STOPPED: after shutdown
36
+ """
37
+ VIRGIN = auto()
38
+ RUNNING = auto()
39
+ STOPPED = auto()
40
+
41
+ class Server(ABC):
42
+ """
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,
45
+ """
46
+ port = 0
47
+
48
+ # constructor
49
+
50
+ def __init__(self):
51
+ pass
52
+
53
+ @abstractmethod
54
+ def boot(self, module_type: Type):
55
+ pass
56
+
57
+ @abstractmethod
58
+ def route(self, url : str, callable: Callable):
59
+ pass
60
+
61
+ @abstractmethod
62
+ def route_health(self, url: str, callable: Callable):
63
+ pass
64
+
65
+ @classmethod
66
+ def get_local_ip(cls):
67
+ try:
68
+ # create a dummy socket to an external address
69
+
70
+ s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
71
+ s.connect(("8.8.8.8", 80)) # Doesn't actually send data
72
+ ip = s.getsockname()[0]
73
+ s.close()
74
+
75
+ raise Exception("TODO")
76
+ #return ip
77
+ except Exception:
78
+ return "127.0.0.1" # Fallback
79
+
80
+
81
+
82
+ @dataclass
83
+ class ChannelAddress:
84
+ """
85
+ A channel address is a combination of:
86
+
87
+ - channel: the channel name
88
+ - uri: uri of the appropriate endpoint
89
+ """
90
+ channel : str
91
+ uri : str
92
+
93
+ def __str__(self):
94
+ return f"{self.channel}({self.uri})"
95
+
96
+ class Component(Service):
97
+ """
98
+ This is the base class for components.
99
+ """
100
+ @abstractmethod
101
+ def startup(self):
102
+ pass
103
+
104
+ @abstractmethod
105
+ def shutdown(self):
106
+ pass
107
+
108
+ @abstractmethod
109
+ def get_addresses(self, port: int) -> list[ChannelAddress]:
110
+ pass
111
+
112
+ @abstractmethod
113
+ def get_status(self) -> ComponentStatus:
114
+ pass
115
+
116
+ @abstractmethod
117
+ async def get_health(self) -> HealthCheckManager.Health:
118
+ pass
119
+
120
+ class AbstractComponent(Component, ABC):
121
+ """
122
+ abstract base class for components
123
+ """
124
+ # constructor
125
+
126
+ def __init__(self):
127
+ self.status = ComponentStatus.VIRGIN
128
+
129
+ def startup(self):
130
+ self.status = ComponentStatus.RUNNING
131
+
132
+ def shutdown(self):
133
+ self.status = ComponentStatus.STOPPED
134
+
135
+ def get_status(self):
136
+ return self.status
137
+
138
+ def component(name = "", description="", services: list[Type] = []):
139
+ def decorator(cls):
140
+ Decorators.add(cls, component, name, description, services)
141
+
142
+ ServiceManager.register_component(cls, services)
143
+
144
+ #Providers.register(ServiceInstanceProvider(cls)) TODO why?
145
+
146
+ return cls
147
+
148
+ return decorator
149
+
150
+ def service(name = "", description = ""):
151
+ def decorator(cls):
152
+ Decorators.add(cls, service, name, description)
153
+
154
+ Providers.register(ServiceInstanceProvider(cls))
155
+
156
+ return cls
157
+
158
+ return decorator
159
+
160
+
161
+ def health(name = ""):
162
+ def decorator(cls):
163
+ Decorators.add(cls, health, name)
164
+
165
+ return cls
166
+
167
+ return decorator
168
+
169
+ def implementation():
170
+ def decorator(cls):
171
+ Decorators.add(cls, implementation)
172
+
173
+ Providers.register(ClassInstanceProvider(cls, True, "singleton"))
174
+
175
+ ServiceManager.register_implementation(cls)
176
+
177
+ return cls
178
+
179
+ return decorator
180
+
181
+ class BaseDescriptor(Generic[T]):
182
+ """
183
+ the base class for the meta data of both services and components.
184
+ """
185
+ __slots__ = [
186
+ "name",
187
+ "description",
188
+ "type",
189
+ "implementation"
190
+ ]
191
+
192
+ # constructor
193
+
194
+ def __init__(self, type: Type[T], decorator: Callable):
195
+ self.name = type.__name__
196
+ self.description = ""
197
+ self.implementation : Type[T] = None
198
+ self.type : Type[T] = type
199
+
200
+ self.analyze_decorator(type, decorator)
201
+
202
+ def report(self, builder: StringBuilder):
203
+ pass
204
+
205
+ # internal
206
+
207
+ def analyze_decorator(self, type: Type, decorator: Callable):
208
+ descriptor = next((decorator_descriptor for decorator_descriptor in Decorators.get(type) if decorator_descriptor.decorator is decorator), None)
209
+
210
+ # name
211
+
212
+ name = descriptor.args[0]
213
+ if name is not None and name != "":
214
+ self.name = name
215
+
216
+ # description
217
+
218
+ description = descriptor.args[1]
219
+ if description is not None and description != "":
220
+ self.description = description
221
+
222
+ # public
223
+
224
+ @abstractmethod
225
+ def get_component_descriptor(self) -> ComponentDescriptor:
226
+ pass
227
+
228
+ def is_component(self) -> bool:
229
+ return False
230
+
231
+ def is_local(self):
232
+ return self.implementation is not None
233
+
234
+ class ServiceDescriptor(BaseDescriptor[T]):
235
+ """
236
+ meta data for services
237
+ """
238
+ __slots__ = [
239
+ "component_descriptor"
240
+ ]
241
+
242
+ # constructor
243
+
244
+ def __init__(self, component_descriptor: ComponentDescriptor, service_type: Type[T]):
245
+ super().__init__(service_type, service)
246
+
247
+ self.component_descriptor = component_descriptor
248
+
249
+ # override
250
+
251
+ def report(self, builder: StringBuilder):
252
+ builder.append(self.name).append("(").append(self.type.__name__).append(")")
253
+
254
+ def get_component_descriptor(self) -> ComponentDescriptor:
255
+ return self.component_descriptor
256
+
257
+ class ComponentDescriptor(BaseDescriptor[T]):
258
+ """
259
+ meta data for components
260
+ """
261
+
262
+ __slots__ = [
263
+ "services",
264
+ "health",
265
+ "addresses"
266
+ ]
267
+
268
+ # constructor
269
+
270
+ def __init__(self, component_type: Type[T], service_types: Type[T]):
271
+ super().__init__(component_type, component)
272
+
273
+ self.health = ""# Decorators.get_decorator(component_type, health).args[0]
274
+ self.services = [ServiceDescriptor(self, type) for type in service_types]
275
+ self.addresses : list[ChannelAddress] = []
276
+
277
+ # override
278
+
279
+ def report(self, builder: StringBuilder):
280
+ builder.append(self.name).append("(").append(self.type.__name__).append(")")
281
+ if self.is_local():
282
+ builder.append("\n\t").append("implementation: ").append(self.implementation.__name__)
283
+ builder.append("\n\t").append("health: ").append(self.health)
284
+ builder.append("\n\t").append("addresses: ").append(', '.join(map(str, self.addresses)))
285
+
286
+
287
+ builder.append("\n\tservices:\n")
288
+ for service in self.services:
289
+ builder.append("\t\t")
290
+ service.report(builder)
291
+ builder.append("\n")
292
+
293
+ def get_component_descriptor(self) -> ComponentDescriptor:
294
+ return self
295
+
296
+ def is_component(self) -> bool:
297
+ return True
298
+
299
+ # a resolved channel address
300
+
301
+ @dataclass()
302
+ class ServiceAddress:
303
+ component: str
304
+ channel: str
305
+ urls: list[str]
306
+
307
+ # constructor
308
+
309
+ def __init__(self, component: str, channel: str, urls: list[str] = []):
310
+ self.component = component
311
+ self.channel : str = channel
312
+ self.urls : list[str] = sorted(urls)
313
+
314
+ class ServiceException(Exception):
315
+ pass
316
+
317
+ class LocalServiceException(ServiceException):
318
+ pass
319
+
320
+ class ServiceCommunicationException(ServiceException):
321
+ pass
322
+
323
+ class RemoteServiceException(ServiceException):
324
+ pass
325
+
326
+ class Channel(DynamicProxy.InvocationHandler, ABC):
327
+ __slots__ = [
328
+ "name",
329
+ "component_descriptor",
330
+ "address"
331
+ ]
332
+
333
+ class URLSelector:
334
+ @abstractmethod
335
+ def get(self, urls: list[str]) -> str:
336
+ pass
337
+
338
+ class FirstURLSelector(URLSelector):
339
+ def get(self, urls: list[str]) -> str:
340
+ if len(urls) == 0:
341
+ raise ServiceCommunicationException("no known url")
342
+
343
+ return urls[0]
344
+
345
+ class RoundRobinURLSelector(URLSelector):
346
+ def __init__(self):
347
+ self.index = 0
348
+
349
+ def get(self, urls: list[str]) -> str:
350
+ if len(urls) > 0:
351
+ try:
352
+ return urls[self.index]
353
+ finally:
354
+ self.index = (self.index + 1) % len(urls)
355
+ else:
356
+ raise ServiceCommunicationException("no known url")
357
+
358
+
359
+ # constructor
360
+
361
+ def __init__(self, name: str):
362
+ self.name = name
363
+ self.component_descriptor : Optional[ComponentDescriptor] = None
364
+ self.address: Optional[ServiceAddress] = None
365
+ self.url_provider : Channel.URLSelector = Channel.FirstURLSelector()
366
+
367
+ # public
368
+
369
+ def customize(self):
370
+ pass
371
+
372
+ def select_round_robin(self):
373
+ self.url_provider = Channel.RoundRobinURLSelector()
374
+
375
+ def select_first_url(self):
376
+ self.url_provider = Channel.FirstURLSelector()
377
+
378
+ def get_url(self) -> str:
379
+ return self.url_provider.get(self.address.urls)
380
+
381
+ def set_address(self, address: Optional[ServiceAddress]):
382
+ self.address = address
383
+
384
+ def setup(self, component_descriptor: ComponentDescriptor, address: ServiceAddress):
385
+ self.component_descriptor = component_descriptor
386
+ self.address = address
387
+
388
+
389
+ class ComponentRegistry:
390
+ @abstractmethod
391
+ def register(self, descriptor: ComponentDescriptor[Component], addresses: list[ChannelAddress]):
392
+ pass
393
+
394
+ @abstractmethod
395
+ def deregister(self, descriptor: ComponentDescriptor[Component]):
396
+ pass
397
+
398
+ @abstractmethod
399
+ def watch(self, channel: Channel):
400
+ pass
401
+
402
+ @abstractmethod
403
+ def get_addresses(self, descriptor: ComponentDescriptor) -> list[ServiceAddress]:
404
+ pass
405
+
406
+ def map_health(self, health: HealthCheckManager.Health) -> int:
407
+ return 200
408
+
409
+ def shutdown(self):
410
+ pass
411
+
412
+ @injectable()
413
+ class ChannelManager:
414
+ factories: dict[str, Type] = {}
415
+
416
+ @classmethod
417
+ def register_channel(cls, channel: str, type: Type):
418
+ ServiceManager.logger.info("register channel %s", channel)
419
+
420
+ ChannelManager.factories[channel] = type
421
+
422
+ # constructor
423
+
424
+ def __init__(self):
425
+ self.environment = None
426
+
427
+ # lifecycle hooks
428
+
429
+ @inject_environment()
430
+ def set_environment(self, environment: Environment):
431
+ self.environment = environment
432
+
433
+ # public
434
+
435
+ def make(self, name: str, descriptor: ComponentDescriptor, address: ServiceAddress) -> Channel:
436
+ ServiceManager.logger.info("create channel %s: %s", name, self.factories.get(name).__name__)
437
+
438
+ result = self.environment.get(self.factories.get(name))
439
+
440
+ result.setup(descriptor, address)
441
+
442
+ return result
443
+
444
+ def channel(name):
445
+ def decorator(cls):
446
+ Decorators.add(cls, channel, name)
447
+
448
+ Providers.register(ClassInstanceProvider(cls, False, "request"))
449
+
450
+ ChannelManager.register_channel(name, cls)
451
+
452
+ return cls
453
+
454
+ return decorator
455
+
456
+ @dataclass(frozen=True)
457
+ class TypeAndChannel:
458
+ type: Type
459
+ channel: str
460
+
461
+ @injectable()
462
+ class ServiceManager:
463
+ # class property
464
+
465
+ logger = logging.getLogger("aspyx.service") # __name__ = module name
466
+
467
+ descriptors_by_name: dict[str, BaseDescriptor] = {}
468
+ descriptors: dict[Type, BaseDescriptor] = {}
469
+ channel_cache : dict[TypeAndChannel, Channel] = {}
470
+ proxy_cache: dict[TypeAndChannel, DynamicProxy[T]] = {}
471
+ lock: threading.Lock = threading.Lock()
472
+
473
+ instances : dict[Type, BaseDescriptor] = {}
474
+
475
+ # class methods
476
+
477
+ @classmethod
478
+ def register_implementation(cls, type: Type):
479
+ cls.logger.info("register implementation %s", type.__name__)
480
+ for base in type.mro():
481
+ if Decorators.has_decorator(base, service):
482
+ ServiceManager.descriptors[base].implementation = type
483
+ return
484
+
485
+ elif Decorators.has_decorator(base, component):
486
+ ServiceManager.descriptors[base].implementation = type
487
+ return
488
+
489
+ @classmethod
490
+ def register_component(cls, component_type: Type, services: list[Type]):
491
+ component_descriptor = ComponentDescriptor(component_type, services)
492
+
493
+ cls.logger.info("register component %s", component_descriptor.name)
494
+
495
+ ServiceManager.descriptors[component_type] = component_descriptor
496
+ ServiceManager.descriptors_by_name[component_descriptor.name] = component_descriptor
497
+
498
+ for component_service in component_descriptor.services:
499
+ ServiceManager.descriptors[component_service.type] = component_service
500
+ ServiceManager.descriptors_by_name[component_service.name] = component_service
501
+
502
+ # constructor
503
+
504
+ def __init__(self, component_registry: ComponentRegistry, channel_manager: ChannelManager):
505
+ self.component_registry = component_registry
506
+ self.channel_manager = channel_manager
507
+ self.environment = None
508
+
509
+ self.ip = Server.get_local_ip()
510
+
511
+ # internal
512
+
513
+ def report(self) -> str:
514
+ builder = StringBuilder()
515
+
516
+ for descriptor in self.descriptors.values():
517
+ if descriptor.is_component():
518
+ descriptor.report(builder)
519
+
520
+ return str(builder)
521
+
522
+ @classmethod
523
+ def get_descriptor(cls, type: Type) -> BaseDescriptor[BaseDescriptor[Component]]:
524
+ return cls.descriptors.get(type)
525
+
526
+ def get_instance(self, type: Type[T]) -> T:
527
+ instance = self.instances.get(type)
528
+ if instance is None:
529
+ ServiceManager.logger.info("create implementation %s", type.__name__)
530
+
531
+ instance = self.environment.get(type)
532
+ self.instances[type] = instance
533
+
534
+ return instance
535
+
536
+ # lifecycle
537
+
538
+ def startup(self, server: Server) -> None:
539
+ self.logger.info("startup on port %s", server.port)
540
+
541
+ for descriptor in self.descriptors.values():
542
+ if descriptor.is_component():
543
+ # register local address
544
+
545
+ if descriptor.is_local():
546
+ # create
547
+
548
+ instance = self.get_instance(descriptor.type)
549
+ descriptor.addresses = instance.get_addresses(server.port)
550
+
551
+ # fetch health
552
+
553
+ descriptor.health = Decorators.get_decorator(descriptor.implementation, health).args[0]
554
+
555
+ self.component_registry.register(descriptor.get_component_descriptor(), [ChannelAddress("local", "")])
556
+
557
+ health_name = next((decorator.args[0] for decorator in Decorators.get(descriptor.type) if decorator.decorator is health), None)
558
+
559
+ # startup
560
+
561
+ instance.startup()
562
+
563
+ # add health route
564
+
565
+ server.route_health(health_name, instance.get_health)
566
+
567
+ # register addresses
568
+
569
+ self.component_registry.register(descriptor.get_component_descriptor(), descriptor.addresses)
570
+
571
+ print(self.report())
572
+
573
+ def shutdown(self):
574
+ self.logger.info("shutdown")
575
+
576
+ for descriptor in self.descriptors.values():
577
+ if descriptor.is_component():
578
+ if descriptor.is_local():
579
+ self.get_instance(descriptor.type).shutdown()
580
+
581
+ self.component_registry.deregister(cast(ComponentDescriptor, descriptor))
582
+
583
+
584
+ @inject_environment()
585
+ def set_environment(self, environment: Environment):
586
+ self.environment = environment
587
+
588
+ # public
589
+
590
+ def find_service_address(self, component_descriptor: ComponentDescriptor, preferred_channel="") -> ServiceAddress:
591
+ addresses = self.component_registry.get_addresses(component_descriptor) # component, channel + urls
592
+ address = next((address for address in addresses if address.channel == preferred_channel), None)
593
+ if address is None:
594
+ if len(addresses) > 0:
595
+ # return the first match
596
+ address = addresses[0]
597
+ else:
598
+ raise ServiceException(f"no matching channel found for component {component_descriptor.name}")
599
+
600
+ return address
601
+
602
+ def get_service(self, service_type: Type[T], preferred_channel="") -> T:
603
+ service_descriptor = ServiceManager.get_descriptor(service_type)
604
+ component_descriptor = service_descriptor.get_component_descriptor()
605
+
606
+ ## shortcut for local implementation
607
+
608
+ if preferred_channel == "local" and service_descriptor.is_local():
609
+ return self.get_instance(service_descriptor.implementation)
610
+
611
+ # check proxy
612
+
613
+ key = TypeAndChannel(type=component_descriptor.type, channel=preferred_channel)
614
+
615
+ proxy = self.proxy_cache.get(key, None)
616
+ if proxy is None:
617
+ channel_instance = self.channel_cache.get(key, None)
618
+
619
+ if channel_instance is None:
620
+ address = self.find_service_address(component_descriptor, preferred_channel)
621
+
622
+ # again shortcut
623
+
624
+ if address.channel == "local":
625
+ return self.get_instance(service_descriptor.type)
626
+
627
+ # channel may have changed
628
+
629
+ if address.channel != preferred_channel:
630
+ key = TypeAndChannel(type=component_descriptor.type, channel=address.channel)
631
+
632
+ channel_instance = self.channel_cache.get(key, None)
633
+ if channel_instance is None:
634
+ # create channel
635
+
636
+ channel_instance = self.channel_manager.make(address.channel, component_descriptor, address)
637
+
638
+ # cache
639
+
640
+ self.channel_cache[key] = channel_instance
641
+
642
+ # and watch for changes in the addresses
643
+
644
+ self.component_registry.watch(channel_instance)
645
+
646
+ # create proxy
647
+
648
+ proxy = DynamicProxy.create(service_type, channel_instance)
649
+ self.proxy_cache[key] = proxy
650
+
651
+ return proxy
652
+
653
+ class ServiceInstanceProvider(InstanceProvider):
654
+ """
655
+ A ServiceInstanceProvider is able to create instances of services.
656
+ """
657
+
658
+ # constructor
659
+
660
+ def __init__(self, clazz : Type[T]):
661
+ super().__init__(clazz, clazz, False, "singleton")
662
+
663
+ self.service_manager = None
664
+
665
+ # implement
666
+
667
+ def get_dependencies(self) -> (list[Type],int):
668
+ return [ServiceManager], 1
669
+
670
+ def create(self, environment: Environment, *args):
671
+ if self.service_manager is None:
672
+ self.service_manager = environment.get(ServiceManager)
673
+
674
+ Environment.logger.debug("%s create service %s", self, self.type.__qualname__)
675
+
676
+ return self.service_manager.get_service(self.get_type())
677
+
678
+ def report(self) -> str:
679
+ return f"service {self.host.__name__}"
680
+
681
+ def __str__(self):
682
+ return f"ServiceInstanceProvider({self.host.__name__} -> {self.type.__name__})"
683
+
684
+ @channel("local")
685
+ class LocalChannel(Channel):
686
+ # properties
687
+
688
+ # constructor
689
+
690
+ def __init__(self, manager: ServiceManager):
691
+ super().__init__("local")
692
+
693
+ self.manager = manager
694
+ self.component = component
695
+ self.environment = None
696
+
697
+ # lifecycle hooks
698
+
699
+ @inject_environment()
700
+ def set_environment(self, environment: Environment):
701
+ self.environment = environment
702
+
703
+ # implement
704
+
705
+ def invoke(self, invocation: DynamicProxy.Invocation):
706
+ instance = self.manager.get_instance(invocation.type)
707
+
708
+ return getattr(instance, invocation.method.__name__)(*invocation.args, **invocation.kwargs)
709
+
710
+
711
+ #@injectable()
712
+ class LocalComponentRegistry(ComponentRegistry):
713
+ # constructor
714
+
715
+ def __init__(self):
716
+ self.component_channels : dict[ComponentDescriptor, list[ChannelAddress]] = {}
717
+
718
+ # implement
719
+
720
+ def register(self, descriptor: ComponentDescriptor[Component], addresses: list[ChannelAddress]):
721
+ self.component_channels[descriptor] = addresses
722
+
723
+ def deregister(self, descriptor: ComponentDescriptor[Component]):
724
+ pass
725
+
726
+ def watch(self, channel: Channel):
727
+ pass
728
+
729
+ def get_addresses(self, descriptor: ComponentDescriptor) -> list[ServiceAddress]:
730
+ return self.component_channels.get(descriptor, [])
731
+
732
+ def inject_service(preferred_channel=""):
733
+ def decorator(func):
734
+ Decorators.add(func, inject_service, preferred_channel)
735
+
736
+ return func
737
+
738
+ return decorator
739
+
740
+ @injectable()
741
+ @order(9)
742
+ class ServiceLifecycleCallable(LifecycleCallable):
743
+ def __init__(self, manager: ServiceManager):
744
+ super().__init__(inject_service, Lifecycle.ON_INJECT)
745
+
746
+ self.manager = manager
747
+
748
+ def args(self, decorator: DecoratorDescriptor, method: TypeDescriptor.MethodDescriptor, environment: Environment):
749
+ return [self.manager.get_service(method.param_types[0], preferred_channel=decorator.args[0])]