codex-core 0.1.0__tar.gz → 0.1.1__tar.gz
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.
- {codex_core-0.1.0 → codex_core-0.1.1}/.gitignore +3 -0
- {codex_core-0.1.0 → codex_core-0.1.1}/CHANGELOG.md +13 -0
- {codex_core-0.1.0 → codex_core-0.1.1}/PKG-INFO +12 -2
- codex_core-0.1.1/README.md +38 -0
- {codex_core-0.1.0 → codex_core-0.1.1}/pyproject.toml +1 -0
- codex_core-0.1.1/src/codex_core/common/log_context.py +140 -0
- codex_core-0.1.1/src/codex_core/common/loguru_setup.py +345 -0
- codex_core-0.1.1/src/codex_core/common/phone.py +75 -0
- codex_core-0.1.1/src/codex_core/common/text.py +233 -0
- codex_core-0.1.1/src/codex_core/core/base_dto.py +95 -0
- codex_core-0.1.1/src/codex_core/core/exceptions.py +50 -0
- codex_core-0.1.1/src/codex_core/core/pii.py +146 -0
- codex_core-0.1.1/src/codex_core/settings/base.py +150 -0
- codex_core-0.1.0/README.md +0 -30
- codex_core-0.1.0/src/codex_core/common/log_context.py +0 -55
- codex_core-0.1.0/src/codex_core/common/loguru_setup.py +0 -192
- codex_core-0.1.0/src/codex_core/common/phone.py +0 -40
- codex_core-0.1.0/src/codex_core/common/text.py +0 -139
- codex_core-0.1.0/src/codex_core/core/base_dto.py +0 -33
- codex_core-0.1.0/src/codex_core/core/exceptions.py +0 -7
- codex_core-0.1.0/src/codex_core/core/pii.py +0 -44
- codex_core-0.1.0/src/codex_core/settings/base.py +0 -82
- {codex_core-0.1.0 → codex_core-0.1.1}/.github/workflows/ci.yml +0 -0
- {codex_core-0.1.0 → codex_core-0.1.1}/.github/workflows/docs.yml +0 -0
- {codex_core-0.1.0 → codex_core-0.1.1}/.github/workflows/publish.yml +0 -0
- {codex_core-0.1.0 → codex_core-0.1.1}/.pre-commit-config.yaml +0 -0
- {codex_core-0.1.0 → codex_core-0.1.1}/docs/api/common.md +0 -0
- {codex_core-0.1.0 → codex_core-0.1.1}/docs/api/core.md +0 -0
- {codex_core-0.1.0 → codex_core-0.1.1}/docs/api/index.md +0 -0
- {codex_core-0.1.0 → codex_core-0.1.1}/docs/api/settings.md +0 -0
- {codex_core-0.1.0 → codex_core-0.1.1}/docs/changelog.md +0 -0
- {codex_core-0.1.0 → codex_core-0.1.1}/docs/en_EN/README.md +0 -0
- {codex_core-0.1.0 → codex_core-0.1.1}/docs/en_EN/architecture/README.md +0 -0
- {codex_core-0.1.0 → codex_core-0.1.1}/docs/en_EN/architecture/platform/common.md +0 -0
- {codex_core-0.1.0 → codex_core-0.1.1}/docs/en_EN/architecture/platform/core.md +0 -0
- {codex_core-0.1.0 → codex_core-0.1.1}/docs/en_EN/architecture/platform/settings.md +0 -0
- {codex_core-0.1.0 → codex_core-0.1.1}/docs/en_EN/guide/getting_started.md +0 -0
- {codex_core-0.1.0 → codex_core-0.1.1}/docs/evolution/roadmap.md +0 -0
- {codex_core-0.1.0 → codex_core-0.1.1}/docs/index.md +0 -0
- {codex_core-0.1.0 → codex_core-0.1.1}/docs/ru_RU/README.md +0 -0
- {codex_core-0.1.0 → codex_core-0.1.1}/docs/ru_RU/architecture/README.md +0 -0
- {codex_core-0.1.0 → codex_core-0.1.1}/docs/ru_RU/architecture/platform/common.md +0 -0
- {codex_core-0.1.0 → codex_core-0.1.1}/docs/ru_RU/architecture/platform/core.md +0 -0
- {codex_core-0.1.0 → codex_core-0.1.1}/docs/ru_RU/architecture/platform/settings.md +0 -0
- {codex_core-0.1.0 → codex_core-0.1.1}/docs/ru_RU/guide/getting_started.md +0 -0
- {codex_core-0.1.0 → codex_core-0.1.1}/mkdocs.yml +0 -0
- {codex_core-0.1.0 → codex_core-0.1.1}/src/codex_core/__init__.py +0 -0
- {codex_core-0.1.0 → codex_core-0.1.1}/src/codex_core/common/__init__.py +0 -0
- {codex_core-0.1.0 → codex_core-0.1.1}/src/codex_core/core/__init__.py +0 -0
- {codex_core-0.1.0 → codex_core-0.1.1}/src/codex_core/settings/__init__.py +0 -0
- {codex_core-0.1.0 → codex_core-0.1.1}/tests/conftest.py +0 -0
- {codex_core-0.1.0 → codex_core-0.1.1}/tests/integration/__init__.py +0 -0
- {codex_core-0.1.0 → codex_core-0.1.1}/tests/integration/test_settings_integration.py +0 -0
- {codex_core-0.1.0 → codex_core-0.1.1}/tests/unit/__init__.py +0 -0
- {codex_core-0.1.0 → codex_core-0.1.1}/tests/unit/common/__init__.py +0 -0
- {codex_core-0.1.0 → codex_core-0.1.1}/tests/unit/common/test_phone.py +0 -0
- {codex_core-0.1.0 → codex_core-0.1.1}/tests/unit/common/test_text.py +0 -0
- {codex_core-0.1.0 → codex_core-0.1.1}/tests/unit/core/__init__.py +0 -0
- {codex_core-0.1.0 → codex_core-0.1.1}/tests/unit/core/test_pii.py +0 -0
- {codex_core-0.1.0 → codex_core-0.1.1}/tests/unit/settings/__init__.py +0 -0
- {codex_core-0.1.0 → codex_core-0.1.1}/tests/unit/settings/test_settings.py +0 -0
- {codex_core-0.1.0 → codex_core-0.1.1}/tools/__init__.py +0 -0
- {codex_core-0.1.0 → codex_core-0.1.1}/tools/dev/README.md +0 -0
- {codex_core-0.1.0 → codex_core-0.1.1}/tools/dev/__init__.py +0 -0
- {codex_core-0.1.0 → codex_core-0.1.1}/tools/dev/check.py +0 -0
- {codex_core-0.1.0 → codex_core-0.1.1}/tools/dev/generate_project_tree.py +0 -0
|
@@ -5,6 +5,19 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.1.1] - 2025-02-12
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- **Declarative PII Registry**: Introduced `PIIRegistry` class in `pii.py` for explicit sensitive field name tracking, complementing the heuristic keyword-based matching.
|
|
12
|
+
- **Enhanced Documentation**:
|
|
13
|
+
- Comprehensive Google-style docstrings added to all core modules: `log_context`, `loguru_setup`, `phone`, `text`, `base_dto`, `exceptions`, and `settings/base`.
|
|
14
|
+
- Added localized (EN/RU) documentation links and PyPI/License badges to `README.md`.
|
|
15
|
+
- **Dependency Management**: Added `loguru` as an explicit optional dependency in `pyproject.toml`.
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
- **PII Masking Logic**: Refined `is_pii_field` and `mask_value` to prioritize explicit registry matches over heuristic keyword search.
|
|
19
|
+
- **Project Configuration**: Updated `.gitignore` to include `.claude/` for modern AI tool support.
|
|
20
|
+
|
|
8
21
|
## [0.1.0] - 2024-05-24
|
|
9
22
|
|
|
10
23
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codex-core
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.1
|
|
4
4
|
Summary: Core utilities, schemas and settings for Codex WaaS toolkit
|
|
5
5
|
Project-URL: Homepage, https://github.com/codexdlc/codex-core
|
|
6
6
|
Project-URL: Documentation, https://codexdlc.github.io/codex-core/
|
|
@@ -35,14 +35,24 @@ Requires-Dist: mkdocs-include-markdown-plugin; extra == 'docs'
|
|
|
35
35
|
Requires-Dist: mkdocs-material>=9.0; extra == 'docs'
|
|
36
36
|
Requires-Dist: mkdocs>=1.5; extra == 'docs'
|
|
37
37
|
Requires-Dist: mkdocstrings[python]>=0.24; extra == 'docs'
|
|
38
|
+
Provides-Extra: loguru
|
|
39
|
+
Requires-Dist: loguru>=0.7.0; extra == 'loguru'
|
|
38
40
|
Description-Content-Type: text/markdown
|
|
39
41
|
|
|
40
42
|
# codex-core
|
|
41
43
|
|
|
42
44
|
**Core utilities, schemas, and settings for the Codex WaaS toolkit.**
|
|
43
45
|
|
|
46
|
+
[](https://pypi.org/project/codex-core/)
|
|
47
|
+
[](https://pypi.org/project/codex-core/)
|
|
48
|
+
[](https://github.com/codexdlc/codex-core/blob/main/LICENSE)
|
|
49
|
+
[](https://codexdlc.github.io/codex-core/)
|
|
50
|
+
|
|
44
51
|
This library provides the foundational building blocks used by all other Codex tools. It focuses on Pydantic-based data models, structured logging, and configuration management.
|
|
45
52
|
|
|
53
|
+
> **Documentation:**
|
|
54
|
+
> [EN](https://codexdlc.github.io/codex-core/en_EN/) · [RU](https://codexdlc.github.io/codex-core/ru_RU/) · [API Reference](https://codexdlc.github.io/codex-core/api/) · [Changelog](https://codexdlc.github.io/codex-core/changelog/)
|
|
55
|
+
|
|
46
56
|
## 🚀 Key Features
|
|
47
57
|
|
|
48
58
|
* **Core Interfaces**: Base classes and protocols for Codex components.
|
|
@@ -66,4 +76,4 @@ logger.info("Codex Core is ready!")
|
|
|
66
76
|
```
|
|
67
77
|
|
|
68
78
|
---
|
|
69
|
-
*Part of the [Codex WaaS](https://github.com/codexdlc) ecosystem
|
|
79
|
+
*Part of the [Codex WaaS](https://github.com/codexdlc) ecosystem. · [EN Docs](https://codexdlc.github.io/codex-core/en_EN/) · [RU Docs](https://codexdlc.github.io/codex-core/ru_RU/) · [API](https://codexdlc.github.io/codex-core/api/) · [Changelog](https://codexdlc.github.io/codex-core/changelog/) · [Source](https://github.com/codexdlc/codex-core)*
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# codex-core
|
|
2
|
+
|
|
3
|
+
**Core utilities, schemas, and settings for the Codex WaaS toolkit.**
|
|
4
|
+
|
|
5
|
+
[](https://pypi.org/project/codex-core/)
|
|
6
|
+
[](https://pypi.org/project/codex-core/)
|
|
7
|
+
[](https://github.com/codexdlc/codex-core/blob/main/LICENSE)
|
|
8
|
+
[](https://codexdlc.github.io/codex-core/)
|
|
9
|
+
|
|
10
|
+
This library provides the foundational building blocks used by all other Codex tools. It focuses on Pydantic-based data models, structured logging, and configuration management.
|
|
11
|
+
|
|
12
|
+
> **Documentation:**
|
|
13
|
+
> [EN](https://codexdlc.github.io/codex-core/en_EN/) · [RU](https://codexdlc.github.io/codex-core/ru_RU/) · [API Reference](https://codexdlc.github.io/codex-core/api/) · [Changelog](https://codexdlc.github.io/codex-core/changelog/)
|
|
14
|
+
|
|
15
|
+
## 🚀 Key Features
|
|
16
|
+
|
|
17
|
+
* **Core Interfaces**: Base classes and protocols for Codex components.
|
|
18
|
+
* **Common Utilities**: Logger setup (Loguru), phone number validation, text processing, and caching.
|
|
19
|
+
* **Schemas**: Shared Pydantic models for cross-service communication.
|
|
20
|
+
* **Settings**: Modern configuration management using `pydantic-settings`.
|
|
21
|
+
|
|
22
|
+
## 📦 Installation
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pip install codex-core
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## 🛠️ Quick Start
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
from codex_tools.common.logger import setup_logger
|
|
32
|
+
|
|
33
|
+
logger = setup_logger("my-app")
|
|
34
|
+
logger.info("Codex Core is ready!")
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
*Part of the [Codex WaaS](https://github.com/codexdlc) ecosystem. · [EN Docs](https://codexdlc.github.io/codex-core/en_EN/) · [RU Docs](https://codexdlc.github.io/codex-core/ru_RU/) · [API](https://codexdlc.github.io/codex-core/api/) · [Changelog](https://codexdlc.github.io/codex-core/changelog/) · [Source](https://github.com/codexdlc/codex-core)*
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"""Structured logging context bound to a named task or worker.
|
|
2
|
+
|
|
3
|
+
Provides :class:`TaskLogContext` — a thin, stateful wrapper around
|
|
4
|
+
``logging.Logger`` that automatically enriches every log record with
|
|
5
|
+
a fixed set of structured fields (task name, worker name, arbitrary
|
|
6
|
+
extras).
|
|
7
|
+
|
|
8
|
+
This module intentionally avoids any dependency on Loguru so that it
|
|
9
|
+
works with any log sink: standard-library ``logging``, Loguru (via
|
|
10
|
+
:class:`~codex_core.common.loguru_setup.InterceptHandler`), JSON
|
|
11
|
+
formatters, ELK, or Loki.
|
|
12
|
+
|
|
13
|
+
Example:
|
|
14
|
+
```python
|
|
15
|
+
from codex_core.common.log_context import TaskLogContext
|
|
16
|
+
|
|
17
|
+
log = TaskLogContext("send_booking_notification", worker="notification_worker")
|
|
18
|
+
log.info("Processing appointment", extra={"appointment_id": 123})
|
|
19
|
+
# LogRecord contains: task="send_booking_notification",
|
|
20
|
+
# worker="notification_worker", appointment_id=123
|
|
21
|
+
```
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
import logging
|
|
25
|
+
from typing import Any
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class TaskLogContext:
|
|
29
|
+
"""Structured logging adapter that binds context fields to every record.
|
|
30
|
+
|
|
31
|
+
Wraps a standard ``logging.Logger`` and automatically merges a
|
|
32
|
+
fixed ``_base_extra`` dict (built at construction time) with any
|
|
33
|
+
per-call ``extra`` dict before forwarding the record. This
|
|
34
|
+
eliminates repetitive ``extra={"task": ...}`` boilerplate in
|
|
35
|
+
worker loops.
|
|
36
|
+
|
|
37
|
+
The class is **stateful** in the sense that ``_base_extra`` is
|
|
38
|
+
set once at construction and shared across all log calls. It is
|
|
39
|
+
**not** thread-safe to mutate ``_base_extra`` after construction;
|
|
40
|
+
create a new instance per task if context must change.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
task_name: Logical name of the current operation
|
|
44
|
+
(e.g. ``"send_booking_notification"``). Stored as
|
|
45
|
+
``task`` in every log record's ``extra``.
|
|
46
|
+
logger_name: Name passed to ``logging.getLogger()``.
|
|
47
|
+
Defaults to *task_name* when omitted.
|
|
48
|
+
**extra: Arbitrary keyword arguments added to ``_base_extra``
|
|
49
|
+
alongside ``task`` (e.g. ``worker="notification_worker"``).
|
|
50
|
+
|
|
51
|
+
Example:
|
|
52
|
+
```python
|
|
53
|
+
log = TaskLogContext(
|
|
54
|
+
"slot_calculation",
|
|
55
|
+
logger_name="codex.booking",
|
|
56
|
+
worker="slot_worker",
|
|
57
|
+
tenant_id=42,
|
|
58
|
+
)
|
|
59
|
+
log.debug("Starting slot search")
|
|
60
|
+
log.error("Slot not found", extra={"slot_id": 7})
|
|
61
|
+
```
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
def __init__(self, task_name: str, logger_name: str | None = None, **extra: Any) -> None:
|
|
65
|
+
self._logger = logging.getLogger(logger_name or task_name)
|
|
66
|
+
self._base_extra = {"task": task_name, **extra}
|
|
67
|
+
|
|
68
|
+
def _merge_extra(self, extra: dict[str, Any] | None) -> dict[str, Any]:
|
|
69
|
+
"""Merge per-call extra fields with the base context.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
extra: Optional per-call fields. Keys in *extra* take
|
|
73
|
+
precedence over identically named keys in
|
|
74
|
+
``_base_extra``.
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
A new ``dict`` containing the merged fields.
|
|
78
|
+
"""
|
|
79
|
+
if extra:
|
|
80
|
+
return {**self._base_extra, **extra}
|
|
81
|
+
return self._base_extra
|
|
82
|
+
|
|
83
|
+
def debug(self, msg: str, *args: Any, extra: dict[str, Any] | None = None, **kwargs: Any) -> None:
|
|
84
|
+
"""Emit a ``DEBUG`` record enriched with the bound context fields.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
msg: Log message format string.
|
|
88
|
+
*args: Positional arguments passed to ``Logger.debug``.
|
|
89
|
+
extra: Per-call structured fields merged with ``_base_extra``.
|
|
90
|
+
**kwargs: Additional keyword arguments forwarded to ``Logger.debug``.
|
|
91
|
+
"""
|
|
92
|
+
self._logger.debug(msg, *args, extra=self._merge_extra(extra), **kwargs)
|
|
93
|
+
|
|
94
|
+
def info(self, msg: str, *args: Any, extra: dict[str, Any] | None = None, **kwargs: Any) -> None:
|
|
95
|
+
"""Emit an ``INFO`` record enriched with the bound context fields.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
msg: Log message format string.
|
|
99
|
+
*args: Positional arguments passed to ``Logger.info``.
|
|
100
|
+
extra: Per-call structured fields merged with ``_base_extra``.
|
|
101
|
+
**kwargs: Additional keyword arguments forwarded to ``Logger.info``.
|
|
102
|
+
"""
|
|
103
|
+
self._logger.info(msg, *args, extra=self._merge_extra(extra), **kwargs)
|
|
104
|
+
|
|
105
|
+
def warning(self, msg: str, *args: Any, extra: dict[str, Any] | None = None, **kwargs: Any) -> None:
|
|
106
|
+
"""Emit a ``WARNING`` record enriched with the bound context fields.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
msg: Log message format string.
|
|
110
|
+
*args: Positional arguments passed to ``Logger.warning``.
|
|
111
|
+
extra: Per-call structured fields merged with ``_base_extra``.
|
|
112
|
+
**kwargs: Additional keyword arguments forwarded to ``Logger.warning``.
|
|
113
|
+
"""
|
|
114
|
+
self._logger.warning(msg, *args, extra=self._merge_extra(extra), **kwargs)
|
|
115
|
+
|
|
116
|
+
def error(self, msg: str, *args: Any, extra: dict[str, Any] | None = None, **kwargs: Any) -> None:
|
|
117
|
+
"""Emit an ``ERROR`` record enriched with the bound context fields.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
msg: Log message format string.
|
|
121
|
+
*args: Positional arguments passed to ``Logger.error``.
|
|
122
|
+
extra: Per-call structured fields merged with ``_base_extra``.
|
|
123
|
+
**kwargs: Additional keyword arguments forwarded to ``Logger.error``.
|
|
124
|
+
"""
|
|
125
|
+
self._logger.error(msg, *args, extra=self._merge_extra(extra), **kwargs)
|
|
126
|
+
|
|
127
|
+
def exception(self, msg: str, *args: Any, extra: dict[str, Any] | None = None, **kwargs: Any) -> None:
|
|
128
|
+
"""Emit an ``ERROR`` record with exception traceback and bound context.
|
|
129
|
+
|
|
130
|
+
Equivalent to :meth:`error` but always captures the current
|
|
131
|
+
exception info (``exc_info=True`` is implicit). Call inside an
|
|
132
|
+
``except`` block.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
msg: Log message format string.
|
|
136
|
+
*args: Positional arguments passed to ``Logger.exception``.
|
|
137
|
+
extra: Per-call structured fields merged with ``_base_extra``.
|
|
138
|
+
**kwargs: Additional keyword arguments forwarded to ``Logger.exception``.
|
|
139
|
+
"""
|
|
140
|
+
self._logger.exception(msg, *args, extra=self._merge_extra(extra), **kwargs)
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
"""Application-level Loguru configuration helpers (optional dependency).
|
|
2
|
+
|
|
3
|
+
Provides opinionated, zero-boilerplate Loguru setup for codex_tools
|
|
4
|
+
applications. The codex_core *library* itself never calls these
|
|
5
|
+
helpers; it uses the standard ``logging`` module exclusively so that
|
|
6
|
+
consumers retain full control over their log infrastructure.
|
|
7
|
+
|
|
8
|
+
Three configurable sinks are created by both setup functions:
|
|
9
|
+
|
|
10
|
+
1. **stdout** — colourised, human-readable format for local development.
|
|
11
|
+
2. **debug.log** — rotating plain-text file, ``enqueue=True`` for
|
|
12
|
+
async-safe writing.
|
|
13
|
+
3. **errors.json** — rotating JSON-serialised file capturing ``ERROR``
|
|
14
|
+
and above; suitable for ingestion by ELK / Loki pipelines.
|
|
15
|
+
|
|
16
|
+
Standard-library ``logging`` records are bridged via
|
|
17
|
+
:class:`InterceptHandler` so that third-party libraries (SQLAlchemy,
|
|
18
|
+
httpx, aiogram, etc.) are automatically captured by Loguru.
|
|
19
|
+
|
|
20
|
+
Availability:
|
|
21
|
+
``loguru`` is an **optional** dependency. Both :func:`setup_logging`
|
|
22
|
+
and :func:`setup_universal_logging` raise :exc:`ImportError` with an
|
|
23
|
+
actionable message when ``loguru`` is not installed.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
import logging
|
|
27
|
+
import sys
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
from typing import TYPE_CHECKING, Protocol, runtime_checkable
|
|
30
|
+
|
|
31
|
+
try:
|
|
32
|
+
from loguru import logger
|
|
33
|
+
except ImportError:
|
|
34
|
+
logger = None # type: ignore
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
if TYPE_CHECKING:
|
|
38
|
+
from types import FrameType
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class InterceptHandler(logging.Handler):
|
|
42
|
+
"""Bridge standard-library ``logging`` records to the Loguru sink.
|
|
43
|
+
|
|
44
|
+
Install this handler on the root logger (or any named logger) to
|
|
45
|
+
forward all ``logging``-based records into Loguru transparently.
|
|
46
|
+
The handler resolves the correct call-stack depth so that Loguru
|
|
47
|
+
reports the *original* call site rather than the handler frame.
|
|
48
|
+
|
|
49
|
+
This class is stateless and thread-safe; a single instance may be
|
|
50
|
+
shared across all intercepted loggers.
|
|
51
|
+
|
|
52
|
+
Example:
|
|
53
|
+
```python
|
|
54
|
+
import logging
|
|
55
|
+
from codex_core.common.loguru_setup import InterceptHandler
|
|
56
|
+
|
|
57
|
+
logging.basicConfig(handlers=[InterceptHandler()], level=0, force=True)
|
|
58
|
+
```
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
def emit(self, record: logging.LogRecord) -> None:
|
|
62
|
+
"""Forward a single ``logging.LogRecord`` to the active Loguru logger.
|
|
63
|
+
|
|
64
|
+
Resolves the Loguru level name from the record's level name,
|
|
65
|
+
falls back to the integer level when the name is unknown, then
|
|
66
|
+
walks up the call stack to find the frame that originally issued
|
|
67
|
+
the log call — ensuring Loguru displays the correct source
|
|
68
|
+
location instead of the handler internals.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
record: The log record produced by the standard-library
|
|
72
|
+
logging infrastructure.
|
|
73
|
+
|
|
74
|
+
Note:
|
|
75
|
+
If ``loguru`` is not installed (``logger is None``), this
|
|
76
|
+
method returns silently to avoid masking the original
|
|
77
|
+
``ImportError``.
|
|
78
|
+
"""
|
|
79
|
+
if logger is None:
|
|
80
|
+
return
|
|
81
|
+
|
|
82
|
+
level: str | int
|
|
83
|
+
try:
|
|
84
|
+
level = logger.level(record.levelname).name
|
|
85
|
+
except ValueError:
|
|
86
|
+
level = record.levelno
|
|
87
|
+
|
|
88
|
+
frame: FrameType | None = logging.currentframe()
|
|
89
|
+
depth = 6
|
|
90
|
+
while frame and frame.f_code.co_filename == logging.__file__:
|
|
91
|
+
frame = frame.f_back
|
|
92
|
+
depth += 1
|
|
93
|
+
|
|
94
|
+
logger.opt(depth=depth, exception=record.exc_info).log(level, record.getMessage())
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def setup_universal_logging(
|
|
98
|
+
log_dir: Path,
|
|
99
|
+
service_name: str = "App",
|
|
100
|
+
console_level: str = "INFO",
|
|
101
|
+
file_level: str = "DEBUG",
|
|
102
|
+
rotation: str = "10 MB",
|
|
103
|
+
is_debug: bool = False,
|
|
104
|
+
) -> None:
|
|
105
|
+
"""Configure Loguru with three sinks and standard-library interception.
|
|
106
|
+
|
|
107
|
+
Intended for applications that do not use
|
|
108
|
+
:class:`~codex_core.settings.BaseCommonSettings` but still want the
|
|
109
|
+
full codex_core logging stack. Callers supply raw configuration
|
|
110
|
+
values directly rather than a settings object.
|
|
111
|
+
|
|
112
|
+
Sink layout after the call:
|
|
113
|
+
|
|
114
|
+
- **stdout** — colourised, ``console_level`` threshold.
|
|
115
|
+
- **<log_dir>/debug.log** — rotating plain text, ``file_level``
|
|
116
|
+
threshold, ``backtrace`` / ``diagnose`` enabled when
|
|
117
|
+
``is_debug=True``.
|
|
118
|
+
- **<log_dir>/errors.json** — rotating JSON, ``ERROR`` threshold,
|
|
119
|
+
async-enqueued.
|
|
120
|
+
- **Root logger** — intercepted via :class:`InterceptHandler`.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
log_dir: Directory where log files are created. Created
|
|
124
|
+
recursively if it does not exist.
|
|
125
|
+
service_name: Label embedded in the console format string to
|
|
126
|
+
distinguish output from multiple co-running services.
|
|
127
|
+
console_level: Minimum level for stdout output (e.g. ``"INFO"``).
|
|
128
|
+
file_level: Minimum level for the debug log file
|
|
129
|
+
(e.g. ``"DEBUG"``).
|
|
130
|
+
rotation: Loguru rotation threshold string (e.g. ``"10 MB"`` or
|
|
131
|
+
``"1 day"``).
|
|
132
|
+
is_debug: When ``True``, enables full ``backtrace`` and
|
|
133
|
+
``diagnose`` output in the debug file sink.
|
|
134
|
+
|
|
135
|
+
Raises:
|
|
136
|
+
ImportError: If ``loguru`` is not installed in the environment.
|
|
137
|
+
|
|
138
|
+
Example:
|
|
139
|
+
```python
|
|
140
|
+
from pathlib import Path
|
|
141
|
+
from codex_core.common.loguru_setup import setup_universal_logging
|
|
142
|
+
|
|
143
|
+
setup_universal_logging(
|
|
144
|
+
log_dir=Path("/var/log/myapp"),
|
|
145
|
+
service_name="booking-worker",
|
|
146
|
+
is_debug=True,
|
|
147
|
+
)
|
|
148
|
+
```
|
|
149
|
+
"""
|
|
150
|
+
if logger is None:
|
|
151
|
+
raise ImportError(
|
|
152
|
+
"loguru is not installed. Please install it manually: "
|
|
153
|
+
"pip install loguru"
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
logger.remove()
|
|
157
|
+
log_dir.mkdir(parents=True, exist_ok=True)
|
|
158
|
+
|
|
159
|
+
# 1. Console output
|
|
160
|
+
logger.add(
|
|
161
|
+
sink=sys.stdout,
|
|
162
|
+
level=console_level,
|
|
163
|
+
colorize=True,
|
|
164
|
+
format=(
|
|
165
|
+
"<green>{time:YYYY-MM-DD HH:mm:ss}</green> | "
|
|
166
|
+
"<level>{level: <8}</level> | "
|
|
167
|
+
f"<magenta>{service_name}</magenta> | "
|
|
168
|
+
"<cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> - "
|
|
169
|
+
"<level>{message}</level>"
|
|
170
|
+
),
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
# 2. Debug file
|
|
174
|
+
logger.add(
|
|
175
|
+
sink=str(log_dir / "debug.log"),
|
|
176
|
+
level=file_level,
|
|
177
|
+
rotation=rotation,
|
|
178
|
+
compression="zip",
|
|
179
|
+
format="{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {name}:{function}:{line} - {message}",
|
|
180
|
+
enqueue=True,
|
|
181
|
+
backtrace=is_debug,
|
|
182
|
+
diagnose=is_debug,
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
# 3. Errors file (JSON)
|
|
186
|
+
logger.add(
|
|
187
|
+
sink=str(log_dir / "errors.json"),
|
|
188
|
+
level="ERROR",
|
|
189
|
+
serialize=True,
|
|
190
|
+
rotation=rotation,
|
|
191
|
+
compression="zip",
|
|
192
|
+
enqueue=True,
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
# 4. Intercept standard logging
|
|
196
|
+
logging.basicConfig(handlers=[InterceptHandler()], level=0, force=True)
|
|
197
|
+
|
|
198
|
+
logger.info(f"Loguru setup complete for {service_name}. Logs: {log_dir}")
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
@runtime_checkable
|
|
202
|
+
class LoggingSettingsProtocol(Protocol):
|
|
203
|
+
"""Structural protocol describing the logging-related subset of settings.
|
|
204
|
+
|
|
205
|
+
Any settings object that exposes these five attributes satisfies this
|
|
206
|
+
protocol and can be passed to :func:`setup_logging` without explicit
|
|
207
|
+
inheritance. :class:`~codex_core.settings.BaseCommonSettings`
|
|
208
|
+
satisfies this protocol out of the box when its subclass adds the
|
|
209
|
+
required logging fields.
|
|
210
|
+
|
|
211
|
+
Attributes:
|
|
212
|
+
log_level_console: Minimum Loguru level for stdout
|
|
213
|
+
(e.g. ``"INFO"``).
|
|
214
|
+
log_level_file: Minimum Loguru level for the rotating debug
|
|
215
|
+
file (e.g. ``"DEBUG"``).
|
|
216
|
+
log_rotation: Loguru rotation threshold
|
|
217
|
+
(e.g. ``"10 MB"`` or ``"1 day"``).
|
|
218
|
+
log_dir: Base directory for log files. A service-name
|
|
219
|
+
subdirectory is appended automatically by
|
|
220
|
+
:func:`setup_logging`.
|
|
221
|
+
debug: When ``True``, enables ``backtrace`` and ``diagnose``
|
|
222
|
+
in the file sink.
|
|
223
|
+
"""
|
|
224
|
+
|
|
225
|
+
log_level_console: str
|
|
226
|
+
log_level_file: str
|
|
227
|
+
log_rotation: str
|
|
228
|
+
log_dir: str
|
|
229
|
+
debug: bool
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def setup_logging(
|
|
233
|
+
settings: LoggingSettingsProtocol,
|
|
234
|
+
service_name: str,
|
|
235
|
+
intercept_loggers: list[str] | None = None,
|
|
236
|
+
log_levels: dict[str, int] | None = None,
|
|
237
|
+
) -> None:
|
|
238
|
+
"""Configure Loguru from a settings object conforming to :class:`LoggingSettingsProtocol`.
|
|
239
|
+
|
|
240
|
+
Preferred entry-point for applications that use
|
|
241
|
+
:class:`~codex_core.settings.BaseCommonSettings`. Configuration
|
|
242
|
+
is read from *settings* rather than raw arguments, enabling
|
|
243
|
+
environment-driven log levels without code changes.
|
|
244
|
+
|
|
245
|
+
PII masking is **not** handled here; it is the responsibility of
|
|
246
|
+
:class:`~codex_core.core.base_dto.BaseDTO.__repr__` at the DTO
|
|
247
|
+
level. Do not log raw user input through this logger.
|
|
248
|
+
|
|
249
|
+
Side effects:
|
|
250
|
+
- Removes all existing Loguru handlers (``logger.remove()``).
|
|
251
|
+
- Creates ``<settings.log_dir>/<service_name>/`` directory tree.
|
|
252
|
+
- Replaces the root ``logging`` handler with
|
|
253
|
+
:class:`InterceptHandler`.
|
|
254
|
+
- Optionally replaces handlers on loggers listed in
|
|
255
|
+
*intercept_loggers*.
|
|
256
|
+
- Optionally sets log levels on loggers listed in *log_levels*.
|
|
257
|
+
|
|
258
|
+
Args:
|
|
259
|
+
settings: Any object satisfying :class:`LoggingSettingsProtocol`.
|
|
260
|
+
Typically a subclass of
|
|
261
|
+
:class:`~codex_core.settings.BaseCommonSettings`.
|
|
262
|
+
service_name: Identifies the service in log output and is used
|
|
263
|
+
as the leaf directory name under ``settings.log_dir``.
|
|
264
|
+
intercept_loggers: Optional list of logger names whose handlers
|
|
265
|
+
should be replaced with :class:`InterceptHandler`
|
|
266
|
+
(e.g. ``["aiogram", "sqlalchemy.engine"]``).
|
|
267
|
+
log_levels: Optional mapping of ``{logger_name: level_int}``
|
|
268
|
+
for silencing verbose third-party libraries
|
|
269
|
+
(e.g. ``{"httpx": logging.WARNING}``).
|
|
270
|
+
|
|
271
|
+
Raises:
|
|
272
|
+
ImportError: If ``loguru`` is not installed in the environment.
|
|
273
|
+
|
|
274
|
+
Example:
|
|
275
|
+
```python
|
|
276
|
+
import logging
|
|
277
|
+
from codex_core.common.loguru_setup import setup_logging
|
|
278
|
+
|
|
279
|
+
setup_logging(
|
|
280
|
+
settings=app_settings,
|
|
281
|
+
service_name="api",
|
|
282
|
+
intercept_loggers=["uvicorn", "sqlalchemy.engine"],
|
|
283
|
+
log_levels={"httpx": logging.WARNING},
|
|
284
|
+
)
|
|
285
|
+
```
|
|
286
|
+
"""
|
|
287
|
+
if logger is None:
|
|
288
|
+
raise ImportError(
|
|
289
|
+
"loguru is not installed. Please install it manually: "
|
|
290
|
+
"pip install loguru"
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
logger.remove()
|
|
294
|
+
|
|
295
|
+
log_dir = Path(settings.log_dir) / service_name
|
|
296
|
+
log_dir.mkdir(parents=True, exist_ok=True)
|
|
297
|
+
|
|
298
|
+
# Console
|
|
299
|
+
logger.add(
|
|
300
|
+
sink=sys.stdout,
|
|
301
|
+
level=settings.log_level_console,
|
|
302
|
+
colorize=True,
|
|
303
|
+
format=(
|
|
304
|
+
"<green>{time:YYYY-MM-DD HH:mm:ss}</green> | "
|
|
305
|
+
"<level>{level: <8}</level> | "
|
|
306
|
+
f"<magenta>{service_name}</magenta> | "
|
|
307
|
+
"<cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> - "
|
|
308
|
+
"<level>{message}</level>"
|
|
309
|
+
),
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
# Debug file
|
|
313
|
+
logger.add(
|
|
314
|
+
sink=str(log_dir / "debug.log"),
|
|
315
|
+
level=settings.log_level_file,
|
|
316
|
+
rotation=settings.log_rotation,
|
|
317
|
+
compression="zip",
|
|
318
|
+
format="{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {name}:{function}:{line} - {message}",
|
|
319
|
+
enqueue=True,
|
|
320
|
+
backtrace=settings.debug,
|
|
321
|
+
diagnose=settings.debug,
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
# Errors file (JSON)
|
|
325
|
+
logger.add(
|
|
326
|
+
sink=str(log_dir / "errors.json"),
|
|
327
|
+
level="ERROR",
|
|
328
|
+
serialize=True,
|
|
329
|
+
rotation=settings.log_rotation,
|
|
330
|
+
compression="zip",
|
|
331
|
+
enqueue=True,
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
# Intercept standard logging
|
|
335
|
+
logging.basicConfig(handlers=[InterceptHandler()], level=0, force=True)
|
|
336
|
+
|
|
337
|
+
# Intercept specified loggers
|
|
338
|
+
if intercept_loggers:
|
|
339
|
+
for name in intercept_loggers:
|
|
340
|
+
logging.getLogger(name).handlers = [InterceptHandler()]
|
|
341
|
+
|
|
342
|
+
# Set levels for noisy libraries
|
|
343
|
+
if log_levels:
|
|
344
|
+
for name, level in log_levels.items():
|
|
345
|
+
logging.getLogger(name).setLevel(level)
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""Utilities for normalizing phone numbers to a canonical digit-only format.
|
|
2
|
+
|
|
3
|
+
Framework-agnostic (zero Django / framework dependencies). Suitable
|
|
4
|
+
for use in any Python 3.10+ environment including async workers.
|
|
5
|
+
|
|
6
|
+
The module intentionally does not validate that the resulting number is
|
|
7
|
+
reachable (no libphonenumber dependency); it only ensures a consistent
|
|
8
|
+
digit-only representation that can be safely stored, compared, and
|
|
9
|
+
passed to SMS / telephony APIs.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def normalize_phone(phone: str, default_country: str = "49") -> str:
|
|
14
|
+
"""Normalize a phone number to a digit-only international string.
|
|
15
|
+
|
|
16
|
+
Handles the three most common input variants encountered in
|
|
17
|
+
European / German-locale data:
|
|
18
|
+
|
|
19
|
+
1. **International ``+`` prefix** — ``+49 151 1234567``
|
|
20
|
+
2. **International ``00`` prefix** — ``0049 151 1234567``
|
|
21
|
+
3. **Local ``0`` prefix** — ``0151 1234567`` (expanded using
|
|
22
|
+
*default_country*)
|
|
23
|
+
4. **Already normalized** — ``491511234567`` (returned as-is)
|
|
24
|
+
|
|
25
|
+
Non-digit, non-plus characters (spaces, hyphens, parentheses) are
|
|
26
|
+
stripped before prefix detection.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
phone: Raw phone string in any common format. Empty string or
|
|
30
|
+
strings containing no digits/plus return ``""``.
|
|
31
|
+
default_country: ITU-T country code (digits only, no ``+``)
|
|
32
|
+
prepended when a local ``0``-prefix number is detected.
|
|
33
|
+
Defaults to ``"49"`` (Germany).
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
Digit-only string representing the international phone number,
|
|
37
|
+
or an empty string if the input is blank or contains no
|
|
38
|
+
recognizable digits.
|
|
39
|
+
|
|
40
|
+
Note:
|
|
41
|
+
No length or reachability validation is performed. The caller
|
|
42
|
+
is responsible for validating that the result conforms to the
|
|
43
|
+
expected E.164 length for the target country.
|
|
44
|
+
|
|
45
|
+
Example:
|
|
46
|
+
```python
|
|
47
|
+
normalize_phone("0151 1234567") # → "491511234567"
|
|
48
|
+
normalize_phone("+49 151 1234567") # → "491511234567"
|
|
49
|
+
normalize_phone("0049 151 1234567") # → "491511234567"
|
|
50
|
+
normalize_phone("+1-800-555-0100", "1") # → "18005550100"
|
|
51
|
+
normalize_phone("") # → ""
|
|
52
|
+
```
|
|
53
|
+
"""
|
|
54
|
+
if not phone:
|
|
55
|
+
return ""
|
|
56
|
+
|
|
57
|
+
# Keep only digits and plus (if present at start)
|
|
58
|
+
cleaned = "".join(c for c in phone if c.isdigit() or c == "+")
|
|
59
|
+
if not cleaned:
|
|
60
|
+
return ""
|
|
61
|
+
|
|
62
|
+
# 1. Already has '+' -> remove it and return
|
|
63
|
+
if cleaned.startswith("+"):
|
|
64
|
+
return cleaned.replace("+", "")
|
|
65
|
+
|
|
66
|
+
# 2. Starts with '00' (European international format)
|
|
67
|
+
if cleaned.startswith("00"):
|
|
68
|
+
return cleaned[2:]
|
|
69
|
+
|
|
70
|
+
# 3. Starts with a single '0' (local format, e.g. German)
|
|
71
|
+
if cleaned.startswith("0"):
|
|
72
|
+
return default_country + cleaned[1:]
|
|
73
|
+
|
|
74
|
+
# 4. Already normalized number without plus (e.g. 49151...)
|
|
75
|
+
return cleaned
|