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 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, logger: Logger, name: str = "AsyncScheduler") -> None:
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
- self._logger.exception(f"Exception in task {task_id}")
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
- self._logger.debug(f"Task {task_id} stopped")
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
- self._logger.debug("Main scheduler task cancelled")
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
- self._logger.warning("AsyncScheduler already running")
132
+ logger.warning("AsyncScheduler already running")
131
133
  return
132
134
 
133
135
  self._running = True
134
- self._logger.debug("Starting AsyncScheduler")
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
- self._logger.warning("AsyncScheduler not running")
146
+ logger.warning("now running")
145
147
  return
146
148
 
147
- self._logger.debug("Stopping AsyncScheduler")
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
- self._logger.debug("AsyncScheduler stopped")
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
- self._logger.warning("Cannot clear tasks while scheduler is running")
169
+ logger.warning("Cannot clear tasks while scheduler is running")
168
170
  return
169
171
  self.tasks.clear()
170
- self._logger.debug("Cleared all tasks from the scheduler")
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__(self, max_concurrent_tasks: int, timeout: float | None = None, name: str | None = None) -> None:
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.19
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.14
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.0
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.9.4
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=yxfH7ROIU5jwRQEeFIloTlDPZm_8443PKk50xzmQq7c,2984
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=6ux6njNKc_ZCQlvWn1FZR6vcSY2Cem-mQzmNXvsg5IE,913
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=qS3QKMA0xpoxCZWjDW1ItAwKMTQ5h8esXMMRA0eXtxE,5644
24
- mm_std/concurrency/async_task_runner.py,sha256=hO_8umGGKVKfQW_hCzy6Nt0OIBDj7Yfjb8d7CCkmtJU,4643
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.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,,
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,,