mm-std 0.3.17__py3-none-any.whl → 0.3.19__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.
- mm_std/__init__.py +1 -0
- mm_std/concurrency/async_decorators.py +36 -0
- mm_std/concurrency/async_scheduler.py +26 -9
- mm_std/concurrency/async_task_runner.py +4 -1
- {mm_std-0.3.17.dist-info → mm_std-0.3.19.dist-info}/METADATA +2 -2
- {mm_std-0.3.17.dist-info → mm_std-0.3.19.dist-info}/RECORD +7 -7
- {mm_std-0.3.17.dist-info → mm_std-0.3.19.dist-info}/WHEEL +0 -0
mm_std/__init__.py
CHANGED
@@ -2,6 +2,7 @@ from .command import CommandResult as CommandResult
|
|
2
2
|
from .command import run_command as run_command
|
3
3
|
from .command import run_ssh_command as run_ssh_command
|
4
4
|
from .concurrency.async_decorators import async_synchronized as async_synchronized
|
5
|
+
from .concurrency.async_decorators import async_synchronized_parameter as async_synchronized_parameter
|
5
6
|
from .concurrency.async_scheduler import AsyncScheduler as AsyncScheduler
|
6
7
|
from .concurrency.async_task_runner import AsyncTaskRunner as AsyncTaskRunner
|
7
8
|
from .concurrency.sync_decorators import synchronized as synchronized
|
@@ -1,5 +1,6 @@
|
|
1
1
|
import asyncio
|
2
2
|
import functools
|
3
|
+
from collections import defaultdict
|
3
4
|
from collections.abc import Awaitable, Callable
|
4
5
|
from typing import ParamSpec, TypeVar
|
5
6
|
|
@@ -16,3 +17,38 @@ def async_synchronized(func: Callable[P, Awaitable[R]]) -> Callable[P, Awaitable
|
|
16
17
|
return await func(*args, **kwargs)
|
17
18
|
|
18
19
|
return wrapper
|
20
|
+
|
21
|
+
|
22
|
+
T = TypeVar("T")
|
23
|
+
|
24
|
+
|
25
|
+
def async_synchronized_parameter[T, **P](
|
26
|
+
arg_index: int = 0, skip_if_locked: bool = False
|
27
|
+
) -> Callable[[Callable[P, Awaitable[T]]], Callable[P, Awaitable[T | None]]]:
|
28
|
+
locks: dict[object, asyncio.Lock] = defaultdict(asyncio.Lock)
|
29
|
+
|
30
|
+
def outer(func: Callable[P, Awaitable[T]]) -> Callable[P, Awaitable[T | None]]:
|
31
|
+
@functools.wraps(func)
|
32
|
+
async def wrapper(*args: P.args, **kwargs: P.kwargs) -> T | None:
|
33
|
+
if len(args) <= arg_index:
|
34
|
+
raise ValueError(f"Function called with fewer than {arg_index + 1} positional arguments")
|
35
|
+
|
36
|
+
key = args[arg_index]
|
37
|
+
|
38
|
+
if skip_if_locked and locks[key].locked():
|
39
|
+
return None
|
40
|
+
|
41
|
+
try:
|
42
|
+
async with locks[key]:
|
43
|
+
return await func(*args, **kwargs)
|
44
|
+
finally:
|
45
|
+
# Clean up the lock if no one is waiting
|
46
|
+
# TODO: I'm not sure if the next like is OK
|
47
|
+
if not locks[key].locked() and not locks[key]._waiters: # noqa: SLF001
|
48
|
+
locks.pop(key, None)
|
49
|
+
|
50
|
+
# Store locks for potential external access
|
51
|
+
wrapper.locks = locks # type: ignore[attr-defined]
|
52
|
+
return wrapper
|
53
|
+
|
54
|
+
return outer
|
@@ -1,11 +1,15 @@
|
|
1
1
|
import asyncio
|
2
|
+
from collections.abc import Awaitable, Callable
|
2
3
|
from dataclasses import dataclass, field
|
3
4
|
from datetime import datetime
|
4
5
|
from logging import Logger
|
5
6
|
from typing import Any
|
6
7
|
|
7
8
|
from mm_std.date import utc_now
|
8
|
-
|
9
|
+
|
10
|
+
type AsyncFunc = Callable[..., Awaitable[object]]
|
11
|
+
type Args = tuple[object, ...]
|
12
|
+
type Kwargs = dict[str, object]
|
9
13
|
|
10
14
|
|
11
15
|
class AsyncScheduler:
|
@@ -30,18 +34,14 @@ class AsyncScheduler:
|
|
30
34
|
last_run: datetime | None = None
|
31
35
|
running: bool = False
|
32
36
|
|
33
|
-
def __init__(self, logger: Logger) -> None:
|
34
|
-
"""
|
35
|
-
Initialize the async scheduler.
|
36
|
-
|
37
|
-
Args:
|
38
|
-
logger: Logger instance for recording scheduler events
|
39
|
-
"""
|
37
|
+
def __init__(self, logger: Logger, name: str = "AsyncScheduler") -> None:
|
38
|
+
"""Initialize the async scheduler."""
|
40
39
|
self.tasks: dict[str, AsyncScheduler.TaskInfo] = {}
|
41
40
|
self._running: bool = False
|
42
41
|
self._tasks: list[asyncio.Task[Any]] = []
|
43
42
|
self._main_task: asyncio.Task[Any] | None = None
|
44
43
|
self._logger = logger
|
44
|
+
self._name = name
|
45
45
|
|
46
46
|
def add_task(self, task_id: str, interval: float, func: AsyncFunc, args: Args = (), kwargs: Kwargs | None = None) -> None:
|
47
47
|
"""
|
@@ -100,7 +100,7 @@ class AsyncScheduler:
|
|
100
100
|
self._tasks = []
|
101
101
|
|
102
102
|
for task_id in self.tasks:
|
103
|
-
task = asyncio.create_task(self._run_task(task_id))
|
103
|
+
task = asyncio.create_task(self._run_task(task_id), name=self._name + "-" + task_id)
|
104
104
|
self._tasks.append(task)
|
105
105
|
|
106
106
|
try:
|
@@ -151,3 +151,20 @@ class AsyncScheduler:
|
|
151
151
|
self._main_task.cancel()
|
152
152
|
|
153
153
|
self._logger.debug("AsyncScheduler stopped")
|
154
|
+
|
155
|
+
def is_running(self) -> bool:
|
156
|
+
"""
|
157
|
+
Check if the scheduler is currently running.
|
158
|
+
|
159
|
+
Returns:
|
160
|
+
True if the scheduler is running, False otherwise
|
161
|
+
"""
|
162
|
+
return self._running
|
163
|
+
|
164
|
+
def clear_tasks(self) -> None:
|
165
|
+
"""Clear all tasks from the scheduler."""
|
166
|
+
if self._running:
|
167
|
+
self._logger.warning("Cannot clear tasks while scheduler is running")
|
168
|
+
return
|
169
|
+
self.tasks.clear()
|
170
|
+
self._logger.debug("Cleared all tasks from the scheduler")
|
@@ -67,6 +67,9 @@ class AsyncTaskRunner:
|
|
67
67
|
self._task_ids.add(task_id)
|
68
68
|
self._tasks.append(AsyncTaskRunner.Task(task_id, awaitable))
|
69
69
|
|
70
|
+
def _task_name(self, task_id: str) -> str:
|
71
|
+
return f"{self.name}-{task_id}" if self.name else task_id
|
72
|
+
|
70
73
|
async def run(self) -> AsyncTaskRunner.Result:
|
71
74
|
"""
|
72
75
|
Executes all added tasks with concurrency limited by the semaphore.
|
@@ -92,7 +95,7 @@ class AsyncTaskRunner:
|
|
92
95
|
exceptions[task.task_id] = e
|
93
96
|
|
94
97
|
# Create asyncio tasks for all runner tasks
|
95
|
-
tasks = [asyncio.create_task(run_task(task)) for task in self._tasks]
|
98
|
+
tasks = [asyncio.create_task(run_task(task), name=self._task_name(task.task_id)) for task in self._tasks]
|
96
99
|
|
97
100
|
try:
|
98
101
|
if self.timeout is not None:
|
@@ -1,12 +1,12 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: mm-std
|
3
|
-
Version: 0.3.
|
3
|
+
Version: 0.3.19
|
4
4
|
Requires-Python: >=3.12
|
5
5
|
Requires-Dist: aiohttp-socks~=0.10.1
|
6
6
|
Requires-Dist: aiohttp~=3.11.14
|
7
7
|
Requires-Dist: cryptography~=44.0.2
|
8
8
|
Requires-Dist: pydantic-settings>=2.8.1
|
9
|
-
Requires-Dist: pydantic~=2.
|
9
|
+
Requires-Dist: pydantic~=2.11.0
|
10
10
|
Requires-Dist: pydash~=8.0.5
|
11
11
|
Requires-Dist: python-dotenv~=1.1.0
|
12
12
|
Requires-Dist: requests[socks]~=2.32.3
|
@@ -1,4 +1,4 @@
|
|
1
|
-
mm_std/__init__.py,sha256=
|
1
|
+
mm_std/__init__.py,sha256=yxfH7ROIU5jwRQEeFIloTlDPZm_8443PKk50xzmQq7c,2984
|
2
2
|
mm_std/command.py,sha256=ze286wjUjg0QSTgIu-2WZks53_Vclg69UaYYgPpQvCU,1283
|
3
3
|
mm_std/config.py,sha256=4ox4D2CgGR76bvZ2n2vGQOYUDagFnlKEDb87to5zpxE,1871
|
4
4
|
mm_std/crypto.py,sha256=jdk0_TCmeU0pPXMyz9xH6kQHSjjZ9GcGClBwQps5vBo,340
|
@@ -19,12 +19,12 @@ mm_std/toml.py,sha256=CNznWKR0bpOxS6e3VB5LGS-Oa9lW-wterkcPUFtPcls,610
|
|
19
19
|
mm_std/types_.py,sha256=9FGd2q47a8M9QQgsWJR1Kq34jLxBAkYSoJuwih4PPqg,257
|
20
20
|
mm_std/zip.py,sha256=axzF1BwcIygtfNNTefZH7hXKaQqwe-ZH3ChuRWr9dnk,396
|
21
21
|
mm_std/concurrency/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
22
|
-
mm_std/concurrency/async_decorators.py,sha256=
|
23
|
-
mm_std/concurrency/async_scheduler.py,sha256=
|
24
|
-
mm_std/concurrency/async_task_runner.py,sha256=
|
22
|
+
mm_std/concurrency/async_decorators.py,sha256=xEpyipzp3ZhaPHtdeTE-Ikrt67SUTFKBE6LQPeoeh6Q,1735
|
23
|
+
mm_std/concurrency/async_scheduler.py,sha256=qS3QKMA0xpoxCZWjDW1ItAwKMTQ5h8esXMMRA0eXtxE,5644
|
24
|
+
mm_std/concurrency/async_task_runner.py,sha256=hO_8umGGKVKfQW_hCzy6Nt0OIBDj7Yfjb8d7CCkmtJU,4643
|
25
25
|
mm_std/concurrency/sync_decorators.py,sha256=syCQBOmN7qPO55yzgJB2rbkh10CVww376hmyvs6e5tA,1080
|
26
26
|
mm_std/concurrency/sync_scheduler.py,sha256=j4tBL_cBI1spr0cZplTA7N2CoYsznuORMeRN8rpR6gY,2407
|
27
27
|
mm_std/concurrency/sync_task_runner.py,sha256=s5JPlLYLGQGHIxy4oDS-PN7O9gcy-yPZFoNm8RQwzcw,1780
|
28
|
-
mm_std-0.3.
|
29
|
-
mm_std-0.3.
|
30
|
-
mm_std-0.3.
|
28
|
+
mm_std-0.3.19.dist-info/METADATA,sha256=DXiV1MyhCD0Hn5tRZYdA9wQPoLutkvLX-d_q_CjJ-Co,415
|
29
|
+
mm_std-0.3.19.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
30
|
+
mm_std-0.3.19.dist-info/RECORD,,
|
File without changes
|