aspyx 1.1.0__py3-none-any.whl → 1.3.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 might be problematic. Click here for more details.
- aspyx/di/__init__.py +10 -5
- aspyx/di/aop/aop.py +86 -4
- aspyx/di/configuration/__init__.py +4 -1
- aspyx/di/configuration/configuration.py +1 -51
- aspyx/di/configuration/env_configuration_source.py +55 -0
- aspyx/di/configuration/yaml_configuration_source.py +26 -0
- aspyx/di/di.py +339 -207
- aspyx/di/threading/__init__.py +11 -0
- aspyx/di/threading/synchronized.py +46 -0
- aspyx/reflection/reflection.py +33 -1
- {aspyx-1.1.0.dist-info → aspyx-1.3.0.dist-info}/METADATA +150 -29
- aspyx-1.3.0.dist-info/RECORD +20 -0
- aspyx-1.1.0.dist-info/RECORD +0 -16
- {aspyx-1.1.0.dist-info → aspyx-1.3.0.dist-info}/WHEEL +0 -0
- {aspyx-1.1.0.dist-info → aspyx-1.3.0.dist-info}/licenses/LICENSE +0 -0
- {aspyx-1.1.0.dist-info → aspyx-1.3.0.dist-info}/top_level.txt +0 -0
aspyx/di/__init__.py
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
"""
|
|
2
2
|
This module provides dependency injection and aop capabilities for Python applications.
|
|
3
3
|
"""
|
|
4
|
-
from .di import
|
|
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
|
|
5
5
|
|
|
6
|
-
# import something from the subpackages, so that
|
|
6
|
+
# import something from the subpackages, so that the decorators are executed
|
|
7
7
|
|
|
8
8
|
from .configuration import ConfigurationManager
|
|
9
9
|
from .aop import before
|
|
10
|
+
from .threading import SynchronizeAdvice
|
|
10
11
|
|
|
11
|
-
imports = [ConfigurationManager, before]
|
|
12
|
+
imports = [ConfigurationManager, before, SynchronizeAdvice]
|
|
12
13
|
|
|
13
14
|
__all__ = [
|
|
14
15
|
"ClassInstanceProvider",
|
|
@@ -19,6 +20,7 @@ __all__ = [
|
|
|
19
20
|
"environment",
|
|
20
21
|
"inject",
|
|
21
22
|
"create",
|
|
23
|
+
"order",
|
|
22
24
|
|
|
23
25
|
"on_init",
|
|
24
26
|
"on_running",
|
|
@@ -28,6 +30,9 @@ __all__ = [
|
|
|
28
30
|
"PostProcessor",
|
|
29
31
|
"AbstractCallableProcessor",
|
|
30
32
|
"LifecycleCallable",
|
|
31
|
-
"
|
|
32
|
-
"Lifecycle"
|
|
33
|
+
"DIException",
|
|
34
|
+
"Lifecycle",
|
|
35
|
+
"conditional",
|
|
36
|
+
"requires_class",
|
|
37
|
+
"requires_feature"
|
|
33
38
|
]
|
aspyx/di/aop/aop.py
CHANGED
|
@@ -50,7 +50,7 @@ class AspectTarget(ABC):
|
|
|
50
50
|
__slots__ = [
|
|
51
51
|
"_function",
|
|
52
52
|
"_type",
|
|
53
|
-
|
|
53
|
+
"_async",
|
|
54
54
|
"_clazz",
|
|
55
55
|
"_instance",
|
|
56
56
|
"names",
|
|
@@ -65,6 +65,7 @@ class AspectTarget(ABC):
|
|
|
65
65
|
def __init__(self):
|
|
66
66
|
self._clazz = None
|
|
67
67
|
self._instance = None
|
|
68
|
+
self._async = False
|
|
68
69
|
self._function = None
|
|
69
70
|
self._type = None
|
|
70
71
|
|
|
@@ -108,6 +109,10 @@ class AspectTarget(ABC):
|
|
|
108
109
|
|
|
109
110
|
return self
|
|
110
111
|
|
|
112
|
+
def that_are_async(self):
|
|
113
|
+
self._async = True
|
|
114
|
+
return self
|
|
115
|
+
|
|
111
116
|
def of_type(self, type: Type):
|
|
112
117
|
self.types.append(type)
|
|
113
118
|
return self
|
|
@@ -178,6 +183,14 @@ class MethodAspectTarget(AspectTarget):
|
|
|
178
183
|
|
|
179
184
|
method_descriptor = descriptor.get_method(func.__name__)
|
|
180
185
|
|
|
186
|
+
# async
|
|
187
|
+
|
|
188
|
+
name = method_descriptor.method.__name__
|
|
189
|
+
is_async = method_descriptor.is_async()
|
|
190
|
+
|
|
191
|
+
if self._async is not method_descriptor.is_async():
|
|
192
|
+
return False
|
|
193
|
+
|
|
181
194
|
# type
|
|
182
195
|
|
|
183
196
|
if len(self.types) > 0:
|
|
@@ -234,6 +247,9 @@ class JoinPoint:
|
|
|
234
247
|
def call(self, invocation: 'Invocation'):
|
|
235
248
|
pass
|
|
236
249
|
|
|
250
|
+
async def call_async(self, invocation: 'Invocation'):
|
|
251
|
+
pass
|
|
252
|
+
|
|
237
253
|
class FunctionJoinPoint(JoinPoint):
|
|
238
254
|
__slots__ = [
|
|
239
255
|
"instance",
|
|
@@ -251,6 +267,11 @@ class FunctionJoinPoint(JoinPoint):
|
|
|
251
267
|
|
|
252
268
|
return self.func(self.instance, invocation)
|
|
253
269
|
|
|
270
|
+
async def call_async(self, invocation: 'Invocation'):
|
|
271
|
+
invocation.current_join_point = self
|
|
272
|
+
|
|
273
|
+
return await self.func(self.instance, invocation)
|
|
274
|
+
|
|
254
275
|
class MethodJoinPoint(FunctionJoinPoint):
|
|
255
276
|
__slots__ = []
|
|
256
277
|
|
|
@@ -262,6 +283,11 @@ class MethodJoinPoint(FunctionJoinPoint):
|
|
|
262
283
|
|
|
263
284
|
return self.func(*invocation.args, **invocation.kwargs)
|
|
264
285
|
|
|
286
|
+
async def call_async(self, invocation: 'Invocation'):
|
|
287
|
+
invocation.current_join_point = self
|
|
288
|
+
|
|
289
|
+
return await self.func(*invocation.args, **invocation.kwargs)
|
|
290
|
+
|
|
265
291
|
@dataclass
|
|
266
292
|
class JoinPoints:
|
|
267
293
|
before: list[JoinPoint]
|
|
@@ -328,6 +354,37 @@ class Invocation:
|
|
|
328
354
|
|
|
329
355
|
return self.result
|
|
330
356
|
|
|
357
|
+
async def call_async(self, *args, **kwargs):
|
|
358
|
+
# remember args
|
|
359
|
+
|
|
360
|
+
self.args = args
|
|
361
|
+
self.kwargs = kwargs
|
|
362
|
+
|
|
363
|
+
# run all before
|
|
364
|
+
|
|
365
|
+
for join_point in self.join_points.before:
|
|
366
|
+
join_point.call(self)
|
|
367
|
+
|
|
368
|
+
# run around's with the method being the last aspect!
|
|
369
|
+
|
|
370
|
+
try:
|
|
371
|
+
self.result = await self.join_points.around[0].call_async(self) # will follow the proceed chain
|
|
372
|
+
|
|
373
|
+
except Exception as e:
|
|
374
|
+
self.exception = e
|
|
375
|
+
for join_point in self.join_points.error:
|
|
376
|
+
join_point.call(self)
|
|
377
|
+
|
|
378
|
+
# run all before
|
|
379
|
+
|
|
380
|
+
for join_point in self.join_points.after:
|
|
381
|
+
join_point.call(self)
|
|
382
|
+
|
|
383
|
+
if self.exception is not None:
|
|
384
|
+
raise self.exception # rethrow the error
|
|
385
|
+
|
|
386
|
+
return self.result
|
|
387
|
+
|
|
331
388
|
def proceed(self, *args, **kwargs):
|
|
332
389
|
"""
|
|
333
390
|
Proceed to the next join point in the around chain up to the original method.
|
|
@@ -340,6 +397,18 @@ class Invocation:
|
|
|
340
397
|
|
|
341
398
|
return self.current_join_point.next.call(self)
|
|
342
399
|
|
|
400
|
+
async def proceed_async(self, *args, **kwargs):
|
|
401
|
+
"""
|
|
402
|
+
Proceed to the next join point in the around chain up to the original method.
|
|
403
|
+
"""
|
|
404
|
+
if len(args) > 0 or len(kwargs) > 0: # as soon as we have args, we replace the current ones
|
|
405
|
+
self.args = args
|
|
406
|
+
self.kwargs = kwargs
|
|
407
|
+
|
|
408
|
+
# next one please...
|
|
409
|
+
|
|
410
|
+
return await self.current_join_point.next.call_async(self)
|
|
411
|
+
|
|
343
412
|
@injectable()
|
|
344
413
|
class Advice:
|
|
345
414
|
# static data
|
|
@@ -381,13 +450,14 @@ class Advice:
|
|
|
381
450
|
|
|
382
451
|
if result is None:
|
|
383
452
|
result = {}
|
|
453
|
+
self.cache[clazz] = result
|
|
384
454
|
|
|
385
455
|
for _, member in inspect.getmembers(clazz, predicate=inspect.isfunction):
|
|
386
456
|
join_points = self.compute_join_points(clazz, member, environment)
|
|
387
457
|
if join_points is not None:
|
|
388
458
|
result[member] = join_points
|
|
389
459
|
|
|
390
|
-
|
|
460
|
+
|
|
391
461
|
|
|
392
462
|
# add around methods
|
|
393
463
|
|
|
@@ -537,6 +607,18 @@ class AdviceProcessor(PostProcessor):
|
|
|
537
607
|
Environment.logger.debug("add aspects for %s:%s", type(instance), member.__name__)
|
|
538
608
|
|
|
539
609
|
def wrap(jp):
|
|
540
|
-
|
|
610
|
+
def sync_wrapper(*args, **kwargs):
|
|
611
|
+
return Invocation(member, jp).call(*args, **kwargs)
|
|
612
|
+
|
|
613
|
+
return sync_wrapper
|
|
614
|
+
|
|
615
|
+
def wrap_async(jp):
|
|
616
|
+
async def async_wrapper(*args, **kwargs):
|
|
617
|
+
return await Invocation(member, jp).call_async(*args, **kwargs)
|
|
618
|
+
|
|
619
|
+
return async_wrapper
|
|
541
620
|
|
|
542
|
-
|
|
621
|
+
if inspect.iscoroutinefunction(member):
|
|
622
|
+
setattr(instance, member.__name__, types.MethodType(wrap_async(join_points), instance))
|
|
623
|
+
else:
|
|
624
|
+
setattr(instance, member.__name__, types.MethodType(wrap(join_points), instance))
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Configuration value handling
|
|
3
3
|
"""
|
|
4
|
-
from .configuration import ConfigurationManager, ConfigurationSource,
|
|
4
|
+
from .configuration import ConfigurationManager, ConfigurationSource, value
|
|
5
|
+
from .env_configuration_source import EnvConfigurationSource
|
|
6
|
+
from .yaml_configuration_source import YamlConfigurationSource
|
|
5
7
|
|
|
6
8
|
__all__ = [
|
|
7
9
|
"ConfigurationManager",
|
|
8
10
|
"ConfigurationSource",
|
|
9
11
|
"EnvConfigurationSource",
|
|
12
|
+
"YamlConfigurationSource",
|
|
10
13
|
"value"
|
|
11
14
|
]
|
|
@@ -4,9 +4,7 @@ Configuration handling module.
|
|
|
4
4
|
from __future__ import annotations
|
|
5
5
|
|
|
6
6
|
from abc import ABC, abstractmethod
|
|
7
|
-
import os
|
|
8
7
|
from typing import Optional, Type, TypeVar
|
|
9
|
-
from dotenv import load_dotenv
|
|
10
8
|
|
|
11
9
|
from aspyx.di import injectable, Environment, LifecycleCallable, Lifecycle
|
|
12
10
|
from aspyx.di.di import order, inject
|
|
@@ -67,7 +65,7 @@ class ConfigurationManager:
|
|
|
67
65
|
|
|
68
66
|
def get(self, path: str, type: Type[T], default : Optional[T]=None) -> T:
|
|
69
67
|
"""
|
|
70
|
-
|
|
68
|
+
Retrieve a configuration value by path and type, with optional coercion.
|
|
71
69
|
Arguments:
|
|
72
70
|
path (str): The path to the configuration value, e.g. "database.host".
|
|
73
71
|
type (Type[T]): The expected type.
|
|
@@ -119,54 +117,6 @@ class ConfigurationSource(ABC):
|
|
|
119
117
|
return the configuration values of this source as a dictionary.
|
|
120
118
|
"""
|
|
121
119
|
|
|
122
|
-
@injectable()
|
|
123
|
-
class EnvConfigurationSource(ConfigurationSource):
|
|
124
|
-
"""
|
|
125
|
-
EnvConfigurationSource loads all environment variables.
|
|
126
|
-
"""
|
|
127
|
-
|
|
128
|
-
__slots__ = []
|
|
129
|
-
|
|
130
|
-
# constructor
|
|
131
|
-
|
|
132
|
-
def __init__(self):
|
|
133
|
-
super().__init__()
|
|
134
|
-
|
|
135
|
-
load_dotenv()
|
|
136
|
-
|
|
137
|
-
# implement
|
|
138
|
-
|
|
139
|
-
def load(self) -> dict:
|
|
140
|
-
def merge_dicts(a, b):
|
|
141
|
-
"""Recursively merges b into a"""
|
|
142
|
-
for key, value in b.items():
|
|
143
|
-
if isinstance(value, dict) and key in a and isinstance(a[key], dict):
|
|
144
|
-
merge_dicts(a[key], value)
|
|
145
|
-
else:
|
|
146
|
-
a[key] = value
|
|
147
|
-
return a
|
|
148
|
-
|
|
149
|
-
def explode_key(key, value):
|
|
150
|
-
"""Explodes keys with '.' or '/' into nested dictionaries"""
|
|
151
|
-
parts = key.replace('/', '.').split('.')
|
|
152
|
-
d = current = {}
|
|
153
|
-
for part in parts[:-1]:
|
|
154
|
-
current[part] = {}
|
|
155
|
-
current = current[part]
|
|
156
|
-
current[parts[-1]] = value
|
|
157
|
-
return d
|
|
158
|
-
|
|
159
|
-
exploded = {}
|
|
160
|
-
|
|
161
|
-
for key, value in os.environ.items():
|
|
162
|
-
if '.' in key or '/' in key:
|
|
163
|
-
partial = explode_key(key, value)
|
|
164
|
-
merge_dicts(exploded, partial)
|
|
165
|
-
else:
|
|
166
|
-
exploded[key] = value
|
|
167
|
-
|
|
168
|
-
return exploded
|
|
169
|
-
|
|
170
120
|
# decorator
|
|
171
121
|
|
|
172
122
|
def value(key: str, default=None):
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""
|
|
2
|
+
EnvConfigurationSource - Loads environment variables as configuration source.
|
|
3
|
+
"""
|
|
4
|
+
import os
|
|
5
|
+
|
|
6
|
+
from dotenv import load_dotenv
|
|
7
|
+
|
|
8
|
+
from .configuration import ConfigurationSource
|
|
9
|
+
|
|
10
|
+
class EnvConfigurationSource(ConfigurationSource):
|
|
11
|
+
"""
|
|
12
|
+
EnvConfigurationSource loads all environment variables.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
__slots__ = []
|
|
16
|
+
|
|
17
|
+
# constructor
|
|
18
|
+
|
|
19
|
+
def __init__(self):
|
|
20
|
+
super().__init__()
|
|
21
|
+
|
|
22
|
+
# implement
|
|
23
|
+
|
|
24
|
+
def load(self) -> dict:
|
|
25
|
+
def merge_dicts(a, b):
|
|
26
|
+
"""Recursively merges b into a"""
|
|
27
|
+
for key, value in b.items():
|
|
28
|
+
if isinstance(value, dict) and key in a and isinstance(a[key], dict):
|
|
29
|
+
merge_dicts(a[key], value)
|
|
30
|
+
else:
|
|
31
|
+
a[key] = value
|
|
32
|
+
return a
|
|
33
|
+
|
|
34
|
+
def explode_key(key, value):
|
|
35
|
+
"""Explodes keys with '.' or '/' into nested dictionaries"""
|
|
36
|
+
parts = key.replace('/', '.').split('.')
|
|
37
|
+
d = current = {}
|
|
38
|
+
for part in parts[:-1]:
|
|
39
|
+
current[part] = {}
|
|
40
|
+
current = current[part]
|
|
41
|
+
current[parts[-1]] = value
|
|
42
|
+
return d
|
|
43
|
+
|
|
44
|
+
exploded = {}
|
|
45
|
+
|
|
46
|
+
load_dotenv()
|
|
47
|
+
|
|
48
|
+
for key, value in os.environ.items():
|
|
49
|
+
if '.' in key or '/' in key:
|
|
50
|
+
partial = explode_key(key, value)
|
|
51
|
+
merge_dicts(exploded, partial)
|
|
52
|
+
else:
|
|
53
|
+
exploded[key] = value
|
|
54
|
+
|
|
55
|
+
return exploded
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""
|
|
2
|
+
YamlConfigurationSource - Loads variables from a YAML configuration file.
|
|
3
|
+
"""
|
|
4
|
+
import yaml
|
|
5
|
+
|
|
6
|
+
from .configuration import ConfigurationSource
|
|
7
|
+
|
|
8
|
+
class YamlConfigurationSource(ConfigurationSource):
|
|
9
|
+
"""
|
|
10
|
+
YamlConfigurationSource loads variables from a YAML configuration file.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
__slots__ = ["file"]
|
|
14
|
+
|
|
15
|
+
# constructor
|
|
16
|
+
|
|
17
|
+
def __init__(self, file: str):
|
|
18
|
+
super().__init__()
|
|
19
|
+
|
|
20
|
+
self.file = file
|
|
21
|
+
|
|
22
|
+
# implement
|
|
23
|
+
|
|
24
|
+
def load(self) -> dict:
|
|
25
|
+
with open(self.file, "r") as file:
|
|
26
|
+
return yaml.safe_load(file)
|