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 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 InjectorException, AbstractCallableProcessor, LifecycleCallable, Lifecycle, Providers, Environment, ClassInstanceProvider, injectable, factory, environment, inject, 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, environment, inject, order, create, on_init, on_running, on_destroy, inject_environment, Factory, PostProcessor
5
5
 
6
- # import something from the subpackages, so that teh decorators are executed
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
- "InjectorException",
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
- self.cache[clazz] = result
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
- return lambda *args, **kwargs: Invocation(member, jp).call(*args, **kwargs)
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
- setattr(instance, member.__name__, types.MethodType(wrap(join_points), instance))
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, EnvConfigurationSource, value
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
- Get a configuration value by path and type, with optional coercion.
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)