mm-std 0.3.20__py3-none-any.whl → 0.3.22__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_scheduler.py +16 -14
- mm_std/concurrency/async_task_runner.py +8 -6
- mm_std/log.py +63 -0
- {mm_std-0.3.20.dist-info → mm_std-0.3.22.dist-info}/METADATA +4 -4
- {mm_std-0.3.20.dist-info → mm_std-0.3.22.dist-info}/RECORD +7 -7
- {mm_std-0.3.20.dist-info → mm_std-0.3.22.dist-info}/WHEEL +0 -0
mm_std/__init__.py
CHANGED
@@ -29,6 +29,7 @@ from .http_ import hrequest as hrequest
|
|
29
29
|
from .http_ import hrequest_async as hrequest_async
|
30
30
|
from .json_ import CustomJSONEncoder as CustomJSONEncoder
|
31
31
|
from .json_ import json_dumps as json_dumps
|
32
|
+
from .log import configure_logging as configure_logging
|
32
33
|
from .log import init_logger as init_logger
|
33
34
|
from .net import check_port as check_port
|
34
35
|
from .net import get_free_local_port as get_free_local_port
|
@@ -1,8 +1,8 @@
|
|
1
1
|
import asyncio
|
2
|
+
import logging
|
2
3
|
from collections.abc import Awaitable, Callable
|
3
4
|
from dataclasses import dataclass, field
|
4
5
|
from datetime import datetime
|
5
|
-
from logging import Logger
|
6
6
|
from typing import Any
|
7
7
|
|
8
8
|
from mm_std.date import utc_now
|
@@ -11,6 +11,8 @@ type AsyncFunc = Callable[..., Awaitable[object]]
|
|
11
11
|
type Args = tuple[object, ...]
|
12
12
|
type Kwargs = dict[str, object]
|
13
13
|
|
14
|
+
logger = logging.getLogger(__name__)
|
15
|
+
|
14
16
|
|
15
17
|
class AsyncScheduler:
|
16
18
|
"""
|
@@ -34,13 +36,12 @@ class AsyncScheduler:
|
|
34
36
|
last_run: datetime | None = None
|
35
37
|
running: bool = False
|
36
38
|
|
37
|
-
def __init__(self,
|
39
|
+
def __init__(self, name: str = "AsyncScheduler") -> None:
|
38
40
|
"""Initialize the async scheduler."""
|
39
41
|
self.tasks: dict[str, AsyncScheduler.TaskInfo] = {}
|
40
42
|
self._running: bool = False
|
41
43
|
self._tasks: list[asyncio.Task[Any]] = []
|
42
44
|
self._main_task: asyncio.Task[Any] | None = None
|
43
|
-
self._logger = logger
|
44
45
|
self._name = name
|
45
46
|
|
46
47
|
def add_task(self, task_id: str, interval: float, func: AsyncFunc, args: Args = (), kwargs: Kwargs | None = None) -> None:
|
@@ -73,6 +74,7 @@ class AsyncScheduler:
|
|
73
74
|
task = self.tasks[task_id]
|
74
75
|
task.running = True
|
75
76
|
|
77
|
+
elapsed = 0.0
|
76
78
|
try:
|
77
79
|
while self._running:
|
78
80
|
task.last_run = utc_now()
|
@@ -81,11 +83,11 @@ class AsyncScheduler:
|
|
81
83
|
await task.func(*task.args, **task.kwargs)
|
82
84
|
except Exception:
|
83
85
|
task.error_count += 1
|
84
|
-
|
86
|
+
logger.exception("Error in task", extra={"task_id": task_id, "error_count": task.error_count})
|
85
87
|
|
86
88
|
# Calculate elapsed time and sleep if needed
|
87
89
|
elapsed = (utc_now() - task.last_run).total_seconds()
|
88
|
-
sleep_time = max(0, task.interval - elapsed)
|
90
|
+
sleep_time = max(0.0, task.interval - elapsed)
|
89
91
|
if sleep_time > 0:
|
90
92
|
try:
|
91
93
|
await asyncio.sleep(sleep_time)
|
@@ -93,7 +95,7 @@ class AsyncScheduler:
|
|
93
95
|
break
|
94
96
|
finally:
|
95
97
|
task.running = False
|
96
|
-
|
98
|
+
logger.debug("Finished task", extra={"task_id": task_id, "elapsed": elapsed})
|
97
99
|
|
98
100
|
async def _start_all_tasks(self) -> None:
|
99
101
|
"""Starts all tasks concurrently using asyncio tasks."""
|
@@ -108,7 +110,7 @@ class AsyncScheduler:
|
|
108
110
|
while self._running: # noqa: ASYNC110
|
109
111
|
await asyncio.sleep(0.1)
|
110
112
|
except asyncio.CancelledError:
|
111
|
-
|
113
|
+
logger.debug("Cancelled all tasks")
|
112
114
|
finally:
|
113
115
|
# Cancel all running tasks when we exit
|
114
116
|
for task in self._tasks:
|
@@ -127,11 +129,11 @@ class AsyncScheduler:
|
|
127
129
|
Creates tasks in the current event loop for each registered task.
|
128
130
|
"""
|
129
131
|
if self._running:
|
130
|
-
|
132
|
+
logger.warning("AsyncScheduler already running")
|
131
133
|
return
|
132
134
|
|
133
135
|
self._running = True
|
134
|
-
|
136
|
+
logger.debug("starting")
|
135
137
|
self._main_task = asyncio.create_task(self._start_all_tasks())
|
136
138
|
|
137
139
|
def stop(self) -> None:
|
@@ -141,16 +143,16 @@ class AsyncScheduler:
|
|
141
143
|
Cancels all running tasks and waits for them to complete.
|
142
144
|
"""
|
143
145
|
if not self._running:
|
144
|
-
|
146
|
+
logger.warning("now running")
|
145
147
|
return
|
146
148
|
|
147
|
-
|
149
|
+
logger.debug("stopping")
|
148
150
|
self._running = False
|
149
151
|
|
150
152
|
if self._main_task and not self._main_task.done():
|
151
153
|
self._main_task.cancel()
|
152
154
|
|
153
|
-
|
155
|
+
logger.debug("stopped")
|
154
156
|
|
155
157
|
def is_running(self) -> bool:
|
156
158
|
"""
|
@@ -164,7 +166,7 @@ class AsyncScheduler:
|
|
164
166
|
def clear_tasks(self) -> None:
|
165
167
|
"""Clear all tasks from the scheduler."""
|
166
168
|
if self._running:
|
167
|
-
|
169
|
+
logger.warning("Cannot clear tasks while scheduler is running")
|
168
170
|
return
|
169
171
|
self.tasks.clear()
|
170
|
-
|
172
|
+
logger.debug("cleared tasks")
|
@@ -1,11 +1,13 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
3
|
import asyncio
|
4
|
+
import logging
|
4
5
|
from collections.abc import Awaitable
|
5
6
|
from dataclasses import dataclass
|
6
|
-
from logging import Logger
|
7
7
|
from typing import Any
|
8
8
|
|
9
|
+
logger = logging.getLogger(__name__)
|
10
|
+
|
9
11
|
|
10
12
|
class AsyncTaskRunner:
|
11
13
|
"""
|
@@ -28,20 +30,20 @@ class AsyncTaskRunner:
|
|
28
30
|
awaitable: Awaitable[Any]
|
29
31
|
|
30
32
|
def __init__(
|
31
|
-
self, max_concurrent_tasks: int, timeout: float | None = None, name: str | None = None,
|
33
|
+
self, max_concurrent_tasks: int, timeout: float | None = None, name: str | None = None, no_logging: bool = False
|
32
34
|
) -> None:
|
33
35
|
"""
|
34
36
|
:param max_concurrent_tasks: Maximum number of tasks that can run concurrently.
|
35
37
|
:param timeout: Optional overall timeout in seconds for running all tasks.
|
36
38
|
:param name: Optional name for the runner.
|
37
|
-
:param
|
39
|
+
:param no_logging: If True, suppresses logging for task exception.
|
38
40
|
"""
|
39
41
|
if timeout is not None and timeout <= 0:
|
40
42
|
raise ValueError("Timeout must be positive if specified.")
|
41
43
|
self.max_concurrent_tasks: int = max_concurrent_tasks
|
42
44
|
self.timeout: float | None = timeout
|
43
45
|
self.name = name
|
44
|
-
self.
|
46
|
+
self.no_logging = no_logging
|
45
47
|
self.semaphore: asyncio.Semaphore = asyncio.Semaphore(max_concurrent_tasks)
|
46
48
|
self._tasks: list[AsyncTaskRunner.Task] = []
|
47
49
|
self._was_run: bool = False
|
@@ -97,8 +99,8 @@ class AsyncTaskRunner:
|
|
97
99
|
res: Any = await task.awaitable
|
98
100
|
results[task.task_id] = res
|
99
101
|
except Exception as e:
|
100
|
-
if self.
|
101
|
-
|
102
|
+
if not self.no_logging:
|
103
|
+
logger.exception("Task raised an exception", extra={"task_id": task.task_id})
|
102
104
|
exceptions[task.task_id] = e
|
103
105
|
|
104
106
|
# Create asyncio tasks for all runner tasks
|
mm_std/log.py
CHANGED
@@ -2,6 +2,69 @@ import logging
|
|
2
2
|
from logging.handlers import RotatingFileHandler
|
3
3
|
from pathlib import Path
|
4
4
|
|
5
|
+
from rich.logging import RichHandler
|
6
|
+
|
7
|
+
|
8
|
+
class ExtraFormatter(logging.Formatter):
|
9
|
+
def format(self, record: logging.LogRecord) -> str:
|
10
|
+
base = super().format(record)
|
11
|
+
extras = {
|
12
|
+
key: value
|
13
|
+
for key, value in record.__dict__.items()
|
14
|
+
if key not in logging.LogRecord.__dict__
|
15
|
+
and key
|
16
|
+
not in (
|
17
|
+
"name",
|
18
|
+
"msg",
|
19
|
+
"args",
|
20
|
+
"levelname",
|
21
|
+
"levelno",
|
22
|
+
"pathname",
|
23
|
+
"filename",
|
24
|
+
"module",
|
25
|
+
"exc_info",
|
26
|
+
"exc_text",
|
27
|
+
"stack_info",
|
28
|
+
"lineno",
|
29
|
+
"funcName",
|
30
|
+
"created",
|
31
|
+
"msecs",
|
32
|
+
"relativeCreated",
|
33
|
+
"thread",
|
34
|
+
"threadName",
|
35
|
+
"processName",
|
36
|
+
"process",
|
37
|
+
"message",
|
38
|
+
"taskName",
|
39
|
+
"asctime",
|
40
|
+
)
|
41
|
+
}
|
42
|
+
if extras:
|
43
|
+
extras_str = " | " + " ".join(f"{k}={v}" for k, v in extras.items())
|
44
|
+
return base + extras_str
|
45
|
+
return base
|
46
|
+
|
47
|
+
|
48
|
+
def configure_logging(developer_console: bool = False, console_level: int = logging.INFO) -> None:
|
49
|
+
"""
|
50
|
+
Configure the root logger with a custom formatter that includes extra fields.
|
51
|
+
"""
|
52
|
+
logger = logging.getLogger()
|
53
|
+
logger.setLevel(console_level)
|
54
|
+
logger.handlers.clear()
|
55
|
+
|
56
|
+
console_handler: logging.Handler
|
57
|
+
|
58
|
+
if developer_console:
|
59
|
+
console_handler = RichHandler(rich_tracebacks=True, show_time=True, show_level=True, show_path=False)
|
60
|
+
formatter = ExtraFormatter("{message}", style="{")
|
61
|
+
else:
|
62
|
+
console_handler = logging.StreamHandler()
|
63
|
+
formatter = ExtraFormatter("{asctime} - {name} - {levelname} - {message}", datefmt="%Y-%m-%d %H:%M:%S", style="{")
|
64
|
+
console_handler.setFormatter(formatter)
|
65
|
+
|
66
|
+
logger.addHandler(console_handler)
|
67
|
+
|
5
68
|
|
6
69
|
def init_logger(name: str, file_path: str | None = None, file_mkdir: bool = True, level: int = logging.DEBUG) -> logging.Logger:
|
7
70
|
log = logging.getLogger(name)
|
@@ -1,14 +1,14 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: mm-std
|
3
|
-
Version: 0.3.
|
3
|
+
Version: 0.3.22
|
4
4
|
Requires-Python: >=3.12
|
5
5
|
Requires-Dist: aiohttp-socks~=0.10.1
|
6
|
-
Requires-Dist: aiohttp~=3.11.
|
6
|
+
Requires-Dist: aiohttp~=3.11.16
|
7
7
|
Requires-Dist: cryptography~=44.0.2
|
8
8
|
Requires-Dist: pydantic-settings>=2.8.1
|
9
|
-
Requires-Dist: pydantic~=2.11.
|
9
|
+
Requires-Dist: pydantic~=2.11.2
|
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
|
13
|
-
Requires-Dist: rich
|
13
|
+
Requires-Dist: rich>=13.9.0
|
14
14
|
Requires-Dist: tomlkit~=0.13.2
|
@@ -1,4 +1,4 @@
|
|
1
|
-
mm_std/__init__.py,sha256=
|
1
|
+
mm_std/__init__.py,sha256=ETGLXQW17ZnV5i6kdhoNA-1MulHZixpzGIOLlYpiYx0,3040
|
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
|
@@ -8,7 +8,7 @@ mm_std/env.py,sha256=5zaR9VeIfObN-4yfgxoFeU5IM1GDeZZj9SuYf7t9sOA,125
|
|
8
8
|
mm_std/fs.py,sha256=RwarNRJq3tIMG6LVX_g03hasfYpjYFh_O27oVDt5IPQ,291
|
9
9
|
mm_std/http_.py,sha256=cozBUGZcbKp9sZuEnu7bklwa6lTE0RxEUVo_aNt1_kE,7468
|
10
10
|
mm_std/json_.py,sha256=Naa6mBE4D0yiQGkPNRrFvndnUH3R7ovw3FeaejWV60o,1196
|
11
|
-
mm_std/log.py,sha256=
|
11
|
+
mm_std/log.py,sha256=0TkTsAlUTt00gjgukvsvnZRIAGELq0MI6Lv8mKP-Wz4,2887
|
12
12
|
mm_std/net.py,sha256=qdRCBIDneip6FaPNe5mx31UtYVmzqam_AoUF7ydEyjA,590
|
13
13
|
mm_std/print_.py,sha256=zB7sVbSSF8RffMxvnOdbKCXjCKtKzKV3R68pBri4NkQ,1638
|
14
14
|
mm_std/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
@@ -20,11 +20,11 @@ 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
22
|
mm_std/concurrency/async_decorators.py,sha256=xEpyipzp3ZhaPHtdeTE-Ikrt67SUTFKBE6LQPeoeh6Q,1735
|
23
|
-
mm_std/concurrency/async_scheduler.py,sha256=
|
24
|
-
mm_std/concurrency/async_task_runner.py,sha256=
|
23
|
+
mm_std/concurrency/async_scheduler.py,sha256=TSu2WDJ8vfKZ4-saM3iZdcpGz8yPpSmud-7jJ4XKdJs,5579
|
24
|
+
mm_std/concurrency/async_task_runner.py,sha256=EN7tN2enkVYVgDbhSiAr-_W4o9m9wBXCveXGT8aVXII,4994
|
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.22.dist-info/METADATA,sha256=BN3U-377sfY6l7yYDzLyW3RRkYqAU581moSJAfIDU8c,415
|
29
|
+
mm_std-0.3.22.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
30
|
+
mm_std-0.3.22.dist-info/RECORD,,
|
File without changes
|