mm-std 0.3.9__py3-none-any.whl → 0.3.11__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,5 @@
|
|
1
|
+
from .async_concurrency import AsyncScheduler as AsyncScheduler
|
2
|
+
from .async_concurrency import async_synchronized as async_synchronized
|
1
3
|
from .command import CommandResult as CommandResult
|
2
4
|
from .command import run_command as run_command
|
3
5
|
from .command import run_ssh_command as run_ssh_command
|
@@ -19,6 +21,8 @@ from .http_ import CHROME_USER_AGENT as CHROME_USER_AGENT
|
|
19
21
|
from .http_ import FIREFOX_USER_AGENT as FIREFOX_USER_AGENT
|
20
22
|
from .http_ import HResponse as HResponse
|
21
23
|
from .http_ import add_query_params_to_url as add_query_params_to_url
|
24
|
+
from .http_ import ahr as ahr
|
25
|
+
from .http_ import async_hrequest as async_hrequest
|
22
26
|
from .http_ import hr as hr
|
23
27
|
from .http_ import hrequest as hrequest
|
24
28
|
from .json_ import CustomJSONEncoder as CustomJSONEncoder
|
@@ -0,0 +1,124 @@
|
|
1
|
+
import functools
|
2
|
+
import threading
|
3
|
+
from collections.abc import Awaitable, Callable
|
4
|
+
from dataclasses import dataclass, field
|
5
|
+
from datetime import UTC, datetime
|
6
|
+
from logging import Logger
|
7
|
+
from typing import ParamSpec, TypeVar
|
8
|
+
|
9
|
+
import anyio
|
10
|
+
|
11
|
+
# Type aliases for clarity
|
12
|
+
AsyncFunc = Callable[..., Awaitable[object]]
|
13
|
+
Args = tuple[object, ...]
|
14
|
+
Kwargs = dict[str, object]
|
15
|
+
|
16
|
+
|
17
|
+
class AsyncScheduler:
|
18
|
+
@dataclass
|
19
|
+
class TaskInfo:
|
20
|
+
task_id: str
|
21
|
+
interval: float
|
22
|
+
func: AsyncFunc
|
23
|
+
args: Args = ()
|
24
|
+
kwargs: Kwargs = field(default_factory=dict)
|
25
|
+
run_count: int = 0
|
26
|
+
last_run: datetime | None = None
|
27
|
+
running: bool = False
|
28
|
+
|
29
|
+
def __init__(self, logger: Logger) -> None:
|
30
|
+
self.tasks: dict[str, AsyncScheduler.TaskInfo] = {}
|
31
|
+
self._running: bool = False
|
32
|
+
self._cancel_scope: anyio.CancelScope | None = None
|
33
|
+
self._thread: threading.Thread | None = None
|
34
|
+
self._logger = logger
|
35
|
+
|
36
|
+
def add_task(self, task_id: str, interval: float, func: AsyncFunc, args: Args = (), kwargs: Kwargs | None = None) -> None:
|
37
|
+
"""Register a new task with the scheduler."""
|
38
|
+
if kwargs is None:
|
39
|
+
kwargs = {}
|
40
|
+
if task_id in self.tasks:
|
41
|
+
raise ValueError(f"Task with id {task_id} already exists")
|
42
|
+
self.tasks[task_id] = AsyncScheduler.TaskInfo(task_id=task_id, interval=interval, func=func, args=args, kwargs=kwargs)
|
43
|
+
|
44
|
+
async def _run_task(self, task_id: str) -> None:
|
45
|
+
"""Internal loop for running a single task repeatedly."""
|
46
|
+
task = self.tasks[task_id]
|
47
|
+
while self._running:
|
48
|
+
task.last_run = datetime.now(UTC)
|
49
|
+
task.run_count += 1
|
50
|
+
try:
|
51
|
+
await task.func(*task.args, **task.kwargs)
|
52
|
+
except Exception:
|
53
|
+
self._logger.exception("AsyncScheduler exception")
|
54
|
+
|
55
|
+
# Calculate elapsed time and sleep if needed so that tasks never overlap.
|
56
|
+
elapsed = (datetime.now(UTC) - task.last_run).total_seconds()
|
57
|
+
sleep_time = task.interval - elapsed
|
58
|
+
if sleep_time > 0:
|
59
|
+
try:
|
60
|
+
await anyio.sleep(sleep_time)
|
61
|
+
except Exception:
|
62
|
+
self._logger.exception("AsyncScheduler exception")
|
63
|
+
|
64
|
+
async def _start_all_tasks(self) -> None:
|
65
|
+
"""Starts all tasks concurrently in an AnyIO task group."""
|
66
|
+
async with anyio.create_task_group() as tg:
|
67
|
+
self._cancel_scope = tg.cancel_scope
|
68
|
+
for task_id in self.tasks:
|
69
|
+
tg.start_soon(self._run_task, task_id)
|
70
|
+
try:
|
71
|
+
while self._running: # noqa: ASYNC110
|
72
|
+
await anyio.sleep(0.1)
|
73
|
+
except anyio.get_cancelled_exc_class():
|
74
|
+
self._logger.info("Task group cancelled. Exiting _start_all_tasks.")
|
75
|
+
|
76
|
+
def start(self) -> None:
|
77
|
+
"""
|
78
|
+
Start the scheduler.
|
79
|
+
|
80
|
+
This method launches the scheduler in a background thread,
|
81
|
+
which runs an AnyIO event loop.
|
82
|
+
"""
|
83
|
+
if self._running:
|
84
|
+
self._logger.warning("Scheduler already running")
|
85
|
+
return
|
86
|
+
self._running = True
|
87
|
+
self._logger.info("Starting scheduler")
|
88
|
+
self._thread = threading.Thread(target=lambda: anyio.run(self._start_all_tasks), daemon=True)
|
89
|
+
self._thread.start()
|
90
|
+
|
91
|
+
def stop(self) -> None:
|
92
|
+
"""
|
93
|
+
Stop the scheduler.
|
94
|
+
|
95
|
+
The running flag is set to False so that each task's loop will exit.
|
96
|
+
This method then waits for the background thread to finish.
|
97
|
+
"""
|
98
|
+
if not self._running:
|
99
|
+
self._logger.warning("Scheduler not running")
|
100
|
+
return
|
101
|
+
self._logger.info("Stopping scheduler")
|
102
|
+
self._running = False
|
103
|
+
if self._cancel_scope is not None:
|
104
|
+
self._cancel_scope.cancel()
|
105
|
+
|
106
|
+
if self._thread:
|
107
|
+
self._thread.join(timeout=5)
|
108
|
+
self._thread = None
|
109
|
+
self._logger.info("Scheduler stopped")
|
110
|
+
|
111
|
+
|
112
|
+
P = ParamSpec("P")
|
113
|
+
R = TypeVar("R")
|
114
|
+
|
115
|
+
|
116
|
+
def async_synchronized(func: Callable[P, Awaitable[R]]) -> Callable[P, Awaitable[R]]:
|
117
|
+
lock = anyio.Lock()
|
118
|
+
|
119
|
+
@functools.wraps(func)
|
120
|
+
async def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
|
121
|
+
async with lock:
|
122
|
+
return await func(*args, **kwargs)
|
123
|
+
|
124
|
+
return wrapper
|
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.
|
3
|
+
Version: 0.3.11
|
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=
|
1
|
+
mm_std/__init__.py,sha256=BJzVoqux6-YWxGMyXJS34v6CgKh3V7SrpV7eXIUVfbw,2718
|
2
|
+
mm_std/async_concurrency.py,sha256=w2cc35g2jJAew_1QUfoGRFjallFJH8jg6q0Jem92zzs,4313
|
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=
|
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.
|
23
|
-
mm_std-0.3.
|
24
|
-
mm_std-0.3.
|
23
|
+
mm_std-0.3.11.dist-info/METADATA,sha256=JXQBD8DtKcAb1n44v5-JDuv9DM-1bgSHT-xgQUNK1j8,410
|
24
|
+
mm_std-0.3.11.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
25
|
+
mm_std-0.3.11.dist-info/RECORD,,
|
File without changes
|