ez-a-sync 0.22.13__py3-none-any.whl → 0.22.15__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 ez-a-sync might be problematic. Click here for more details.
- a_sync/ENVIRONMENT_VARIABLES.py +4 -3
- a_sync/__init__.py +30 -12
- a_sync/_smart.py +132 -28
- a_sync/_typing.py +56 -12
- a_sync/a_sync/__init__.py +35 -10
- a_sync/a_sync/_descriptor.py +74 -26
- a_sync/a_sync/_flags.py +14 -6
- a_sync/a_sync/_helpers.py +8 -7
- a_sync/a_sync/_kwargs.py +3 -2
- a_sync/a_sync/_meta.py +120 -28
- a_sync/a_sync/abstract.py +102 -28
- a_sync/a_sync/base.py +34 -16
- a_sync/a_sync/config.py +47 -13
- a_sync/a_sync/decorator.py +239 -117
- a_sync/a_sync/function.py +416 -146
- a_sync/a_sync/method.py +197 -59
- a_sync/a_sync/modifiers/__init__.py +47 -5
- a_sync/a_sync/modifiers/cache/__init__.py +46 -17
- a_sync/a_sync/modifiers/cache/memory.py +86 -20
- a_sync/a_sync/modifiers/limiter.py +52 -22
- a_sync/a_sync/modifiers/manager.py +98 -16
- a_sync/a_sync/modifiers/semaphores.py +48 -15
- a_sync/a_sync/property.py +383 -82
- a_sync/a_sync/singleton.py +1 -0
- a_sync/aliases.py +0 -1
- a_sync/asyncio/__init__.py +4 -1
- a_sync/asyncio/as_completed.py +177 -49
- a_sync/asyncio/create_task.py +31 -17
- a_sync/asyncio/gather.py +72 -52
- a_sync/asyncio/utils.py +3 -3
- a_sync/exceptions.py +78 -23
- a_sync/executor.py +120 -71
- a_sync/future.py +575 -158
- a_sync/iter.py +110 -50
- a_sync/primitives/__init__.py +14 -2
- a_sync/primitives/_debug.py +13 -13
- a_sync/primitives/_loggable.py +5 -4
- a_sync/primitives/locks/__init__.py +5 -2
- a_sync/primitives/locks/counter.py +38 -36
- a_sync/primitives/locks/event.py +21 -7
- a_sync/primitives/locks/prio_semaphore.py +182 -62
- a_sync/primitives/locks/semaphore.py +78 -77
- a_sync/primitives/queue.py +560 -58
- a_sync/sphinx/__init__.py +0 -1
- a_sync/sphinx/ext.py +160 -50
- a_sync/task.py +262 -97
- a_sync/utils/__init__.py +12 -6
- a_sync/utils/iterators.py +127 -43
- {ez_a_sync-0.22.13.dist-info → ez_a_sync-0.22.15.dist-info}/METADATA +1 -1
- ez_a_sync-0.22.15.dist-info/RECORD +74 -0
- {ez_a_sync-0.22.13.dist-info → ez_a_sync-0.22.15.dist-info}/WHEEL +1 -1
- tests/conftest.py +1 -2
- tests/executor.py +112 -9
- tests/fixtures.py +61 -32
- tests/test_abstract.py +7 -4
- tests/test_as_completed.py +54 -21
- tests/test_base.py +66 -17
- tests/test_cache.py +31 -15
- tests/test_decorator.py +54 -28
- tests/test_executor.py +8 -13
- tests/test_future.py +45 -8
- tests/test_gather.py +8 -2
- tests/test_helpers.py +2 -0
- tests/test_iter.py +55 -13
- tests/test_limiter.py +5 -3
- tests/test_meta.py +23 -9
- tests/test_modified.py +4 -1
- tests/test_semaphore.py +15 -8
- tests/test_singleton.py +15 -10
- tests/test_task.py +126 -28
- ez_a_sync-0.22.13.dist-info/RECORD +0 -74
- {ez_a_sync-0.22.13.dist-info → ez_a_sync-0.22.15.dist-info}/LICENSE.txt +0 -0
- {ez_a_sync-0.22.13.dist-info → ez_a_sync-0.22.15.dist-info}/top_level.txt +0 -0
|
@@ -7,34 +7,76 @@ from async_lru import alru_cache
|
|
|
7
7
|
from a_sync import exceptions
|
|
8
8
|
from a_sync._typing import *
|
|
9
9
|
|
|
10
|
+
|
|
10
11
|
class CacheKwargs(TypedDict):
|
|
12
|
+
"""Typed dictionary for cache keyword arguments."""
|
|
13
|
+
|
|
11
14
|
maxsize: Optional[int]
|
|
12
15
|
ttl: Optional[int]
|
|
13
16
|
typed: bool
|
|
14
17
|
|
|
18
|
+
|
|
15
19
|
@overload
|
|
16
|
-
def apply_async_memory_cache(
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
20
|
+
def apply_async_memory_cache(**kwargs: Unpack[CacheKwargs]) -> AsyncDecorator[P, T]:
|
|
21
|
+
"""Creates a decorator to apply an asynchronous LRU cache.
|
|
22
|
+
|
|
23
|
+
This overload is used when no coroutine function is provided. The returned
|
|
24
|
+
decorator can be applied to a coroutine function later.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
**kwargs: Keyword arguments for cache configuration, including maxsize,
|
|
28
|
+
ttl, and typed.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
|
|
21
32
|
@overload
|
|
22
33
|
def apply_async_memory_cache(
|
|
23
|
-
coro_fn: int,
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
34
|
+
coro_fn: int, **kwargs: Unpack[CacheKwargs]
|
|
35
|
+
) -> AsyncDecorator[P, T]:
|
|
36
|
+
"""Creates a decorator with maxsize set by an integer.
|
|
37
|
+
|
|
38
|
+
This overload is used when an integer is provided as the coroutine function,
|
|
39
|
+
which sets the maxsize for the cache. The returned decorator can be applied
|
|
40
|
+
to a coroutine function later.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
coro_fn: An integer to set as maxsize for the cache.
|
|
44
|
+
**kwargs: Additional keyword arguments for cache configuration, including
|
|
45
|
+
ttl and typed.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
|
|
27
49
|
@overload
|
|
28
50
|
def apply_async_memory_cache(
|
|
29
|
-
coro_fn: CoroFn[P, T],
|
|
30
|
-
|
|
31
|
-
|
|
51
|
+
coro_fn: CoroFn[P, T], **kwargs: Unpack[CacheKwargs]
|
|
52
|
+
) -> CoroFn[P, T]:
|
|
53
|
+
"""Applies an asynchronous LRU cache to a provided coroutine function.
|
|
54
|
+
|
|
55
|
+
This overload is used when a coroutine function is provided. The cache is
|
|
56
|
+
applied directly to the function.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
coro_fn: The coroutine function to be cached.
|
|
60
|
+
**kwargs: Keyword arguments for cache configuration, including maxsize,
|
|
61
|
+
ttl, and typed.
|
|
62
|
+
"""
|
|
63
|
+
|
|
32
64
|
|
|
33
65
|
@overload
|
|
34
66
|
def apply_async_memory_cache(
|
|
35
|
-
coro_fn: Literal[None],
|
|
36
|
-
|
|
37
|
-
|
|
67
|
+
coro_fn: Literal[None], **kwargs: Unpack[CacheKwargs]
|
|
68
|
+
) -> AsyncDecorator[P, T]:
|
|
69
|
+
"""Creates a decorator to apply an asynchronous LRU cache.
|
|
70
|
+
|
|
71
|
+
This duplicate overload is used when no coroutine function is provided. The
|
|
72
|
+
returned decorator can be applied to a coroutine function later.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
coro_fn: None, indicating no coroutine function is provided.
|
|
76
|
+
**kwargs: Keyword arguments for cache configuration, including maxsize,
|
|
77
|
+
ttl, and typed.
|
|
78
|
+
"""
|
|
79
|
+
|
|
38
80
|
|
|
39
81
|
def apply_async_memory_cache(
|
|
40
82
|
coro_fn: Optional[Union[CoroFn[P, T], int]] = None,
|
|
@@ -42,19 +84,43 @@ def apply_async_memory_cache(
|
|
|
42
84
|
ttl: Optional[int] = None,
|
|
43
85
|
typed: bool = False,
|
|
44
86
|
) -> AsyncDecoratorOrCoroFn[P, T]:
|
|
87
|
+
"""Applies an asynchronous LRU cache to a coroutine function.
|
|
88
|
+
|
|
89
|
+
This function uses the `alru_cache` from the `async_lru` library to cache
|
|
90
|
+
the results of an asynchronous coroutine function. The cache can be configured
|
|
91
|
+
with a maximum size, a time-to-live (TTL), and whether the cache keys should
|
|
92
|
+
be typed.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
coro_fn: The coroutine function to be cached, or an integer to set as maxsize.
|
|
96
|
+
maxsize: The maximum size of the cache. If set to -1, it is converted to None,
|
|
97
|
+
making the cache unbounded.
|
|
98
|
+
ttl: The time-to-live for cache entries in seconds. If None, entries do not expire.
|
|
99
|
+
typed: Whether to consider the types of arguments as part of the cache key.
|
|
100
|
+
|
|
101
|
+
Raises:
|
|
102
|
+
TypeError: If `maxsize` is not a positive integer or None when `coro_fn` is None.
|
|
103
|
+
exceptions.FunctionNotAsync: If `coro_fn` is not an asynchronous function.
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
A decorator if `coro_fn` is None, otherwise the cached coroutine function.
|
|
107
|
+
"""
|
|
45
108
|
# Parse Inputs
|
|
46
109
|
if isinstance(coro_fn, int):
|
|
47
110
|
assert maxsize is None
|
|
48
111
|
maxsize = coro_fn
|
|
49
112
|
coro_fn = None
|
|
50
|
-
|
|
51
|
-
# Validate
|
|
113
|
+
|
|
114
|
+
# Validate
|
|
52
115
|
elif coro_fn is None:
|
|
53
|
-
if not
|
|
54
|
-
raise TypeError(
|
|
116
|
+
if maxsize not in [None, -1] and (not isinstance(maxsize, int) or maxsize <= 0):
|
|
117
|
+
raise TypeError(
|
|
118
|
+
"'lru_cache_maxsize' must be a positive integer or None.", maxsize
|
|
119
|
+
)
|
|
120
|
+
|
|
55
121
|
elif not asyncio.iscoroutinefunction(coro_fn):
|
|
56
122
|
raise exceptions.FunctionNotAsync(coro_fn)
|
|
57
|
-
|
|
123
|
+
|
|
58
124
|
if maxsize == -1:
|
|
59
125
|
maxsize = None
|
|
60
126
|
|
|
@@ -8,48 +8,78 @@ from a_sync import aliases, exceptions
|
|
|
8
8
|
from a_sync._typing import *
|
|
9
9
|
|
|
10
10
|
|
|
11
|
+
LimiterSpec = Union[int, AsyncLimiter]
|
|
12
|
+
|
|
13
|
+
|
|
11
14
|
@overload
|
|
12
15
|
def apply_rate_limit(
|
|
13
|
-
coro_fn: Literal[None],
|
|
14
16
|
runs_per_minute: int,
|
|
15
|
-
) -> AsyncDecorator[P, T]
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
17
|
+
) -> AsyncDecorator[P, T]:
|
|
18
|
+
"""Decorator to apply a rate limit to an asynchronous function.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
runs_per_minute: The number of allowed executions per minute.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
|
|
23
25
|
@overload
|
|
24
26
|
def apply_rate_limit(
|
|
25
27
|
coro_fn: CoroFn[P, T],
|
|
26
|
-
runs_per_minute:
|
|
27
|
-
) -> CoroFn[P, T]
|
|
28
|
-
|
|
28
|
+
runs_per_minute: LimiterSpec,
|
|
29
|
+
) -> CoroFn[P, T]:
|
|
30
|
+
"""Decorator to apply a rate limit to an asynchronous function.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
coro_fn: The coroutine function to be rate-limited.
|
|
34
|
+
runs_per_minute: The number of allowed executions per minute or an AsyncLimiter instance.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
|
|
29
38
|
def apply_rate_limit(
|
|
30
39
|
coro_fn: Optional[Union[CoroFn[P, T], int]] = None,
|
|
31
|
-
runs_per_minute: Optional[
|
|
40
|
+
runs_per_minute: Optional[LimiterSpec] = None,
|
|
32
41
|
) -> AsyncDecoratorOrCoroFn[P, T]:
|
|
42
|
+
"""Applies a rate limit to an asynchronous function.
|
|
43
|
+
|
|
44
|
+
This function can be used as a decorator to limit the number of times
|
|
45
|
+
an asynchronous function can be called per minute. It can be configured
|
|
46
|
+
with either an integer specifying the number of runs per minute or an
|
|
47
|
+
AsyncLimiter instance.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
coro_fn: The coroutine function to be rate-limited. If an integer is provided, it is treated as runs per minute, and runs_per_minute should be None.
|
|
51
|
+
runs_per_minute: The number of allowed executions per minute or an AsyncLimiter instance. If coro_fn is an integer, this should be None.
|
|
52
|
+
|
|
53
|
+
Raises:
|
|
54
|
+
TypeError: If 'runs_per_minute' is neither an integer nor an AsyncLimiter when 'coro_fn' is None.
|
|
55
|
+
exceptions.FunctionNotAsync: If 'coro_fn' is not an asynchronous function.
|
|
56
|
+
"""
|
|
33
57
|
# Parse Inputs
|
|
34
|
-
if isinstance(coro_fn, int):
|
|
58
|
+
if isinstance(coro_fn, (int, AsyncLimiter)):
|
|
35
59
|
assert runs_per_minute is None
|
|
36
60
|
runs_per_minute = coro_fn
|
|
37
61
|
coro_fn = None
|
|
38
|
-
|
|
62
|
+
|
|
39
63
|
elif coro_fn is None:
|
|
40
|
-
if runs_per_minute is not None and not isinstance(
|
|
64
|
+
if runs_per_minute is not None and not isinstance(
|
|
65
|
+
runs_per_minute, (int, AsyncLimiter)
|
|
66
|
+
):
|
|
41
67
|
raise TypeError("'runs_per_minute' must be an integer.", runs_per_minute)
|
|
42
|
-
|
|
68
|
+
|
|
43
69
|
elif not asyncio.iscoroutinefunction(coro_fn):
|
|
44
70
|
raise exceptions.FunctionNotAsync(coro_fn)
|
|
45
|
-
|
|
71
|
+
|
|
46
72
|
def rate_limit_decorator(coro_fn: CoroFn[P, T]) -> CoroFn[P, T]:
|
|
47
|
-
limiter =
|
|
73
|
+
limiter = (
|
|
74
|
+
runs_per_minute
|
|
75
|
+
if isinstance(runs_per_minute, AsyncLimiter)
|
|
76
|
+
else AsyncLimiter(runs_per_minute) if runs_per_minute else aliases.dummy
|
|
77
|
+
)
|
|
78
|
+
|
|
48
79
|
async def rate_limit_wrap(*args: P.args, **kwargs: P.kwargs) -> T:
|
|
49
80
|
async with limiter: # type: ignore [attr-defined]
|
|
50
81
|
return await coro_fn(*args, **kwargs)
|
|
82
|
+
|
|
51
83
|
return rate_limit_wrap
|
|
52
|
-
|
|
84
|
+
|
|
53
85
|
return rate_limit_decorator if coro_fn is None else rate_limit_decorator(coro_fn)
|
|
54
|
-
|
|
55
|
-
|
|
@@ -5,9 +5,20 @@ from a_sync._typing import *
|
|
|
5
5
|
from a_sync.a_sync.config import user_set_default_modifiers, null_modifiers
|
|
6
6
|
from a_sync.a_sync.modifiers import cache, limiter, semaphores
|
|
7
7
|
|
|
8
|
-
valid_modifiers = [
|
|
8
|
+
valid_modifiers = [
|
|
9
|
+
key
|
|
10
|
+
for key in ModifierKwargs.__annotations__
|
|
11
|
+
if not key.startswith("_") and not key.endswith("_")
|
|
12
|
+
]
|
|
13
|
+
|
|
9
14
|
|
|
10
15
|
class ModifierManager(Dict[str, Any]):
|
|
16
|
+
"""Manages modifiers for asynchronous and synchronous functions.
|
|
17
|
+
|
|
18
|
+
This class is responsible for applying modifiers to functions, such as
|
|
19
|
+
caching, rate limiting, and semaphores for asynchronous functions.
|
|
20
|
+
"""
|
|
21
|
+
|
|
11
22
|
default: DefaultMode
|
|
12
23
|
cache_type: CacheType
|
|
13
24
|
cache_typed: bool
|
|
@@ -17,36 +28,74 @@ class ModifierManager(Dict[str, Any]):
|
|
|
17
28
|
semaphore: SemaphoreSpec
|
|
18
29
|
# sync modifiers
|
|
19
30
|
executor: Executor
|
|
20
|
-
|
|
31
|
+
"""This is not applied like a typical modifier. The executor is used to run the sync function in an asynchronous context."""
|
|
32
|
+
|
|
33
|
+
__slots__ = ("_modifiers",)
|
|
34
|
+
|
|
21
35
|
def __init__(self, modifiers: ModifierKwargs) -> None:
|
|
36
|
+
"""Initializes the ModifierManager with the given modifiers.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
modifiers: A dictionary of modifiers to be applied.
|
|
40
|
+
|
|
41
|
+
Raises:
|
|
42
|
+
ValueError: If an unsupported modifier is provided.
|
|
43
|
+
"""
|
|
22
44
|
for key in modifiers.keys():
|
|
23
45
|
if key not in valid_modifiers:
|
|
24
46
|
raise ValueError(f"'{key}' is not a supported modifier.")
|
|
25
47
|
self._modifiers = modifiers
|
|
48
|
+
|
|
26
49
|
def __repr__(self) -> str:
|
|
50
|
+
"""Returns a string representation of the modifiers."""
|
|
27
51
|
return str(self._modifiers)
|
|
52
|
+
|
|
28
53
|
def __getattribute__(self, modifier_key: str) -> Any:
|
|
54
|
+
"""Gets the value of a modifier.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
modifier_key: The key of the modifier to retrieve.
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
The value of the modifier, or the default value if not set.
|
|
61
|
+
"""
|
|
29
62
|
if modifier_key not in valid_modifiers:
|
|
30
63
|
return super().__getattribute__(modifier_key)
|
|
31
|
-
return
|
|
64
|
+
return (
|
|
65
|
+
self[modifier_key] if modifier_key in self else user_defaults[modifier_key]
|
|
66
|
+
)
|
|
32
67
|
|
|
33
|
-
|
|
34
68
|
@property
|
|
35
69
|
def use_limiter(self) -> bool:
|
|
70
|
+
"""Determines if a rate limiter should be used."""
|
|
36
71
|
return self.runs_per_minute != nulls.runs_per_minute
|
|
72
|
+
|
|
37
73
|
@property
|
|
38
74
|
def use_semaphore(self) -> bool:
|
|
75
|
+
"""Determines if a semaphore should be used."""
|
|
39
76
|
return self.semaphore != nulls.semaphore
|
|
77
|
+
|
|
40
78
|
@property
|
|
41
79
|
def use_cache(self) -> bool:
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
80
|
+
"""Determines if caching should be used."""
|
|
81
|
+
return any(
|
|
82
|
+
[
|
|
83
|
+
self.cache_type != nulls.cache_type,
|
|
84
|
+
self.ram_cache_maxsize != nulls.ram_cache_maxsize,
|
|
85
|
+
self.ram_cache_ttl != nulls.ram_cache_ttl,
|
|
86
|
+
self.cache_typed != nulls.cache_typed,
|
|
87
|
+
]
|
|
88
|
+
)
|
|
89
|
+
|
|
49
90
|
def apply_async_modifiers(self, coro_fn: CoroFn[P, T]) -> CoroFn[P, T]:
|
|
91
|
+
"""Applies asynchronous modifiers to a coroutine function.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
coro_fn: The coroutine function to modify.
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
The modified coroutine function.
|
|
98
|
+
"""
|
|
50
99
|
# NOTE: THESE STACK IN REVERSE ORDER
|
|
51
100
|
if self.use_limiter:
|
|
52
101
|
coro_fn = limiter.apply_rate_limit(coro_fn, self.runs_per_minute)
|
|
@@ -55,35 +104,68 @@ class ModifierManager(Dict[str, Any]):
|
|
|
55
104
|
if self.use_cache:
|
|
56
105
|
coro_fn = cache.apply_async_cache(
|
|
57
106
|
coro_fn,
|
|
58
|
-
cache_type=self.cache_type or
|
|
107
|
+
cache_type=self.cache_type or "memory",
|
|
59
108
|
cache_typed=self.cache_typed,
|
|
60
109
|
ram_cache_maxsize=self.ram_cache_maxsize,
|
|
61
|
-
ram_cache_ttl=self.ram_cache_ttl
|
|
110
|
+
ram_cache_ttl=self.ram_cache_ttl,
|
|
62
111
|
)
|
|
63
112
|
return coro_fn
|
|
64
|
-
|
|
113
|
+
|
|
65
114
|
def apply_sync_modifiers(self, function: SyncFn[P, T]) -> SyncFn[P, T]:
|
|
115
|
+
"""Wraps a synchronous function.
|
|
116
|
+
|
|
117
|
+
Note:
|
|
118
|
+
There are no sync modifiers at this time, but they will be added here for convenience.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
function: The synchronous function to wrap.
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
The wrapped synchronous function.
|
|
125
|
+
"""
|
|
126
|
+
|
|
66
127
|
@functools.wraps(function)
|
|
67
128
|
def sync_modifier_wrap(*args: P.args, **kwargs: P.kwargs) -> T:
|
|
68
129
|
return function(*args, **kwargs)
|
|
69
|
-
|
|
130
|
+
|
|
70
131
|
return sync_modifier_wrap
|
|
71
|
-
|
|
132
|
+
|
|
72
133
|
# Dictionary api
|
|
73
134
|
def keys(self) -> KeysView[str]: # type: ignore [override]
|
|
135
|
+
"""Returns the keys of the modifiers."""
|
|
74
136
|
return self._modifiers.keys()
|
|
137
|
+
|
|
75
138
|
def values(self) -> ValuesView[Any]: # type: ignore [override]
|
|
139
|
+
"""Returns the values of the modifiers."""
|
|
76
140
|
return self._modifiers.values()
|
|
141
|
+
|
|
77
142
|
def items(self) -> ItemsView[str, Any]: # type: ignore [override]
|
|
143
|
+
"""Returns the items of the modifiers."""
|
|
78
144
|
return self._modifiers.items()
|
|
145
|
+
|
|
79
146
|
def __contains__(self, key: str) -> bool: # type: ignore [override]
|
|
147
|
+
"""Checks if a key is in the modifiers."""
|
|
80
148
|
return key in self._modifiers
|
|
149
|
+
|
|
81
150
|
def __iter__(self) -> Iterator[str]:
|
|
151
|
+
"""Returns an iterator over the modifier keys."""
|
|
82
152
|
return self._modifiers.__iter__()
|
|
153
|
+
|
|
83
154
|
def __len__(self) -> int:
|
|
155
|
+
"""Returns the number of modifiers."""
|
|
84
156
|
return len(self._modifiers)
|
|
157
|
+
|
|
85
158
|
def __getitem__(self, modifier_key: str):
|
|
159
|
+
"""Gets the value of a modifier by key.
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
modifier_key: The key of the modifier to retrieve.
|
|
163
|
+
|
|
164
|
+
Returns:
|
|
165
|
+
The value of the modifier.
|
|
166
|
+
"""
|
|
86
167
|
return self._modifiers[modifier_key] # type: ignore [literal-required]
|
|
87
168
|
|
|
169
|
+
|
|
88
170
|
nulls = ModifierManager(null_modifiers)
|
|
89
171
|
user_defaults = ModifierManager(user_set_default_modifiers)
|
|
@@ -12,58 +12,91 @@ from a_sync.primitives import ThreadsafeSemaphore, DummySemaphore
|
|
|
12
12
|
|
|
13
13
|
@overload
|
|
14
14
|
def apply_semaphore( # type: ignore [misc]
|
|
15
|
-
coro_fn: Literal[None],
|
|
16
15
|
semaphore: SemaphoreSpec,
|
|
17
|
-
) -> AsyncDecorator[P, T]
|
|
16
|
+
) -> AsyncDecorator[P, T]:
|
|
17
|
+
"""Applies a semaphore to a coroutine function.
|
|
18
|
+
|
|
19
|
+
This overload is used when the semaphore is provided as a single argument,
|
|
20
|
+
returning a decorator that can be applied to a coroutine function.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
semaphore: The semaphore to apply, which can be an integer or an asyncio.Semaphore object.
|
|
24
|
+
"""
|
|
18
25
|
|
|
19
|
-
@overload
|
|
20
|
-
def apply_semaphore(
|
|
21
|
-
coro_fn: SemaphoreSpec,
|
|
22
|
-
semaphore: Literal[None],
|
|
23
|
-
) -> AsyncDecorator[P, T]:...
|
|
24
26
|
|
|
25
27
|
@overload
|
|
26
28
|
def apply_semaphore(
|
|
27
29
|
coro_fn: CoroFn[P, T],
|
|
28
30
|
semaphore: SemaphoreSpec,
|
|
29
|
-
) -> CoroFn[P, T]
|
|
30
|
-
|
|
31
|
+
) -> CoroFn[P, T]:
|
|
32
|
+
"""Applies a semaphore to a coroutine function.
|
|
33
|
+
|
|
34
|
+
This overload is used when both the coroutine function and semaphore are provided,
|
|
35
|
+
directly applying the semaphore to the coroutine function.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
coro_fn: The coroutine function to which the semaphore will be applied.
|
|
39
|
+
semaphore: The semaphore to apply, which can be an integer or an asyncio.Semaphore object.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
|
|
31
43
|
def apply_semaphore(
|
|
32
44
|
coro_fn: Optional[Union[CoroFn[P, T], SemaphoreSpec]] = None,
|
|
33
45
|
semaphore: SemaphoreSpec = None,
|
|
34
46
|
) -> AsyncDecoratorOrCoroFn[P, T]:
|
|
47
|
+
"""Applies a semaphore to a coroutine function or returns a decorator.
|
|
48
|
+
|
|
49
|
+
This function can be used to apply a semaphore to a coroutine function either by
|
|
50
|
+
passing the coroutine function and semaphore as arguments or by using the semaphore
|
|
51
|
+
as a decorator. It raises exceptions if the inputs are not valid.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
coro_fn: The coroutine function to which the semaphore will be applied, or None
|
|
55
|
+
if the semaphore is to be used as a decorator.
|
|
56
|
+
semaphore: The semaphore to apply, which can be an integer or an asyncio.Semaphore object.
|
|
57
|
+
|
|
58
|
+
Raises:
|
|
59
|
+
ValueError: If both coro_fn and semaphore are provided as invalid inputs.
|
|
60
|
+
exceptions.FunctionNotAsync: If the provided function is not a coroutine.
|
|
61
|
+
TypeError: If the semaphore is not an integer or an asyncio.Semaphore object.
|
|
62
|
+
"""
|
|
35
63
|
# Parse Inputs
|
|
36
64
|
if isinstance(coro_fn, (int, asyncio.Semaphore)):
|
|
37
65
|
if semaphore is not None:
|
|
38
66
|
raise ValueError("You can only pass in one arg.")
|
|
39
67
|
semaphore = coro_fn
|
|
40
68
|
coro_fn = None
|
|
41
|
-
|
|
69
|
+
|
|
42
70
|
elif not asyncio.iscoroutinefunction(coro_fn):
|
|
43
71
|
raise exceptions.FunctionNotAsync(coro_fn)
|
|
44
|
-
|
|
72
|
+
|
|
45
73
|
# Create the semaphore if necessary
|
|
46
74
|
if isinstance(semaphore, int):
|
|
47
75
|
semaphore = primitives.ThreadsafeSemaphore(semaphore)
|
|
48
76
|
elif not isinstance(semaphore, asyncio.Semaphore):
|
|
49
|
-
raise TypeError(
|
|
50
|
-
|
|
77
|
+
raise TypeError(
|
|
78
|
+
f"'semaphore' must either be an integer or a Semaphore object. You passed {semaphore}"
|
|
79
|
+
)
|
|
80
|
+
|
|
51
81
|
# Create and return the decorator
|
|
52
82
|
if isinstance(semaphore, primitives.Semaphore):
|
|
53
83
|
# NOTE: Our `Semaphore` primitive can be used as a decorator.
|
|
54
84
|
# While you can use it the `async with` way like any other semaphore and we could make this code section cleaner,
|
|
55
85
|
# applying it as a decorator adds some useful info to its debug logs so we do that here if we can.
|
|
56
86
|
semaphore_decorator = semaphore
|
|
57
|
-
|
|
87
|
+
|
|
58
88
|
else:
|
|
89
|
+
|
|
59
90
|
def semaphore_decorator(coro_fn: CoroFn[P, T]) -> CoroFn[P, T]:
|
|
60
91
|
@functools.wraps(coro_fn)
|
|
61
92
|
async def semaphore_wrap(*args, **kwargs) -> T:
|
|
62
93
|
async with semaphore: # type: ignore [union-attr]
|
|
63
94
|
return await coro_fn(*args, **kwargs)
|
|
95
|
+
|
|
64
96
|
return semaphore_wrap
|
|
65
|
-
|
|
97
|
+
|
|
66
98
|
return semaphore_decorator if coro_fn is None else semaphore_decorator(coro_fn)
|
|
67
99
|
|
|
68
100
|
|
|
69
101
|
dummy_semaphore = primitives.DummySemaphore()
|
|
102
|
+
"""A dummy semaphore that does not enforce any concurrency limits."""
|