mm-std 0.3.19__py3-none-any.whl → 0.3.21__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 +15 -13
- mm_std/concurrency/async_task_runner.py +11 -1
- mm_std/log.py +54 -0
- {mm_std-0.3.19.dist-info → mm_std-0.3.21.dist-info}/METADATA +5 -4
- {mm_std-0.3.19.dist-info → mm_std-0.3.21.dist-info}/RECORD +7 -7
- {mm_std-0.3.19.dist-info → mm_std-0.3.21.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_structlog as configure_structlog
|
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
|
@@ -2,15 +2,18 @@ import asyncio
|
|
2
2
|
from collections.abc import Awaitable, Callable
|
3
3
|
from dataclasses import dataclass, field
|
4
4
|
from datetime import datetime
|
5
|
-
from logging import Logger
|
6
5
|
from typing import Any
|
7
6
|
|
7
|
+
import structlog
|
8
|
+
|
8
9
|
from mm_std.date import utc_now
|
9
10
|
|
10
11
|
type AsyncFunc = Callable[..., Awaitable[object]]
|
11
12
|
type Args = tuple[object, ...]
|
12
13
|
type Kwargs = dict[str, object]
|
13
14
|
|
15
|
+
logger = structlog.stdlib.get_logger()
|
16
|
+
|
14
17
|
|
15
18
|
class AsyncScheduler:
|
16
19
|
"""
|
@@ -34,13 +37,12 @@ class AsyncScheduler:
|
|
34
37
|
last_run: datetime | None = None
|
35
38
|
running: bool = False
|
36
39
|
|
37
|
-
def __init__(self,
|
40
|
+
def __init__(self, name: str = "AsyncScheduler") -> None:
|
38
41
|
"""Initialize the async scheduler."""
|
39
42
|
self.tasks: dict[str, AsyncScheduler.TaskInfo] = {}
|
40
43
|
self._running: bool = False
|
41
44
|
self._tasks: list[asyncio.Task[Any]] = []
|
42
45
|
self._main_task: asyncio.Task[Any] | None = None
|
43
|
-
self._logger = logger
|
44
46
|
self._name = name
|
45
47
|
|
46
48
|
def add_task(self, task_id: str, interval: float, func: AsyncFunc, args: Args = (), kwargs: Kwargs | None = None) -> None:
|
@@ -81,7 +83,7 @@ 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", task_id=task_id, error=task.error_count)
|
85
87
|
|
86
88
|
# Calculate elapsed time and sleep if needed
|
87
89
|
elapsed = (utc_now() - task.last_run).total_seconds()
|
@@ -93,7 +95,7 @@ class AsyncScheduler:
|
|
93
95
|
break
|
94
96
|
finally:
|
95
97
|
task.running = False
|
96
|
-
|
98
|
+
logger.debug("Finished task", 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")
|
@@ -5,6 +5,10 @@ from collections.abc import Awaitable
|
|
5
5
|
from dataclasses import dataclass
|
6
6
|
from typing import Any
|
7
7
|
|
8
|
+
import structlog
|
9
|
+
|
10
|
+
logger = structlog.stdlib.get_logger()
|
11
|
+
|
8
12
|
|
9
13
|
class AsyncTaskRunner:
|
10
14
|
"""
|
@@ -26,17 +30,21 @@ class AsyncTaskRunner:
|
|
26
30
|
task_id: str
|
27
31
|
awaitable: Awaitable[Any]
|
28
32
|
|
29
|
-
def __init__(
|
33
|
+
def __init__(
|
34
|
+
self, max_concurrent_tasks: int, timeout: float | None = None, name: str | None = None, no_logging: bool = False
|
35
|
+
) -> None:
|
30
36
|
"""
|
31
37
|
:param max_concurrent_tasks: Maximum number of tasks that can run concurrently.
|
32
38
|
:param timeout: Optional overall timeout in seconds for running all tasks.
|
33
39
|
:param name: Optional name for the runner.
|
40
|
+
:param no_logging: If True, suppresses logging for task exception.
|
34
41
|
"""
|
35
42
|
if timeout is not None and timeout <= 0:
|
36
43
|
raise ValueError("Timeout must be positive if specified.")
|
37
44
|
self.max_concurrent_tasks: int = max_concurrent_tasks
|
38
45
|
self.timeout: float | None = timeout
|
39
46
|
self.name = name
|
47
|
+
self.no_logging = no_logging
|
40
48
|
self.semaphore: asyncio.Semaphore = asyncio.Semaphore(max_concurrent_tasks)
|
41
49
|
self._tasks: list[AsyncTaskRunner.Task] = []
|
42
50
|
self._was_run: bool = False
|
@@ -92,6 +100,8 @@ class AsyncTaskRunner:
|
|
92
100
|
res: Any = await task.awaitable
|
93
101
|
results[task.task_id] = res
|
94
102
|
except Exception as e:
|
103
|
+
if not self.no_logging:
|
104
|
+
logger.exception("Task raised an exception", task_id=task.task_id)
|
95
105
|
exceptions[task.task_id] = e
|
96
106
|
|
97
107
|
# Create asyncio tasks for all runner tasks
|
mm_std/log.py
CHANGED
@@ -2,6 +2,9 @@ import logging
|
|
2
2
|
from logging.handlers import RotatingFileHandler
|
3
3
|
from pathlib import Path
|
4
4
|
|
5
|
+
import structlog
|
6
|
+
from structlog.typing import Processor
|
7
|
+
|
5
8
|
|
6
9
|
def init_logger(name: str, file_path: str | None = None, file_mkdir: bool = True, level: int = logging.DEBUG) -> logging.Logger:
|
7
10
|
log = logging.getLogger(name)
|
@@ -20,3 +23,54 @@ def init_logger(name: str, file_path: str | None = None, file_mkdir: bool = True
|
|
20
23
|
file_handler.setFormatter(fmt)
|
21
24
|
log.addHandler(file_handler)
|
22
25
|
return log
|
26
|
+
|
27
|
+
|
28
|
+
def configure_structlog(json_logs: bool = False, log_level: str = "DEBUG", compact: bool = False) -> None:
|
29
|
+
timestamper: Processor = structlog.processors.TimeStamper(fmt="%H:%M:%S" if compact else "[%Y-%m-%d %H:%M:%S.%f]")
|
30
|
+
|
31
|
+
shared_processors: list[Processor] = [
|
32
|
+
structlog.contextvars.merge_contextvars,
|
33
|
+
structlog.stdlib.add_logger_name,
|
34
|
+
structlog.stdlib.add_log_level,
|
35
|
+
structlog.stdlib.PositionalArgumentsFormatter(),
|
36
|
+
structlog.stdlib.ExtraAdder(),
|
37
|
+
timestamper,
|
38
|
+
structlog.processors.StackInfoRenderer(),
|
39
|
+
]
|
40
|
+
|
41
|
+
if json_logs:
|
42
|
+
# Format the exception only for JSON logs, as we want to pretty-print them when
|
43
|
+
# using the ConsoleRenderer
|
44
|
+
shared_processors.append(structlog.processors.format_exc_info)
|
45
|
+
|
46
|
+
structlog.configure(
|
47
|
+
processors=shared_processors # noqa: RUF005
|
48
|
+
+ [
|
49
|
+
# Prepare event dict for `ProcessorFormatter`.
|
50
|
+
structlog.stdlib.ProcessorFormatter.wrap_for_formatter,
|
51
|
+
],
|
52
|
+
logger_factory=structlog.stdlib.LoggerFactory(),
|
53
|
+
cache_logger_on_first_use=True,
|
54
|
+
)
|
55
|
+
|
56
|
+
log_renderer: Processor
|
57
|
+
log_renderer = structlog.processors.JSONRenderer() if json_logs else structlog.dev.ConsoleRenderer()
|
58
|
+
|
59
|
+
formatter: structlog.stdlib.ProcessorFormatter = structlog.stdlib.ProcessorFormatter(
|
60
|
+
# These run ONLY on `logging` entries that do NOT originate within
|
61
|
+
# structlog.
|
62
|
+
foreign_pre_chain=shared_processors,
|
63
|
+
# These run on ALL entries after the pre_chain is done.
|
64
|
+
processors=[
|
65
|
+
# Remove _record & _from_structlog.
|
66
|
+
structlog.stdlib.ProcessorFormatter.remove_processors_meta,
|
67
|
+
log_renderer,
|
68
|
+
],
|
69
|
+
)
|
70
|
+
|
71
|
+
handler = logging.StreamHandler()
|
72
|
+
# Use OUR `ProcessorFormatter` to format all `logging` entries.
|
73
|
+
handler.setFormatter(formatter)
|
74
|
+
root_logger: logging.Logger = logging.getLogger()
|
75
|
+
root_logger.addHandler(handler)
|
76
|
+
root_logger.setLevel(log_level.upper())
|
@@ -1,14 +1,15 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: mm-std
|
3
|
-
Version: 0.3.
|
3
|
+
Version: 0.3.21
|
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
|
+
Requires-Dist: structlog>=25.2.0
|
14
15
|
Requires-Dist: tomlkit~=0.13.2
|
@@ -1,4 +1,4 @@
|
|
1
|
-
mm_std/__init__.py,sha256=
|
1
|
+
mm_std/__init__.py,sha256=ATaZN6AzmRUJIc8IE4PichCbh45pNncbBidUG7V1m0Q,3044
|
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=vQzNJ0oenvuHF9sXDI6CUZDR12BcDURUZVwkpdNle4Q,2983
|
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=cTsEY0FJKqlA94Ap-5Yx4KFOc-0QP5lRocn4P2n3cdE,5526
|
24
|
+
mm_std/concurrency/async_task_runner.py,sha256=JpofaXCSqQDgQTh1FvaCWxsUsFWNPJGfXpuTlkF7hS0,4988
|
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.21.dist-info/METADATA,sha256=_VWt4WbyHA-fN5KkHKXpSyu1evey3AtLnrJR4Sbdprs,448
|
29
|
+
mm_std-0.3.21.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
30
|
+
mm_std-0.3.21.dist-info/RECORD,,
|
File without changes
|