mm-std 0.3.9__py3-none-any.whl → 0.3.10__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
@@ -1,3 +1,4 @@
1
+ from .async_concurrency import AsyncScheduler as AsyncScheduler
1
2
  from .command import CommandResult as CommandResult
2
3
  from .command import run_command as run_command
3
4
  from .command import run_ssh_command as run_ssh_command
@@ -19,6 +20,8 @@ from .http_ import CHROME_USER_AGENT as CHROME_USER_AGENT
19
20
  from .http_ import FIREFOX_USER_AGENT as FIREFOX_USER_AGENT
20
21
  from .http_ import HResponse as HResponse
21
22
  from .http_ import add_query_params_to_url as add_query_params_to_url
23
+ from .http_ import ahr as ahr
24
+ from .http_ import async_hrequest as async_hrequest
22
25
  from .http_ import hr as hr
23
26
  from .http_ import hrequest as hrequest
24
27
  from .json_ import CustomJSONEncoder as CustomJSONEncoder
@@ -0,0 +1,107 @@
1
+ import threading
2
+ from collections.abc import Awaitable, Callable
3
+ from dataclasses import dataclass, field
4
+ from datetime import UTC, datetime
5
+ from logging import Logger
6
+
7
+ import anyio
8
+
9
+ # Type aliases for clarity
10
+ AsyncFunc = Callable[..., Awaitable[object]]
11
+ Args = tuple[object, ...]
12
+ Kwargs = dict[str, object]
13
+
14
+
15
+ class AsyncScheduler:
16
+ @dataclass
17
+ class TaskInfo:
18
+ task_id: str
19
+ interval: float
20
+ func: AsyncFunc
21
+ args: Args = ()
22
+ kwargs: Kwargs = field(default_factory=dict)
23
+ run_count: int = 0
24
+ last_run: datetime | None = None
25
+ running: bool = False
26
+
27
+ def __init__(self, logger: Logger) -> None:
28
+ self.tasks: dict[str, AsyncScheduler.TaskInfo] = {}
29
+ self._running: bool = False
30
+ self._cancel_scope: anyio.CancelScope | None = None
31
+ self._thread: threading.Thread | None = None
32
+ self._logger = logger
33
+
34
+ def add_task(self, task_id: str, interval: float, func: AsyncFunc, args: Args = (), kwargs: Kwargs | None = None) -> None:
35
+ """Register a new task with the scheduler."""
36
+ if kwargs is None:
37
+ kwargs = {}
38
+ if task_id in self.tasks:
39
+ raise ValueError(f"Task with id {task_id} already exists")
40
+ self.tasks[task_id] = AsyncScheduler.TaskInfo(task_id=task_id, interval=interval, func=func, args=args, kwargs=kwargs)
41
+
42
+ async def _run_task(self, task_id: str) -> None:
43
+ """Internal loop for running a single task repeatedly."""
44
+ task = self.tasks[task_id]
45
+ while self._running:
46
+ task.last_run = datetime.now(UTC)
47
+ task.run_count += 1
48
+ try:
49
+ await task.func(*task.args, **task.kwargs)
50
+ except Exception:
51
+ self._logger.exception("AsyncScheduler exception")
52
+
53
+ # Calculate elapsed time and sleep if needed so that tasks never overlap.
54
+ elapsed = (datetime.now(UTC) - task.last_run).total_seconds()
55
+ sleep_time = task.interval - elapsed
56
+ if sleep_time > 0:
57
+ try:
58
+ await anyio.sleep(sleep_time)
59
+ except Exception:
60
+ self._logger.exception("AsyncScheduler exception")
61
+
62
+ async def _start_all_tasks(self) -> None:
63
+ """Starts all tasks concurrently in an AnyIO task group."""
64
+ async with anyio.create_task_group() as tg:
65
+ self._cancel_scope = tg.cancel_scope
66
+ for task_id in self.tasks:
67
+ tg.start_soon(self._run_task, task_id)
68
+ try:
69
+ while self._running: # noqa: ASYNC110
70
+ await anyio.sleep(0.1)
71
+ except anyio.get_cancelled_exc_class():
72
+ self._logger.info("Task group cancelled. Exiting _start_all_tasks.")
73
+
74
+ def start(self) -> None:
75
+ """
76
+ Start the scheduler.
77
+
78
+ This method launches the scheduler in a background thread,
79
+ which runs an AnyIO event loop.
80
+ """
81
+ if self._running:
82
+ self._logger.warning("Scheduler already running")
83
+ return
84
+ self._running = True
85
+ self._logger.info("Starting scheduler")
86
+ self._thread = threading.Thread(target=lambda: anyio.run(self._start_all_tasks), daemon=True)
87
+ self._thread.start()
88
+
89
+ def stop(self) -> None:
90
+ """
91
+ Stop the scheduler.
92
+
93
+ The running flag is set to False so that each task's loop will exit.
94
+ This method then waits for the background thread to finish.
95
+ """
96
+ if not self._running:
97
+ self._logger.warning("Scheduler not running")
98
+ return
99
+ self._logger.info("Stopping scheduler")
100
+ self._running = False
101
+ if self._cancel_scope is not None:
102
+ self._cancel_scope.cancel()
103
+
104
+ if self._thread:
105
+ self._thread.join(timeout=5)
106
+ self._thread = None
107
+ self._logger.info("Scheduler stopped")
mm_std/http_.py CHANGED
@@ -3,6 +3,7 @@ from dataclasses import asdict, dataclass, field
3
3
  from typing import Any
4
4
  from urllib.parse import urlencode
5
5
 
6
+ import httpx
6
7
  import pydash
7
8
  import requests
8
9
  from requests.auth import AuthBase
@@ -140,6 +141,62 @@ def hrequest(
140
141
  return HResponse(error=f"exception: {err}")
141
142
 
142
143
 
144
+ async def async_hrequest(
145
+ url: str,
146
+ *,
147
+ method: str = "GET",
148
+ proxy: str | None = None,
149
+ params: dict[str, Any] | None = None,
150
+ headers: dict[str, Any] | None = None,
151
+ cookies: dict[str, Any] | None = None,
152
+ timeout: float = 10, # noqa: ASYNC109
153
+ user_agent: str | None = None,
154
+ json_params: bool = True,
155
+ auth: httpx.Auth | tuple[str, str] | None = None,
156
+ verify: bool = True,
157
+ ) -> HResponse:
158
+ query_params: dict[str, Any] | None = None
159
+ data: dict[str, Any] | None = None
160
+ json_: dict[str, Any] | None = None
161
+ method = method.upper()
162
+ if not headers:
163
+ headers = {}
164
+ if user_agent:
165
+ headers["user-agent"] = user_agent
166
+ if method == "GET":
167
+ query_params = params
168
+ elif json_params:
169
+ json_ = params
170
+ else:
171
+ data = params
172
+
173
+ try:
174
+ async with httpx.AsyncClient(
175
+ proxy=proxy,
176
+ timeout=timeout,
177
+ cookies=cookies,
178
+ auth=auth,
179
+ verify=verify,
180
+ ) as client:
181
+ r = await client.request(
182
+ method,
183
+ url,
184
+ headers=headers,
185
+ params=query_params,
186
+ json=json_,
187
+ data=data,
188
+ )
189
+ return HResponse(code=r.status_code, body=r.text, headers=dict(r.headers))
190
+ except httpx.TimeoutException:
191
+ return HResponse(error="timeout")
192
+ except httpx.ProxyError:
193
+ return HResponse(error="proxy_error")
194
+ except httpx.RequestError as err:
195
+ return HResponse(error=f"connection_error: {err}")
196
+ except Exception as err:
197
+ return HResponse(error=f"exception: {err}")
198
+
199
+
143
200
  def add_query_params_to_url(url: str, params: dict[str, object]) -> str:
144
201
  query_params = urlencode({k: v for k, v in params.items() if v is not None})
145
202
  if query_params:
@@ -148,3 +205,4 @@ def add_query_params_to_url(url: str, params: dict[str, object]) -> str:
148
205
 
149
206
 
150
207
  hr = hrequest
208
+ ahr = async_hrequest
@@ -1,8 +1,11 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mm-std
3
- Version: 0.3.9
3
+ Version: 0.3.10
4
4
  Requires-Python: >=3.12
5
+ Requires-Dist: anyio>=4.9.0
5
6
  Requires-Dist: cryptography~=44.0.2
7
+ Requires-Dist: httpx[socks]>=0.28.1
8
+ Requires-Dist: pydantic-settings>=2.8.1
6
9
  Requires-Dist: pydantic~=2.10.6
7
10
  Requires-Dist: pydash~=8.0.5
8
11
  Requires-Dist: python-dotenv~=1.0.1
@@ -1,4 +1,5 @@
1
- mm_std/__init__.py,sha256=wzKZe6qMG7cmZLqnA-thfr4QaFaPudJzcQKqLsfZeB8,2500
1
+ mm_std/__init__.py,sha256=HBJRK8f5Qz3v3XtwT7r5lvaANNknAuoV1NsPuO9yQDc,2646
2
+ mm_std/async_concurrency.py,sha256=Zp0_tcRhoGJPJ1iueXKTlJHv0IZDDYLgOmpHSvb1kKs,3925
2
3
  mm_std/command.py,sha256=ze286wjUjg0QSTgIu-2WZks53_Vclg69UaYYgPpQvCU,1283
3
4
  mm_std/concurrency.py,sha256=4kKLhde6YQYsjJJjH6K5eMQj6FtegEz55Mo5TmhQMM0,5242
4
5
  mm_std/config.py,sha256=4ox4D2CgGR76bvZ2n2vGQOYUDagFnlKEDb87to5zpxE,1871
@@ -7,7 +8,7 @@ mm_std/date.py,sha256=976eEkSONuNqHQBgSRu8hrtH23tJqztbmHFHLdbP2TY,1879
7
8
  mm_std/dict.py,sha256=6GkhJPXD0LiJDxPcYe6jPdEDw-MN7P7mKu6U5XxwYDk,675
8
9
  mm_std/env.py,sha256=5zaR9VeIfObN-4yfgxoFeU5IM1GDeZZj9SuYf7t9sOA,125
9
10
  mm_std/fs.py,sha256=RwarNRJq3tIMG6LVX_g03hasfYpjYFh_O27oVDt5IPQ,291
10
- mm_std/http_.py,sha256=NkA98bCAqBmjJgoain0KZVORizGwH0TkbtYvjBEsrB4,4305
11
+ mm_std/http_.py,sha256=JkyHZ29EOC6Ulqw_5eBNLmeMrca6NDGueTSwpj96aKM,6003
11
12
  mm_std/json_.py,sha256=Naa6mBE4D0yiQGkPNRrFvndnUH3R7ovw3FeaejWV60o,1196
12
13
  mm_std/log.py,sha256=6ux6njNKc_ZCQlvWn1FZR6vcSY2Cem-mQzmNXvsg5IE,913
13
14
  mm_std/net.py,sha256=qdRCBIDneip6FaPNe5mx31UtYVmzqam_AoUF7ydEyjA,590
@@ -19,6 +20,6 @@ mm_std/str.py,sha256=BEjJ1p5O4-uSYK0h-enasSSDdwzkBbiwdQ4_dsrlEE8,3257
19
20
  mm_std/toml.py,sha256=CNznWKR0bpOxS6e3VB5LGS-Oa9lW-wterkcPUFtPcls,610
20
21
  mm_std/types_.py,sha256=hvZlnvBWyB8CL_MeEWWD0Y0nN677plibYn3yD-5g7xs,99
21
22
  mm_std/zip.py,sha256=axzF1BwcIygtfNNTefZH7hXKaQqwe-ZH3ChuRWr9dnk,396
22
- mm_std-0.3.9.dist-info/METADATA,sha256=QqBM2hxSUenibV7VnHSuu0QHn5eVoKOPGClxbmfklEI,305
23
- mm_std-0.3.9.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
24
- mm_std-0.3.9.dist-info/RECORD,,
23
+ mm_std-0.3.10.dist-info/METADATA,sha256=1i_m2ldTzID9GccEEPB8ZDqs1DOlGsSdVgkoaHhNdZQ,410
24
+ mm_std-0.3.10.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
25
+ mm_std-0.3.10.dist-info/RECORD,,