aspyx 1.4.0__py3-none-any.whl → 1.4.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 might be problematic. Click here for more details.

aspyx/di/__init__.py CHANGED
@@ -1,7 +1,7 @@
1
1
  """
2
2
  This module provides dependency injection and aop capabilities for Python applications.
3
3
  """
4
- from .di import conditional, requires_class, requires_feature, DIException, AbstractCallableProcessor, LifecycleCallable, Lifecycle, Providers, Environment, ClassInstanceProvider, injectable, factory, environment, inject, order, create, on_init, on_running, on_destroy, inject_environment, Factory, PostProcessor
4
+ from .di import conditional, requires_class, requires_feature, DIException, AbstractCallableProcessor, LifecycleCallable, Lifecycle, Providers, Environment, ClassInstanceProvider, injectable, factory, module, inject, order, create, on_init, on_running, on_destroy, inject_environment, Factory, PostProcessor
5
5
 
6
6
  # import something from the subpackages, so that the decorators are executed
7
7
 
@@ -17,7 +17,7 @@ __all__ = [
17
17
  "Environment",
18
18
  "injectable",
19
19
  "factory",
20
- "environment",
20
+ "module",
21
21
  "inject",
22
22
  "create",
23
23
  "order",
aspyx/di/aop/__init__.py CHANGED
@@ -1,7 +1,20 @@
1
1
  """
2
- AOP module
2
+ The AOP module gives you the possibility to define aspects that will participate in method execution flows.
3
+
4
+ **Example**: all method executions of methods named "foo" will include a `before` aspect, that will be executed before the original method
5
+
6
+ ```python
7
+ @advice
8
+ class Advice:
9
+ @before(methods().named("foo"))
10
+ def before_call(self, invocation: Invocation):
11
+ ...
12
+
13
+ ```
14
+
15
+ Note, that this requires that both the advice and the targeted methods need to be managed by an environment.
3
16
  """
4
- from .aop import before, after, classes, around, error, advice, methods, Invocation
17
+ from .aop import before, after, classes, around, error, advice, methods, Invocation, AspectTarget
5
18
  __all__ = [
6
19
  "before",
7
20
  "after",
@@ -11,4 +24,5 @@ __all__ = [
11
24
  "classes",
12
25
  "methods",
13
26
  "Invocation",
27
+ "AspectTarget"
14
28
  ]
aspyx/di/aop/aop.py CHANGED
@@ -25,6 +25,7 @@ class AspectType(Enum):
25
25
  AspectType defines the types of aspect-oriented advice that can be applied to methods.
26
26
 
27
27
  The available types are:
28
+
28
29
  - BEFORE: Advice to be executed before the method invocation.
29
30
  - AROUND: Advice that intercepts the method invocation.
30
31
  - AFTER: Advice to be executed after the method invocation, regardless of its outcome.
@@ -98,7 +99,7 @@ class AspectTarget(ABC):
98
99
 
99
100
  # fluent
100
101
 
101
- def function(self, func):
102
+ def function(self, func) -> AspectTarget:
102
103
  self._function = func
103
104
  return self
104
105
 
@@ -107,39 +108,65 @@ class AspectTarget(ABC):
107
108
 
108
109
  return self
109
110
 
110
- def that_are_async(self):
111
+ def that_are_async(self) -> AspectTarget:
111
112
  """
112
113
  matches methods that are async
113
- :return: self
114
+
115
+ Returns:
116
+ AspectTarget: self
114
117
  """
115
118
  self._async = True
116
119
  return self
117
120
 
118
- def of_type(self, type: Type):
121
+ def of_type(self, type: Type) -> AspectTarget:
119
122
  """
120
123
  matches methods belonging to a class or classes that are subclasses of the specified type
121
- :return: self
124
+
125
+ Args:
126
+ type (Type): the type to match against
127
+
128
+ Returns:
129
+ AspectTarget: self
122
130
  """
123
131
  self.types.append(type)
124
132
  return self
125
133
 
126
- def decorated_with(self, decorator):
134
+ def decorated_with(self, decorator: Callable) -> AspectTarget:
127
135
  """
128
136
  matches methods or classes that are decorated with the specified decorator
129
- :param decorator: the decorator callable
130
- :return:
137
+
138
+ Args:
139
+ decorator (Callable): the decorator callable
140
+
141
+ Returns:
142
+ AspectTarget: self
131
143
  """
132
144
  self.decorators.append(decorator)
133
145
  return self
134
146
 
135
- def matches(self, pattern: str):
147
+ def matches(self, pattern: str) -> AspectTarget:
136
148
  """
137
149
  Matches the target against a pattern.
150
+
151
+ Args:
152
+ pattern (str): the pattern
153
+
154
+ Returns:
155
+ AspectTarget: self
138
156
  """
139
157
  self.patterns.append(re.compile(pattern))
140
158
  return self
141
159
 
142
- def named(self, name: str):
160
+ def named(self, name: str) -> AspectTarget:
161
+ """
162
+ Matches the target against a name.
163
+
164
+ Args:
165
+ name (str): the name
166
+
167
+ Returns:
168
+ AspectTarget: self
169
+ """
143
170
  self.names.append(name)
144
171
  return self
145
172
 
@@ -234,15 +261,21 @@ class MethodAspectTarget(AspectTarget):
234
261
 
235
262
  return True
236
263
 
237
- def methods():
264
+ def methods() -> AspectTarget:
238
265
  """
239
266
  Create a new AspectTarget instance to define method aspect targets.
267
+
268
+ Returns:
269
+ AspectTarget: the method target
240
270
  """
241
271
  return MethodAspectTarget()
242
272
 
243
- def classes():
273
+ def classes() -> AspectTarget:
244
274
  """
245
275
  Create a new AspectTarget instance to define class aspect targets.
276
+
277
+ Returns:
278
+ AspectTarget: the method target
246
279
  """
247
280
  return ClassAspectTarget()
248
281
 
@@ -405,7 +438,7 @@ class Invocation:
405
438
 
406
439
  def proceed(self, *args, **kwargs):
407
440
  """
408
- Proceed to the next join point in the around chain up to the original method.
441
+ Proceed to the next aspect in the around chain up to the original method.
409
442
  """
410
443
  if len(args) > 0 or len(kwargs) > 0: # as soon as we have args, we replace the current ones
411
444
  self.args = args
@@ -417,7 +450,7 @@ class Invocation:
417
450
 
418
451
  async def proceed_async(self, *args, **kwargs):
419
452
  """
420
- Proceed to the next join point in the around chain up to the original method.
453
+ Proceed to the next aspect in the around chain up to the original method.
421
454
  """
422
455
  if len(args) > 0 or len(kwargs) > 0: # as soon as we have args, we replace the current ones
423
456
  self.args = args
@@ -428,6 +461,9 @@ class Invocation:
428
461
  return await self.current_aspect.next.call_async(self)
429
462
 
430
463
  class Advices:
464
+ """
465
+ Internal utility class that collects all advice s
466
+ """
431
467
  # static data
432
468
 
433
469
  targets: list[AspectTarget] = []
@@ -443,7 +479,13 @@ class Advices:
443
479
 
444
480
  @classmethod
445
481
  def collect(cls, clazz, member, type: AspectType, environment: Environment):
446
- aspects = [FunctionAspect(environment.get(target._clazz), target._function, None) for target in Advices.targets if target._type == type and target._clazz is not clazz and environment.providers.get(target._clazz) is not None and target._matches(clazz, member)]
482
+ aspects = [
483
+ FunctionAspect(environment.get(target._clazz), target._function, None) for target in Advices.targets
484
+ if target._type == type
485
+ and target._clazz is not clazz
486
+ and environment.providers.get(target._clazz) is not None
487
+ and target._matches(clazz, member)
488
+ ]
447
489
 
448
490
  # sort according to order
449
491
 
@@ -518,8 +560,8 @@ def sanity_check(clazz: Type, name: str):
518
560
 
519
561
  def advice(cls):
520
562
  """
521
- Classes decorated with @advice are treated as advice classes.
522
- They can contain methods decorated with @before, @after, @around, or @error to define aspects.
563
+ Classes decorated with `@advice` are treated as advice classes.
564
+ They can contain methods decorated with `@before`, `@after`, `@around`, or `@error` to define aspects.
523
565
  """
524
566
  Providers.register(ClassInstanceProvider(cls, True))
525
567
 
@@ -528,7 +570,7 @@ def advice(cls):
528
570
  for name, member in TypeDescriptor.for_type(cls).methods.items():
529
571
  decorator = next((decorator for decorator in member.decorators if decorator.decorator in [before, after, around, error]), None)
530
572
  if decorator is not None:
531
- target = decorator.args[0] # ? ...?? TODO, can be multiple
573
+ target = decorator.args[0] # multiple targets are already merged in a single! check _register
532
574
  target._clazz = cls
533
575
  sanity_check(cls, name)
534
576
  Advices.targets.append(target) #??
@@ -550,7 +592,7 @@ def _register(decorator, targets: list[AspectTarget], func, aspect_type: AspectT
550
592
 
551
593
  def before(*targets: AspectTarget):
552
594
  """
553
- Methods decorated with @before will be executed before the target method is invoked.
595
+ Methods decorated with `@before` will be executed before the target method is invoked.
554
596
  """
555
597
  def decorator(func):
556
598
  _register(before, targets, func, AspectType.BEFORE)
@@ -561,7 +603,7 @@ def before(*targets: AspectTarget):
561
603
 
562
604
  def error(*targets: AspectTarget):
563
605
  """
564
- Methods decorated with @error will be executed if the target method raises an exception."""
606
+ Methods decorated with `@error` will be executed if the target method raises an exception."""
565
607
  def decorator(func):
566
608
  _register(error, targets, func, AspectType.ERROR)
567
609
 
@@ -571,7 +613,7 @@ def error(*targets: AspectTarget):
571
613
 
572
614
  def after(*targets: AspectTarget):
573
615
  """
574
- Methods decorated with @after will be executed after the target method is invoked.
616
+ Methods decorated with `@after` will be executed after the target method is invoked.
575
617
  """
576
618
  def decorator(func):
577
619
  _register(after, targets, func, AspectType.AFTER)
@@ -582,7 +624,7 @@ def after(*targets: AspectTarget):
582
624
 
583
625
  def around(*targets: AspectTarget):
584
626
  """
585
- Methods decorated with @around will be executed around the target method.
627
+ Methods decorated with `@around` will be executed around the target method.
586
628
  Every around method must accept a single parameter of type Invocation and needs to call proceed
587
629
  on this parameter to proceed to the next around method.
588
630
  """
@@ -1,7 +1,7 @@
1
1
  """
2
- Configuration value handling
2
+ This module contains functionality to read configuration values from different sources and to retrieve or inject them.
3
3
  """
4
- from .configuration import ConfigurationManager, ConfigurationSource, value
4
+ from .configuration import ConfigurationManager, ConfigurationSource, inject_value
5
5
  from .env_configuration_source import EnvConfigurationSource
6
6
  from .yaml_configuration_source import YamlConfigurationSource
7
7
 
@@ -10,5 +10,5 @@ __all__ = [
10
10
  "ConfigurationSource",
11
11
  "EnvConfigurationSource",
12
12
  "YamlConfigurationSource",
13
- "value"
13
+ "inject_value"
14
14
  ]
@@ -66,10 +66,12 @@ class ConfigurationManager:
66
66
  def get(self, path: str, type: Type[T], default : Optional[T]=None) -> T:
67
67
  """
68
68
  Retrieve a configuration value by path and type, with optional coercion.
69
- Arguments:
69
+
70
+ Args:
70
71
  path (str): The path to the configuration value, e.g. "database.host".
71
72
  type (Type[T]): The expected type.
72
73
  default (Optional[T]): The default value to return if the path is not found.
74
+
73
75
  Returns:
74
76
  T: The configuration value coerced to the specified type, or the default value if not found.
75
77
  """
@@ -119,17 +121,17 @@ class ConfigurationSource(ABC):
119
121
 
120
122
  # decorator
121
123
 
122
- def value(key: str, default=None):
124
+ def inject_value(key: str, default=None):
123
125
  """
124
126
  Decorator to inject a configuration value into a method.
125
127
 
126
- Arguments:
128
+ Args:
127
129
  key (str): The configuration key to inject.
128
130
  default: The default value to use if the key is not found.
129
131
 
130
132
  """
131
133
  def decorator(func):
132
- Decorators.add(func, value, key, default)
134
+ Decorators.add(func, inject_value, key, default)
133
135
 
134
136
  return func
135
137
 
@@ -139,7 +141,7 @@ def value(key: str, default=None):
139
141
  @order(9)
140
142
  class ConfigurationLifecycleCallable(LifecycleCallable):
141
143
  def __init__(self, manager: ConfigurationManager):
142
- super().__init__(value, Lifecycle.ON_INJECT)
144
+ super().__init__(inject_value, Lifecycle.ON_INJECT)
143
145
 
144
146
  self.manager = manager
145
147
 
aspyx/di/di.py CHANGED
@@ -62,33 +62,76 @@ class DIRuntimeException(DIException):
62
62
 
63
63
  class AbstractInstanceProvider(ABC, Generic[T]):
64
64
  """
65
- Interface for instance providers.
65
+ An AbstractInstanceProvider is responsible to create instances.
66
66
  """
67
67
  @abstractmethod
68
68
  def get_module(self) -> str:
69
- pass
69
+ """
70
+ return the module name of the provider
71
+
72
+ Returns:
73
+ str: the module name of the provider
74
+ """
70
75
 
71
76
  def get_host(self) -> Type[T]:
77
+ """
78
+ return the class which is responsible for creation ( e.g. the injectable class )
79
+
80
+ Returns:
81
+ Type[T]: the class which is responsible for creation
82
+ """
72
83
  return type(self)
73
84
 
74
85
  @abstractmethod
75
86
  def get_type(self) -> Type[T]:
76
- pass
87
+ """
88
+ return the type of the created instance
89
+
90
+ Returns:
91
+ Type[T: the type]
92
+ """
77
93
 
78
94
  @abstractmethod
79
95
  def is_eager(self) -> bool:
80
- pass
96
+ """
97
+ return True, if the provider will eagerly construct instances
98
+
99
+ Returns:
100
+ bool: eager flag
101
+ """
81
102
 
82
103
  @abstractmethod
83
104
  def get_scope(self) -> str:
84
- pass
105
+ """
106
+ return the scope name
107
+
108
+ Returns:
109
+ str: the scope name
110
+ """
111
+
85
112
 
86
113
  def get_dependencies(self) -> (list[Type],int):
114
+ """
115
+ return the types that i depend on ( for constructor or setter injection ).
116
+ The second tuple element is the number of parameters that a construction injection will require
117
+
118
+ Returns:
119
+ (list[Type],int): the type array and the number of parameters
120
+ """
87
121
  return [],1
88
122
 
89
123
  @abstractmethod
90
- def create(self, environment: Environment, *args):
91
- pass
124
+ def create(self, environment: Environment, *args) -> T:
125
+ """
126
+ Create a new instance.
127
+
128
+ Args:
129
+ environment: the Environment
130
+ *args: the required arguments
131
+
132
+ Returns:
133
+ T: the instance
134
+ """
92
135
 
93
136
  def report(self) -> str:
94
137
  return str(self)
@@ -163,7 +206,7 @@ class SingletonScopeInstanceProvider(InstanceProvider):
163
206
 
164
207
  class EnvironmentScopeInstanceProvider(InstanceProvider):
165
208
  def __init__(self):
166
- super().__init__(SingletonScopeInstanceProvider, SingletonScope, False, "request") # TODO?
209
+ super().__init__(SingletonScopeInstanceProvider, SingletonScope, False, "request")
167
210
 
168
211
  def create(self, environment: Environment, *args):
169
212
  return EnvironmentScope()
@@ -362,7 +405,7 @@ class ClassInstanceProvider(InstanceProvider):
362
405
  for param in method.param_types:
363
406
  types.append(param)
364
407
 
365
- return (types, self.params)
408
+ return types, self.params
366
409
 
367
410
  def create(self, environment: Environment, *args):
368
411
  Environment.logger.debug("%s create class %s", self, self.type.__qualname__)
@@ -449,6 +492,12 @@ class FactoryInstanceProvider(InstanceProvider):
449
492
  class Lifecycle(Enum):
450
493
  """
451
494
  This enum defines the lifecycle phases that can be processed by lifecycle processors.
495
+ Phases are:
496
+
497
+ - ON_INJECT
498
+ - ON_INIT
499
+ - ON_RUNNING
500
+ - ON_DESTROY
452
501
  """
453
502
 
454
503
  __slots__ = []
@@ -740,6 +789,10 @@ def injectable(eager=True, scope="singleton"):
740
789
  def factory(eager=True, scope="singleton"):
741
790
  """
742
791
  Decorator that needs to be used on a class that implements the Factory interface.
792
+
793
+ Args:
794
+ eager (bool): If True, the corresponding object will be created eagerly when the environment is created.
795
+ scope (str): The scope of the factory, e.g. "singleton", "request", "environment".
743
796
  """
744
797
  def decorator(cls):
745
798
  Decorators.add(cls, factory)
@@ -754,6 +807,10 @@ def factory(eager=True, scope="singleton"):
754
807
  def create(eager=True, scope="singleton"):
755
808
  """
756
809
  Any method annotated with @create will be registered as a factory method.
810
+
811
+ Args:
812
+ eager (bool): If True, the corresponding object will be created eagerly when the environment is created.
813
+ scope (str): The scope of the factory, e.g. "singleton", "request", "environment".
757
814
  """
758
815
  def decorator(func):
759
816
  Decorators.add(func, create, eager, scope)
@@ -763,7 +820,7 @@ def create(eager=True, scope="singleton"):
763
820
 
764
821
  def on_init():
765
822
  """
766
- Methods annotated with @on_init will be called when the instance is created."""
823
+ Methods annotated with `@on_init` will be called when the instance is created."""
767
824
  def decorator(func):
768
825
  Decorators.add(func, on_init)
769
826
  return func
@@ -772,7 +829,7 @@ def on_init():
772
829
 
773
830
  def on_running():
774
831
  """
775
- Methods annotated with @on_running will be called when the container up and running."""
832
+ Methods annotated with `@on_running` will be called when the container up and running."""
776
833
  def decorator(func):
777
834
  Decorators.add(func, on_running)
778
835
  return func
@@ -781,7 +838,7 @@ def on_running():
781
838
 
782
839
  def on_destroy():
783
840
  """
784
- Methods annotated with @on_destroy will be called when the instance is destroyed.
841
+ Methods annotated with `@on_destroy` will be called when the instance is destroyed.
785
842
  """
786
843
  def decorator(func):
787
844
  Decorators.add(func, on_destroy)
@@ -789,18 +846,19 @@ def on_destroy():
789
846
 
790
847
  return decorator
791
848
 
792
- def environment(imports: Optional[list[Type]] = None):
849
+ def module(imports: Optional[list[Type]] = None):
793
850
  """
794
- This annotation is used to mark classes that control the set of injectables that will be managed based on their location
795
- relative to the module of the class. All @injectable s and @factory s that are located in the same or any sub-module will
851
+ This annotation is used to mark classes that control the discovery process of injectables based on their location
852
+ relative to the module of the class. All `@injectable`s and `@factory`s that are located in the same or any sub-module will
796
853
  be registered and managed accordingly.
797
- Arguments:
798
- imports (Optional[list[Type]]): Optional list of imported environment types
854
+
855
+ Args:
856
+ imports (Optional[list[Type]]): Optional list of imported module types
799
857
  """
800
858
  def decorator(cls):
801
859
  Providers.register(ClassInstanceProvider(cls, True))
802
860
 
803
- Decorators.add(cls, environment, imports)
861
+ Decorators.add(cls, module, imports)
804
862
  Decorators.add(cls, injectable) # do we need that?
805
863
 
806
864
  return cls
@@ -873,11 +931,28 @@ def conditional(*conditions: Condition):
873
931
  class Environment:
874
932
  """
875
933
  Central class that manages the lifecycle of instances and their dependencies.
934
+
935
+ Usage:
936
+
937
+ ```python
938
+ @injectable()
939
+ class Foo:
940
+ def __init__(self):
941
+
942
+ @environment()
943
+ class SimpleEnvironment:
944
+ def __init__(self):
945
+ pass
946
+
947
+ environment = Environment(SimpleEnvironment)
948
+
949
+ foo = environment.get(Foo) # will create an instance of Foo
950
+ ```
876
951
  """
877
952
 
878
953
  # static data
879
954
 
880
- logger = logging.getLogger(__name__) # __name__ = module name
955
+ logger = logging.getLogger("aspyx.di") # __name__ = module name
881
956
 
882
957
  instance : 'Environment' = None
883
958
 
@@ -987,7 +1062,7 @@ class Environment:
987
1062
 
988
1063
  # sanity check
989
1064
 
990
- decorator = TypeDescriptor.for_type(env).get_decorator(environment)
1065
+ decorator = TypeDescriptor.for_type(env).get_decorator(module)
991
1066
  if decorator is None:
992
1067
  raise DIRegistrationException(f"{env.__name__} is not an environment class")
993
1068
 
@@ -1136,10 +1211,11 @@ class Environment:
1136
1211
  """
1137
1212
  Create or return a cached instance for the given type.
1138
1213
 
1139
- Arguments:
1214
+ Args:
1140
1215
  type (Type): The desired type
1141
1216
 
1142
- Returns: The requested instance
1217
+ Returns:
1218
+ T: The requested instance
1143
1219
  """
1144
1220
  provider = self.providers.get(type, None)
1145
1221
  if provider is None:
@@ -1413,11 +1489,12 @@ class ThreadScope(Scope):
1413
1489
  def get(self, provider: AbstractInstanceProvider, environment: Environment, arg_provider: Callable[[], list]):
1414
1490
  if not hasattr(self._local, "value"):
1415
1491
  self._local.value = provider.create(environment, *arg_provider())
1492
+
1416
1493
  return self._local.value
1417
1494
 
1418
1495
  # internal class that is required to import technical instance providers
1419
1496
 
1420
- @environment()
1497
+ @module()
1421
1498
  class Boot:
1422
1499
  # class
1423
1500
 
@@ -1,5 +1,5 @@
1
1
  """
2
- This module provides utility functions.
2
+ This module provides exception handling functions.
3
3
  """
4
4
  from .exception_manager import exception_handler, handle, ExceptionManager
5
5
 
@@ -32,9 +32,6 @@ def handle():
32
32
 
33
33
  return decorator
34
34
 
35
- class ErrorContext():
36
- pass
37
-
38
35
  class Handler:
39
36
  # constructor
40
37
 
@@ -43,8 +40,13 @@ class Handler:
43
40
  self.instance = instance
44
41
  self.handler = handler
45
42
 
46
- def handle(self, exception: BaseException):
47
- self.handler(self.instance, exception)
43
+ def handle(self, exception: BaseException) -> BaseException:
44
+ result = self.handler(self.instance, exception)
45
+
46
+ if result is not None:
47
+ return result
48
+ else:
49
+ return exception
48
50
 
49
51
  class Chain:
50
52
  # constructor
@@ -55,11 +57,11 @@ class Chain:
55
57
 
56
58
  # public
57
59
 
58
- def handle(self, exception: BaseException):
59
- self.handler.handle(exception)
60
+ def handle(self, exception: BaseException) -> BaseException:
61
+ return self.handler.handle(exception)
60
62
 
61
63
  class Invocation:
62
- def __init__(self, exception: Exception, chain: Chain):
64
+ def __init__(self, exception: BaseException, chain: Chain):
63
65
  self.exception = exception
64
66
  self.chain = chain
65
67
  self.current = self.chain
@@ -74,17 +76,26 @@ class ExceptionManager:
74
76
 
75
77
  exception_handler_classes = []
76
78
 
77
- invocation = ThreadLocal()
79
+ invocation = ThreadLocal[Invocation]()
78
80
 
79
81
  # class methods
80
82
 
81
83
  @classmethod
82
- def proceed(cls):
84
+ def proceed(cls) -> BaseException:
85
+ """
86
+ proceed with the next most applicable handler
87
+
88
+ Returns:
89
+ BaseException: the resulting exception
90
+
91
+ """
83
92
  invocation = cls.invocation.get()
84
93
 
85
94
  invocation.current = invocation.current.next
86
95
  if invocation.current is not None:
87
- invocation.current.handle(invocation.exception)
96
+ return invocation.current.handle(invocation.exception)
97
+ else:
98
+ return invocation.exception
88
99
 
89
100
  # constructor
90
101
 
@@ -93,7 +104,6 @@ class ExceptionManager:
93
104
  self.handler : list[Handler] = []
94
105
  self.cache: Dict[Type, Chain] = {}
95
106
  self.lock = RLock()
96
- self.current_context: Optional[ErrorContext] = None
97
107
 
98
108
  # internal
99
109
 
@@ -153,16 +163,23 @@ class ExceptionManager:
153
163
  else:
154
164
  return None
155
165
 
156
- def handle(self, exception: Exception):
166
+ def handle(self, exception: BaseException) -> BaseException:
157
167
  """
158
- handle an exception by invoking the most applicable handler ( according to mro )
159
- :param exception: the exception
168
+ handle an exception by invoking the most applicable handler (according to mro)
169
+ and return a possible modified exception as a result.
170
+
171
+ Args:
172
+ exception (BaseException): the exception
173
+
174
+ Returns:
175
+ BaseException: the resulting exception
160
176
  """
161
177
  chain = self.get_handlers(type(exception))
162
178
  if chain is not None:
163
-
164
179
  self.invocation.set(Invocation(exception, chain))
165
180
  try:
166
- chain.handle(exception)
181
+ return chain.handle(exception)
167
182
  finally:
168
183
  self.invocation.clear()
184
+ else:
185
+ return exception # hmmm?
aspyx/reflection/proxy.py CHANGED
@@ -14,6 +14,7 @@ class DynamicProxy(Generic[T]):
14
14
  by intercepting method calls at runtime and handling them as needed.
15
15
 
16
16
  Usage:
17
+ ```python
17
18
  class MyHandler(DynamicProxy.InvocationHandler):
18
19
  def invoke(self, invocation):
19
20
  print(f"Intercepted: {invocation.name}")
@@ -22,10 +23,7 @@ class DynamicProxy(Generic[T]):
22
23
 
23
24
  proxy = DynamicProxy.create(SomeClass, MyHandler())
24
25
  proxy.some_method(args) # Will be intercepted by MyHandler.invoke
25
-
26
- Attributes:
27
- type: The proxied class type.
28
- invocation_handler: The handler that processes intercepted method calls.
26
+ ```
29
27
  """
30
28
  # inner class
31
29
 
@@ -32,20 +32,44 @@ class Decorators:
32
32
  Utility class that caches decorators ( Python does not have a feature for this )
33
33
  """
34
34
  @classmethod
35
- def add(cls, func, decorator: Callable, *args):
36
- decorators = getattr(func, '__decorators__', None)
35
+ def add(cls, func_or_class, decorator: Callable, *args):
36
+ """
37
+ Remember the decorator
38
+ Args:
39
+ func_or_class: a function or class
40
+ decorator: the decorator
41
+ *args: any arguments supplied to the decorator
42
+ """
43
+ decorators = getattr(func_or_class, '__decorators__', None)
37
44
  if decorators is None:
38
- setattr(func, '__decorators__', [DecoratorDescriptor(decorator, *args)])
45
+ setattr(func_or_class, '__decorators__', [DecoratorDescriptor(decorator, *args)])
39
46
  else:
40
47
  decorators.append(DecoratorDescriptor(decorator, *args))
41
48
 
42
49
  @classmethod
43
- def has_decorator(cls, func, callable: Callable) -> bool:
44
- return any(decorator.decorator is callable for decorator in Decorators.get(func))
50
+ def has_decorator(cls, func_or_class, callable: Callable) -> bool:
51
+ """
52
+ Return True, if the function or class is decorated with the decorator
53
+ Args:
54
+ func_or_class: a function or class
55
+ callable: the decorator
56
+
57
+ Returns:
58
+ bool: the result
59
+ """
60
+ return any(decorator.decorator is callable for decorator in Decorators.get(func_or_class))
45
61
 
46
62
  @classmethod
47
- def get(cls, func) -> list[DecoratorDescriptor]:
48
- return getattr(func, '__decorators__', [])
63
+ def get(cls, func_or_class) -> list[DecoratorDescriptor]:
64
+ """
65
+ return the list of decorators associated with the given function or class
66
+ Args:
67
+ func_or_class: the function or class
68
+
69
+ Returns:
70
+ list[DecoratorDescriptor]: ths list
71
+ """
72
+ return getattr(func_or_class, '__decorators__', [])
49
73
 
50
74
  class TypeDescriptor:
51
75
  """
@@ -79,30 +103,42 @@ class TypeDescriptor:
79
103
  def get_name(self) -> str:
80
104
  """
81
105
  return the method name
82
- :return: the method name
106
+
107
+ Returns:
108
+ str: the method name
83
109
  """
84
110
  return self.method.__name__
85
111
 
86
112
  def get_doc(self, default = "") -> str:
87
113
  """
88
114
  return the method docstring
89
- :param default: the default if no docstring is found
90
- :return: the docstring
115
+
116
+ Args:
117
+ default: the default if no docstring is found
118
+
119
+ Returns:
120
+ str: the docstring
91
121
  """
92
122
  return self.method.__doc__ or default
93
123
 
94
124
  def is_async(self) -> bool:
95
125
  """
96
126
  return true if the method is asynchronous
97
- :return: async flag
127
+
128
+ Returns:
129
+ bool: async flag
98
130
  """
99
131
  return inspect.iscoroutinefunction(self.method)
100
132
 
101
133
  def get_decorator(self, decorator: Callable) -> Optional[DecoratorDescriptor]:
102
134
  """
103
135
  return the DecoratorDescriptor - if any - associated with the passed Callable
104
- :param decorator:
105
- :return: the DecoratorDescriptor or None
136
+
137
+ Args:
138
+ decorator: the decorator
139
+
140
+ Returns:
141
+ Optional[DecoratorDescriptor]: the DecoratorDescriptor or None
106
142
  """
107
143
  for dec in self.decorators:
108
144
  if dec.decorator is decorator:
@@ -113,8 +149,12 @@ class TypeDescriptor:
113
149
  def has_decorator(self, decorator: Callable) -> bool:
114
150
  """
115
151
  return True if the method is decorated with the decorator
116
- :param decorator: the decorator callable
117
- :return: True if the method is decorated with the decorator
152
+
153
+ Args:
154
+ decorator: the decorator callable
155
+
156
+ Returns:
157
+ bool: True if the method is decorated with the decorator
118
158
  """
119
159
  for dec in self.decorators:
120
160
  if dec.decorator is decorator:
@@ -1,5 +1,5 @@
1
1
  """
2
- threading utilities
2
+ A module with threading related utilities
3
3
  """
4
4
  from .thread_local import ThreadLocal
5
5
 
@@ -22,7 +22,9 @@ class ThreadLocal(Generic[T]):
22
22
  def get(self) -> Optional[T]:
23
23
  """
24
24
  return the current value or invoke the optional factory to compute one
25
- :return: the value associated with the current thread
25
+
26
+ Returns:
27
+ Optional[T]: the value associated with the current thread
26
28
  """
27
29
  if not hasattr(self.local, "value"):
28
30
  if self.factory is not None:
@@ -35,7 +37,9 @@ class ThreadLocal(Generic[T]):
35
37
  def set(self, value: T) -> None:
36
38
  """
37
39
  set a value in the current thread
38
- :param value: the value
40
+
41
+ Args:
42
+ value: the value
39
43
  """
40
44
  self.local.value = value
41
45
 
@@ -17,8 +17,12 @@ class StringBuilder:
17
17
  def append(self, s: str) -> "StringBuilder":
18
18
  """
19
19
  append a string to the end of the string builder
20
- :param s: the string
21
- :return: self
20
+
21
+ Args:
22
+ s (str): the string
23
+
24
+ Returns:
25
+ StringBuilder: self
22
26
  """
23
27
  self._parts.append(str(s))
24
28
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aspyx
3
- Version: 1.4.0
3
+ Version: 1.4.1
4
4
  Summary: A DI and AOP library for Python
5
5
  Author-email: Andreas Ernst <andreas.ernst7@gmail.com>
6
6
  License: MIT License
@@ -40,12 +40,14 @@ Dynamic: license-file
40
40
  ![License](https://img.shields.io/github/license/coolsamson7/aspyx)
41
41
  ![coverage](https://img.shields.io/badge/coverage-94%25-brightgreen)
42
42
  [![PyPI](https://img.shields.io/pypi/v/aspyx)](https://pypi.org/project/aspyx/)
43
- [![Docs](https://img.shields.io/badge/docs-online-blue?logo=github)](https://coolsamson7.github.io/aspyx/)
43
+ [![Docs](https://img.shields.io/badge/docs-online-blue?logo=github)](https://coolsamson7.github.io/aspyx/index/introduction)
44
+
45
+ ![image](https://github.com/user-attachments/assets/e808210a-b1a4-4fd0-93f1-b5f9845fa520)
44
46
 
45
47
  ## Table of Contents
46
48
 
47
49
  - [Motivation](#motivation)
48
- - [Introduction](#introduction)
50
+ - [Overview](#overview)
49
51
  - [Installation](#installation)
50
52
  - [Registration](#registration)
51
53
  - [Class](#class)
@@ -71,16 +73,19 @@ Dynamic: license-file
71
73
 
72
74
  While working on AI-related projects in Python, I was looking for a dependency injection (DI) framework. After evaluating existing options, my impression was that the most either lacked key features — such as integrated AOP — or had APIs that felt overly technical and complex, which made me develop a library on my own with the following goals
73
75
 
74
- - bring both di and AOP features together in a lightweight library,
76
+ - bring both di and AOP features together in a lightweight library ( still only about 2T loc),
75
77
  - be as minimal invasive as possible,
76
78
  - offering mechanisms to easily extend and customize features without touching the core,
77
- - while still offering a _simple_ and _readable_ api that doesnt overwhelm developers
79
+ - while still offering a _simple_ and _readable_ api that doesnt overwhelm developers and only requires a minimum initial learning curve
80
+
81
+ The AOP integration, in particular, makes a lot of sense because:
78
82
 
79
- Especially the AOP integration definitely makes sense, as aspects on their own also usually require a context, which in a DI world is simply injected.
83
+ - Aspects typically require context, which is naturally provided through DI,
84
+ - And they should only apply to objects managed by the container, rather than acting globally.
80
85
 
81
- # Introduction
86
+ # Overview
82
87
 
83
- Aspyx is a lightweight Python library that provides both Dependency Injection (DI) and Aspect-Oriented Programming (AOP) support.
88
+ Aspyx is a lightweight - still only about 2T LOC- Python library that provides both Dependency Injection (DI) and Aspect-Oriented Programming (AOP) support.
84
89
 
85
90
  The following DI features are supported
86
91
  - constructor and setter injection
@@ -89,13 +94,13 @@ The following DI features are supported
89
94
  - post processors
90
95
  - support for factory classes and methods
91
96
  - support for eager and lazy construction
92
- - support for scopes singleton, request and thread
97
+ - support for scopes "singleton", "request" and "thread"
93
98
  - possibility to add custom scopes
94
99
  - conditional registration of classes and factories ( aka profiles in spring )
95
100
  - lifecycle events methods `on_init`, `on_destroy`, `on_running`
96
- - bundling of injectable objects according to their module location including recursive imports and inheritance
97
- - instantiation of - possibly multiple - container instances - so called environments - that manage the lifecycle of related objects
98
- - hierarchical environments
101
+ - Automatic discovery and bundling of injectable objects based on their module location, including support for recursive imports
102
+ - Instantiation of one or possible more isolated container instances called environments each managing the lifecycle of a related set of objects,
103
+ - Support for hierarchical environments, enabling structured scoping and layered object management.
99
104
 
100
105
  With respect to AOP:
101
106
  - support for before, around, after and error aspects
@@ -107,7 +112,8 @@ The library is thread-safe and heavily performance optimized as most of the runt
107
112
  Let's look at a simple example
108
113
 
109
114
  ```python
110
- from aspyx.di import injectable, on_init, on_destroy, environment, Environment
115
+ from aspyx.di import injectable, on_init, on_destroy, module, Environment
116
+
111
117
 
112
118
  @injectable()
113
119
  class Foo:
@@ -117,27 +123,28 @@ class Foo:
117
123
  def hello(self, msg: str):
118
124
  print(f"hello {msg}")
119
125
 
126
+
120
127
  @injectable() # eager and singleton by default
121
128
  class Bar:
122
- def __init__(self, foo: Foo): # will inject the Foo dependency
129
+ def __init__(self, foo: Foo): # will inject the Foo dependency
123
130
  self.foo = foo
124
131
 
125
- @on_init() # a lifecycle callback called after the constructor and all possible injections
132
+ @on_init() # a lifecycle callback called after the constructor and all possible injections
126
133
  def init(self):
127
134
  ...
128
135
 
129
136
 
130
- # this class will register all - specifically decorated - classes and factories in the own module
131
- # In this case Foo and Bar
137
+ # this class will discover and manage all - specifically decorated - classes and factories that are part of the own module
132
138
 
133
- @environment()
134
- class SampleEnvironment:
139
+ @module()
140
+ class SampleModule:
135
141
  def __init__(self):
136
142
  pass
137
143
 
144
+
138
145
  # create environment
139
146
 
140
- environment = Environment(SampleEnvironment)
147
+ environment = Environment(SampleModule)
141
148
 
142
149
  # fetch an instance
143
150
 
@@ -171,12 +178,7 @@ class SampleAdvice:
171
178
  return invocation.proceed()
172
179
  ```
173
180
 
174
- The invocation parameter stores the complete context of the current execution, which are
175
- - the method
176
- - args
177
- - kwargs
178
- - the result
179
- - the possible caught error
181
+ While features like DI and AOP are often associated with enterprise applcations, this example hopefully demonstrates that they work just as well in small- to medium-sized projects—without introducing significant overhead—while still providing powerful tools for achieving clean architecture, resulting in maintainable and easily testable code.
180
182
 
181
183
  Let's look at the details
182
184
 
@@ -280,21 +282,26 @@ Valid conditions are created by:
280
282
 
281
283
  ## Definition
282
284
 
283
- An `Environment` is the container that manages the lifecycle of objects. The set of classes and instances is determined by a constructor argument that controls the class registry.
285
+ An `Environment` is the container that manages the lifecycle of objects.
286
+ The set of classes and instances is determined by a
287
+ constructor type argument called `module`.
284
288
 
285
289
  **Example**:
286
290
  ```python
287
- @environment()
288
- class SampleEnvironment:
291
+ @module()
292
+ class SampleModule:
289
293
  def __init__(self):
290
294
  pass
291
-
292
- environment = Environment(SampleEnvironment)
293
295
  ```
294
296
 
295
- The default is that all eligible classes, that are implemented in the containing module or in any submodule will be managed.
296
- THe container will import the module and its children automatically. No need to add artificial import statements!
297
+ A module is a regular injectable class decorated with `@module` that controls the discovery of injectable classes, by filtering classes according to their module location relative to this class.
298
+ All eligible classes, that are implemented in the containing module or in any submodule will be managed.
297
299
 
300
+ In a second step the real container - the environment - is created based on a module:
301
+
302
+ ```python
303
+ environment = Environment(SampleModule, features=["dev"])
304
+ ```
298
305
 
299
306
  By adding the parameter `features: list[str]`, it is possible to filter injectables by evaluating the corresponding `@conditional` decorators.
300
307
 
@@ -307,21 +314,21 @@ class DevOnly:
307
314
  def __init__(self):
308
315
  pass
309
316
 
310
- @environment()
311
- class SampleEnvironmen():
317
+ @module()
318
+ class SampleModule():
312
319
  def __init__(self):
313
320
  pass
314
321
 
315
- environment = Environment(SampleEnvironment, features=["dev"])
322
+ environment = Environment(SampleModule, features=["dev"])
316
323
  ```
317
324
 
318
325
 
319
- By adding an `imports: list[Type]` parameter, specifying other environment types, it will register the appropriate classes recursively.
326
+ By adding an `imports: list[Type]` parameter, specifying other module types, it will register the appropriate classes recursively.
320
327
 
321
328
  **Example**:
322
329
  ```python
323
- @environment()
324
- class SampleEnvironmen(imports=[OtherEnvironment]):
330
+ @module()
331
+ class SampleModule(imports=[OtherModule]):
325
332
  def __init__(self):
326
333
  pass
327
334
  ```
@@ -330,8 +337,9 @@ Another possibility is to add a parent environment as an `Environment` construct
330
337
 
331
338
  **Example**:
332
339
  ```python
333
- rootEnvironment = Environment(RootEnvironment)
334
- environment = Environment(SampleEnvironment, parent=rootEnvironment)
340
+ rootEnvironment = Environment(RootModule)
341
+
342
+ environment = Environment(SampleModule, parent=rootEnvironment)
335
343
  ```
336
344
 
337
345
  The difference is, that in the first case, class instances of imported modules will be created in the scope of the _own_ environment, while in the second case, it will return instances managed by the parent.
@@ -464,7 +472,9 @@ class SingletonScope(Scope):
464
472
 
465
473
  It is possible to define different aspects, that will be part of method calling flow. This logic fits nicely in the library, since the DI framework controls the instantiation logic and can handle aspects within a regular post processor.
466
474
 
467
- Advice classes need to be part of classes that add a `@advice()` decorator and can define methods that add aspects.
475
+ On the other hand, advices are also regular DI objects, as they will usually require some kind of - injected - context.
476
+
477
+ Advices are regular classes decorated with `@advice` that define aspect methods.
468
478
 
469
479
  ```python
470
480
  @advice
@@ -516,7 +526,7 @@ All methods are expected to have single `Invocation` parameter, that stores
516
526
  - `result` the result ( initially `None`)
517
527
  - `exception` a possible caught exception ( initially `None`)
518
528
 
519
- ⚠️ **Attention:** It is essential for `around` methods to call `proceed()` on the invocation, which will call the next around method in the chain and finally the original method.
529
+ ⚠️ **Note:** It is essential for `around` methods to call `proceed()` on the invocation, which will call the next around method in the chain and finally the original method.
520
530
 
521
531
  If the `proceed` is called with parameters, they will replace the original parameters!
522
532
 
@@ -562,6 +572,12 @@ class TransactionAdvice:
562
572
 
563
573
  With respect to async methods, you need to make sure, to replace a `proceed()` with a `await proceed_async()` to have the overall chain async!
564
574
 
575
+ ## Advice Lifecycle and visibility.
576
+
577
+ Advices are always part of a specific environment, and only modify methods of objects managed by exactly this environment.
578
+
579
+ An advice of a parent environment will for example not see classes of inherited environments. What is done instead, is to recreate the advice - more technically speaking, a processor that will collect and apply the advices - in every child environment, and let it operate on the local objects. With this approach different environments are completely isolated from each other with no side effects whatsoever.
580
+
565
581
  # Threading
566
582
 
567
583
  A handy decorator `@synchronized` in combination with the respective advice is implemented that automatically synchronizes methods with a `RLock` associated with the instance.
@@ -652,8 +668,8 @@ Two specific source are already implemented:
652
668
  Typically you create the required configuration sources in an environment class, e.g.
653
669
 
654
670
  ```python
655
- @environment()
656
- class SampleEnvironment:
671
+ @module()
672
+ class SampleModule:
657
673
  # constructor
658
674
 
659
675
  def __init__(self):
@@ -725,8 +741,8 @@ class DerivedException(Exception):
725
741
  def __init__(self):
726
742
  pass
727
743
 
728
- @environment()
729
- class SampleEnvironment:
744
+ @module()
745
+ class SampleModule:
730
746
  # constructor
731
747
 
732
748
  def __init__(self):
@@ -820,6 +836,10 @@ class ExceptionAdvice:
820
836
  - bugfixes
821
837
  - added `@ExceptionManager`
822
838
 
839
+ **1.4.1**
840
+
841
+ - mkdocs
842
+
823
843
 
824
844
 
825
845
 
@@ -0,0 +1,25 @@
1
+ aspyx/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ aspyx/di/__init__.py,sha256=BDOloIhmCIUJWC7l4PLtuiWS1LlWyitIofLCLcyXYpQ,1097
3
+ aspyx/di/di.py,sha256=ozZanDcrmluzghQBqqD_vbav3Civ-V4R8XXwlBAM3MI,44068
4
+ aspyx/di/aop/__init__.py,sha256=rn6LSpzFtUOlgaBATyhLRWBzFmZ6XoVKA9B8SgQzYEI,746
5
+ aspyx/di/aop/aop.py,sha256=Cn-fqFW6PznVDM38fPX7mqlSpjGKMsgpJRBSYBv59xY,18403
6
+ aspyx/di/configuration/__init__.py,sha256=flM9A79J2wfA5I8goQbxs4tTqYustR9tn_9s0YO2WJQ,484
7
+ aspyx/di/configuration/configuration.py,sha256=cXW40bPXiUZ9hUtBoZkSATT3nLrDPWsSqxtgASIBQaM,4375
8
+ aspyx/di/configuration/env_configuration_source.py,sha256=FXPvREzq2ZER6_GG5xdpx154TQQDxZVf7LW7cvaylAk,1446
9
+ aspyx/di/configuration/yaml_configuration_source.py,sha256=NDl3SeoLMNVlzHgfP-Ysvhco1tRew_zFnBL5gGy2WRk,550
10
+ aspyx/di/threading/__init__.py,sha256=qrWdaq7MewQ2UmZy4J0Dn6BhY-ahfiG3xsv-EHqoqSE,191
11
+ aspyx/di/threading/synchronized.py,sha256=BQ9PjMQUJsF5r-qWaDgvqg3AvFm_R9QZdKB49EkoelQ,1263
12
+ aspyx/exception/__init__.py,sha256=OZwv-C3ZHD0Eg1rohCQMj575WLJ7lfYuk6PZD6sh1MA,211
13
+ aspyx/exception/exception_manager.py,sha256=ihQ8Hs_EAUi-4xtVOn6kNZVblJjznpscLQ4vKXfWq7s,5228
14
+ aspyx/reflection/__init__.py,sha256=r2sNJrfHDpuqaIYu4fTYsoo046gpgn4VTd7bsS3mQJY,282
15
+ aspyx/reflection/proxy.py,sha256=kaVPeEGuerdYgcgchMe99c8xykDskRSYR-w4OF83Ofo,1868
16
+ aspyx/reflection/reflection.py,sha256=AgChenUzK9elcqOc_BfMiszTQuXrsy0NHYkqy9Jsl0E,8066
17
+ aspyx/threading/__init__.py,sha256=3clmbCDP37GPan3dWtxTQvpg0Ti4aFzruAbUClkHGi0,147
18
+ aspyx/threading/thread_local.py,sha256=86dNtbA4k2B-rNUUnZgn3_pU0DAojgLrRnh8RL6zf1E,1196
19
+ aspyx/util/__init__.py,sha256=8H2yKkXu3nkRGeTerb8ialzKGfvzUx44XUWFUYcYuQM,125
20
+ aspyx/util/stringbuilder.py,sha256=a-0T4YEXSJFUuQ3ztKN1ZPARkh8dIGMSkNEEJHRN7dc,856
21
+ aspyx-1.4.1.dist-info/licenses/LICENSE,sha256=n4jfx_MNj7cBtPhhI7MCoB_K35cj1icP9yJ4Rh4vlvY,1070
22
+ aspyx-1.4.1.dist-info/METADATA,sha256=cEYH9P1Mng4RjRQID-bUp-5X6fbxUHhQWf3WDUwkPI0,26564
23
+ aspyx-1.4.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
24
+ aspyx-1.4.1.dist-info/top_level.txt,sha256=A_ZwhBY_ybIgjZlztd44eaOrWqkJAndiqjGlbJ3tR_I,6
25
+ aspyx-1.4.1.dist-info/RECORD,,
@@ -1,25 +0,0 @@
1
- aspyx/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- aspyx/di/__init__.py,sha256=OfETLGaquTbFHhhDRzzGtnSu-KkMj68aDdEaVU19KoI,1107
3
- aspyx/di/di.py,sha256=qoOChsiUBla52UaclRlX6o5ZxRylaFF4IneanvwISos,42095
4
- aspyx/di/aop/__init__.py,sha256=nOABex49zSyMZ2w1ezwX3Q3yrOcQRSDjDtSj0DwKVbQ,233
5
- aspyx/di/aop/aop.py,sha256=36p3jCnNtrDL11jTi7NNpW1eGO_BqKL0s5rErsPdxps,17520
6
- aspyx/di/configuration/__init__.py,sha256=mweJ3tZX1YJfY1d4ra-i0TWEcF3EwXBpGbHrKg1Kc6E,380
7
- aspyx/di/configuration/configuration.py,sha256=KfPjrlUhhmEOUxdJiXePt5RGxKc8JczkWqlEBjpWQTg,4362
8
- aspyx/di/configuration/env_configuration_source.py,sha256=FXPvREzq2ZER6_GG5xdpx154TQQDxZVf7LW7cvaylAk,1446
9
- aspyx/di/configuration/yaml_configuration_source.py,sha256=NDl3SeoLMNVlzHgfP-Ysvhco1tRew_zFnBL5gGy2WRk,550
10
- aspyx/di/threading/__init__.py,sha256=qrWdaq7MewQ2UmZy4J0Dn6BhY-ahfiG3xsv-EHqoqSE,191
11
- aspyx/di/threading/synchronized.py,sha256=BQ9PjMQUJsF5r-qWaDgvqg3AvFm_R9QZdKB49EkoelQ,1263
12
- aspyx/exception/__init__.py,sha256=2Jo0a_fZK8_U9SpPZ0j4aeAXJZ28uw6g-20TH_85JqY,200
13
- aspyx/exception/exception_manager.py,sha256=8H5fbbcpzLxiK7OI-EZaXyX5Db4uZt9-VrAx5LMiSm8,4692
14
- aspyx/reflection/__init__.py,sha256=r2sNJrfHDpuqaIYu4fTYsoo046gpgn4VTd7bsS3mQJY,282
15
- aspyx/reflection/proxy.py,sha256=zJ6Psd6zWfFABdrKOf4cULt3gibyqCRdcR6z8WKIkzE,1982
16
- aspyx/reflection/reflection.py,sha256=bzH5KVJ5X5ycQu5SG7BFZtioWN7sa1w1Y-xR_Y-oN6w,7113
17
- aspyx/threading/__init__.py,sha256=_j_AQ4t1ecRaKIb9KCTT_EV0b7hivNML-2wV2XF7G6Y,125
18
- aspyx/threading/thread_local.py,sha256=nOSS2DM1rIHmzdU9_fjxaUF3oXCaRTBHwe76IdwMqC8,1158
19
- aspyx/util/__init__.py,sha256=8H2yKkXu3nkRGeTerb8ialzKGfvzUx44XUWFUYcYuQM,125
20
- aspyx/util/stringbuilder.py,sha256=L3MkHAo4CJrBXuWmaRQASIa9EAs8O_ea7EjZoLsvp08,811
21
- aspyx-1.4.0.dist-info/licenses/LICENSE,sha256=n4jfx_MNj7cBtPhhI7MCoB_K35cj1icP9yJ4Rh4vlvY,1070
22
- aspyx-1.4.0.dist-info/METADATA,sha256=S9Qcqc9v6VHJSU76dGT_iEo1Z4ghvpp8nE7uxoc2NkI,25213
23
- aspyx-1.4.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
24
- aspyx-1.4.0.dist-info/top_level.txt,sha256=A_ZwhBY_ybIgjZlztd44eaOrWqkJAndiqjGlbJ3tR_I,6
25
- aspyx-1.4.0.dist-info/RECORD,,
File without changes