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.
@@ -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