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 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
- from mm_std.types_ import Args, AsyncFunc, Kwargs
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.17
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.10.6
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=wQywfba2Uxe5-IKqz3pvaR-p5miYM9DRQ0Qsdq23GTQ,2881
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=srJXjngf7InLSyD4sazDPNNtZBx9JddxDmH9WZi7s7g,452
23
- mm_std/concurrency/async_scheduler.py,sha256=bqC43svC_vIPV3U6iUxUzB6tdmEC2OZ2kYKld5sf9QE,5015
24
- mm_std/concurrency/async_task_runner.py,sha256=zYC2Jv5taUh8dnyDfWwh394SkzTXtdE9hOhvjV2FWKc,4493
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.17.dist-info/METADATA,sha256=R2m2TLzehKG-9kPy56CyRhOL_ndpu_aUMjx6eSsH3PY,415
29
- mm_std-0.3.17.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
30
- mm_std-0.3.17.dist-info/RECORD,,
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,,