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 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, logger: Logger, name: str = "AsyncScheduler") -> None:
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
- self._logger.exception(f"Exception in task {task_id}")
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
- self._logger.debug(f"Task {task_id} stopped")
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
- 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")
@@ -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, logger: Logger | 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 logger: Optional logger for task exceptions.
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.logger = logger
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.logger:
101
- self.logger.exception(f"Task '{self._task_name(task.task_id)}' raised an exception")
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.20
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.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
14
  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=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=6ux6njNKc_ZCQlvWn1FZR6vcSY2Cem-mQzmNXvsg5IE,913
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=qS3QKMA0xpoxCZWjDW1ItAwKMTQ5h8esXMMRA0eXtxE,5644
24
- mm_std/concurrency/async_task_runner.py,sha256=KyTp4aHt-qBc-qJpXIutFaTP9ykupG7uKafghAwbBvg,4948
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.20.dist-info/METADATA,sha256=bcvPFhVCzwNBhB8qz0DW1AZY_jeKqdFIuMiOHHz-22I,415
29
- mm_std-0.3.20.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
30
- mm_std-0.3.20.dist-info/RECORD,,
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,,