codex-core 0.2.2__tar.gz → 0.4.0__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.
Files changed (90) hide show
  1. {codex_core-0.2.2 → codex_core-0.4.0}/CHANGELOG.md +18 -0
  2. {codex_core-0.2.2 → codex_core-0.4.0}/PKG-INFO +1 -1
  3. {codex_core-0.2.2 → codex_core-0.4.0}/docs/en/architecture/platform/dev.md +39 -9
  4. {codex_core-0.2.2 → codex_core-0.4.0}/docs/ru/architecture/platform/dev.md +39 -9
  5. {codex_core-0.2.2 → codex_core-0.4.0}/pyproject.toml +9 -0
  6. {codex_core-0.2.2 → codex_core-0.4.0}/src/codex_core/common/__init__.py +4 -1
  7. {codex_core-0.2.2 → codex_core-0.4.0}/src/codex_core/common/log_context.py +46 -12
  8. codex_core-0.4.0/src/codex_core/common/loguru_setup.py +307 -0
  9. codex_core-0.4.0/src/codex_core/dev/check_runner.py +544 -0
  10. {codex_core-0.2.2 → codex_core-0.4.0}/src/codex_core/dev/static_compiler/compiler.py +12 -5
  11. codex_core-0.4.0/src/codex_core/dev/static_compiler/js.py +226 -0
  12. codex_core-0.4.0/tests/unit/common/test_log_context.py +98 -0
  13. codex_core-0.4.0/tests/unit/common/test_loguru_setup.py +221 -0
  14. codex_core-0.4.0/tests/unit/dev/__init__.py +1 -0
  15. codex_core-0.4.0/tests/unit/dev/test_check_runner.py +94 -0
  16. codex_core-0.4.0/tests/unit/dev/test_static_compiler.py +174 -0
  17. {codex_core-0.2.2 → codex_core-0.4.0}/tools/dev/README.md +2 -9
  18. codex_core-0.4.0/tools/dev/check.py +13 -0
  19. codex_core-0.2.2/src/codex_core/common/loguru_setup.py +0 -343
  20. codex_core-0.2.2/src/codex_core/dev/check_runner.py +0 -276
  21. codex_core-0.2.2/src/codex_core/dev/static_compiler/js.py +0 -56
  22. codex_core-0.2.2/tests/unit/common/test_log_context.py +0 -53
  23. codex_core-0.2.2/tests/unit/common/test_loguru_setup.py +0 -140
  24. codex_core-0.2.2/tools/dev/check.py +0 -22
  25. {codex_core-0.2.2 → codex_core-0.4.0}/.github/workflows/ci.yml +0 -0
  26. {codex_core-0.2.2 → codex_core-0.4.0}/.github/workflows/docs.yml +0 -0
  27. {codex_core-0.2.2 → codex_core-0.4.0}/.github/workflows/publish.yml +0 -0
  28. {codex_core-0.2.2 → codex_core-0.4.0}/.gitignore +0 -0
  29. {codex_core-0.2.2 → codex_core-0.4.0}/.pre-commit-config.yaml +0 -0
  30. {codex_core-0.2.2 → codex_core-0.4.0}/.python-version +0 -0
  31. {codex_core-0.2.2 → codex_core-0.4.0}/README.md +0 -0
  32. {codex_core-0.2.2 → codex_core-0.4.0}/docs/changelog.md +0 -0
  33. {codex_core-0.2.2 → codex_core-0.4.0}/docs/en/README.md +0 -0
  34. {codex_core-0.2.2 → codex_core-0.4.0}/docs/en/api/common.md +0 -0
  35. {codex_core-0.2.2 → codex_core-0.4.0}/docs/en/api/core.md +0 -0
  36. {codex_core-0.2.2 → codex_core-0.4.0}/docs/en/api/dev/check_runner.md +0 -0
  37. {codex_core-0.2.2 → codex_core-0.4.0}/docs/en/api/dev/index.md +0 -0
  38. {codex_core-0.2.2 → codex_core-0.4.0}/docs/en/api/dev/project_tree.md +0 -0
  39. {codex_core-0.2.2 → codex_core-0.4.0}/docs/en/api/dev/static_compiler.md +0 -0
  40. {codex_core-0.2.2 → codex_core-0.4.0}/docs/en/api/index.md +0 -0
  41. {codex_core-0.2.2 → codex_core-0.4.0}/docs/en/api/settings.md +0 -0
  42. {codex_core-0.2.2 → codex_core-0.4.0}/docs/en/architecture/README.md +0 -0
  43. {codex_core-0.2.2 → codex_core-0.4.0}/docs/en/architecture/platform/common.md +0 -0
  44. {codex_core-0.2.2 → codex_core-0.4.0}/docs/en/architecture/platform/core.md +0 -0
  45. {codex_core-0.2.2 → codex_core-0.4.0}/docs/en/architecture/platform/settings.md +0 -0
  46. {codex_core-0.2.2 → codex_core-0.4.0}/docs/en/tasks/getting_started.md +0 -0
  47. {codex_core-0.2.2 → codex_core-0.4.0}/docs/evolution/roadmap.md +0 -0
  48. {codex_core-0.2.2 → codex_core-0.4.0}/docs/index.md +0 -0
  49. {codex_core-0.2.2 → codex_core-0.4.0}/docs/planning/python_version_policy.md +0 -0
  50. {codex_core-0.2.2 → codex_core-0.4.0}/docs/ru/README.md +0 -0
  51. {codex_core-0.2.2 → codex_core-0.4.0}/docs/ru/architecture/README.md +0 -0
  52. {codex_core-0.2.2 → codex_core-0.4.0}/docs/ru/architecture/platform/common.md +0 -0
  53. {codex_core-0.2.2 → codex_core-0.4.0}/docs/ru/architecture/platform/core.md +0 -0
  54. {codex_core-0.2.2 → codex_core-0.4.0}/docs/ru/architecture/platform/settings.md +0 -0
  55. {codex_core-0.2.2 → codex_core-0.4.0}/docs/ru/tasks/getting_started.md +0 -0
  56. {codex_core-0.2.2 → codex_core-0.4.0}/docs/stylesheets/extra.css +0 -0
  57. {codex_core-0.2.2 → codex_core-0.4.0}/mkdocs.yml +0 -0
  58. {codex_core-0.2.2 → codex_core-0.4.0}/project_structure.txt +0 -0
  59. {codex_core-0.2.2 → codex_core-0.4.0}/src/codex_core/__init__.py +0 -0
  60. {codex_core-0.2.2 → codex_core-0.4.0}/src/codex_core/common/phone.py +0 -0
  61. {codex_core-0.2.2 → codex_core-0.4.0}/src/codex_core/common/text.py +0 -0
  62. {codex_core-0.2.2 → codex_core-0.4.0}/src/codex_core/core/__init__.py +0 -0
  63. {codex_core-0.2.2 → codex_core-0.4.0}/src/codex_core/core/base_dto.py +0 -0
  64. {codex_core-0.2.2 → codex_core-0.4.0}/src/codex_core/core/exceptions.py +0 -0
  65. {codex_core-0.2.2 → codex_core-0.4.0}/src/codex_core/core/pii.py +0 -0
  66. {codex_core-0.2.2 → codex_core-0.4.0}/src/codex_core/dev/__init__.py +0 -0
  67. {codex_core-0.2.2 → codex_core-0.4.0}/src/codex_core/dev/project_tree.py +0 -0
  68. {codex_core-0.2.2 → codex_core-0.4.0}/src/codex_core/dev/static_compiler/__init__.py +0 -0
  69. {codex_core-0.2.2 → codex_core-0.4.0}/src/codex_core/dev/static_compiler/css.py +0 -0
  70. {codex_core-0.2.2 → codex_core-0.4.0}/src/codex_core/py.typed +0 -0
  71. {codex_core-0.2.2 → codex_core-0.4.0}/src/codex_core/settings/__init__.py +0 -0
  72. {codex_core-0.2.2 → codex_core-0.4.0}/src/codex_core/settings/base.py +0 -0
  73. {codex_core-0.2.2 → codex_core-0.4.0}/tests/conftest.py +0 -0
  74. {codex_core-0.2.2 → codex_core-0.4.0}/tests/integration/__init__.py +0 -0
  75. {codex_core-0.2.2 → codex_core-0.4.0}/tests/integration/conftest.py +0 -0
  76. {codex_core-0.2.2 → codex_core-0.4.0}/tests/integration/test_settings_integration.py +0 -0
  77. {codex_core-0.2.2 → codex_core-0.4.0}/tests/unit/__init__.py +0 -0
  78. {codex_core-0.2.2 → codex_core-0.4.0}/tests/unit/common/__init__.py +0 -0
  79. {codex_core-0.2.2 → codex_core-0.4.0}/tests/unit/common/test_phone.py +0 -0
  80. {codex_core-0.2.2 → codex_core-0.4.0}/tests/unit/common/test_text.py +0 -0
  81. {codex_core-0.2.2 → codex_core-0.4.0}/tests/unit/conftest.py +0 -0
  82. {codex_core-0.2.2 → codex_core-0.4.0}/tests/unit/core/__init__.py +0 -0
  83. {codex_core-0.2.2 → codex_core-0.4.0}/tests/unit/core/test_exceptions.py +0 -0
  84. {codex_core-0.2.2 → codex_core-0.4.0}/tests/unit/core/test_pii.py +0 -0
  85. {codex_core-0.2.2 → codex_core-0.4.0}/tests/unit/settings/__init__.py +0 -0
  86. {codex_core-0.2.2 → codex_core-0.4.0}/tests/unit/settings/test_settings.py +0 -0
  87. {codex_core-0.2.2 → codex_core-0.4.0}/tools/__init__.py +0 -0
  88. {codex_core-0.2.2 → codex_core-0.4.0}/tools/dev/__init__.py +0 -0
  89. {codex_core-0.2.2 → codex_core-0.4.0}/tools/dev/generate_project_tree.py +0 -0
  90. {codex_core-0.2.2 → codex_core-0.4.0}/uv.lock +0 -0
@@ -5,6 +5,24 @@ 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.3.0] - 2026-04-04
9
+
10
+ ### Added
11
+
12
+ - **Quality Gate**: Declarative configuration support via `pyproject.toml` (`[tool.codex-check]`).
13
+ - **Static Compiler**: Dependency-graph strategy for JS bundles (@provides/@depends parsing and resolution).
14
+ - **Static Compiler**: New `compile_bundle` entry point for unified asset compilation.
15
+
16
+ ### Changed
17
+
18
+ - **Quality Gate**: Refactored `BaseCheckRunner` to prioritize `pyproject.toml` over class attributes.
19
+ - **Developer Tools**: Simplified `tools/dev/check.py` to a thin launcher.
20
+ - **Documentation**: Updated architecture guides to promote declarative configuration.
21
+
22
+ ### Fixed
23
+
24
+ - **Quality Gate**: Resolved line length issues and suppressed security warnings (Bandit B603) for internal subprocess calls.
25
+
8
26
  ## [0.2.2] - 2026-03-29
9
27
 
10
28
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codex-core
3
- Version: 0.2.2
3
+ Version: 0.4.0
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/
@@ -40,22 +40,29 @@ python tools/dev/check.py --ci # everything non-interactively (GitHub A
40
40
 
41
41
  ### Project switches
42
42
 
43
- Projects can keep the shared orchestration and declare their local policy with
44
- simple boolean attributes on `CheckRunner`:
43
+ The preferred way to declare project-level quality gate policy is via `pyproject.toml` using the `[tool.codex-check]` section:
44
+
45
+ ```toml
46
+ [tool.codex-check]
47
+ project-name = "my-project"
48
+ audit-flags = "--skip-editable"
49
+ run-lint = true
50
+ run-types = true
51
+ run-security = true
52
+ run-unit-tests = true
53
+ run-integration-tests = false
54
+ test-stages = ["unit", "integration"]
55
+ prompt-test-stages = ["integration"]
56
+ ```
57
+
58
+ Legacy support for class attributes in `CheckRunner` is still available for backwards compatibility:
45
59
 
46
60
  ```python
47
61
  class CheckRunner(BaseCheckRunner):
48
- RUN_LINT = True
49
- RUN_TYPES = True
50
- RUN_SECURITY = True
51
- RUN_EXTRA_CHECKS = True
52
62
  RUN_UNIT_TESTS = True
53
63
  RUN_INTEGRATION_TESTS = False
54
64
  ```
55
65
 
56
- This is the preferred extension point when a library does not need every stage.
57
- Use method overrides only when behavior itself must change.
58
-
59
66
  ### Usage in a project
60
67
 
61
68
  ```python
@@ -171,6 +178,29 @@ JS source files into bundles. No Node.js, no external packages required.
171
178
  }
172
179
  ```
173
180
 
181
+ JS also supports a dependency-aware mode:
182
+
183
+ ```json
184
+ {
185
+ "js": {
186
+ "app.js": {
187
+ "strategy": "dependency_graph",
188
+ "entry": ["app/entry.js"],
189
+ "roots": ["core", "widgets", "builders", "app"]
190
+ }
191
+ }
192
+ }
193
+ ```
194
+
195
+ In this mode the compiler reads metadata from JS files:
196
+
197
+ ```js
198
+ /* @provides cabinet.widgets.client_lookup
199
+ @depends cabinet.core.dom, cabinet.core.events */
200
+ ```
201
+
202
+ and builds the bundle order automatically. The old ordered-mode with an explicit source list remains fully supported.
203
+
174
204
  Old CSS-only format is supported for backwards compatibility:
175
205
  ```json
176
206
  { "base.css": "app.css" }
@@ -40,22 +40,29 @@ python tools/dev/check.py --ci # всё без промптов (GitHub
40
40
 
41
41
  ### Проектные переключатели
42
42
 
43
- Проект может оставить общую оркестрацию и задать свою локальную политику через
44
- булевы атрибуты в `CheckRunner`:
43
+ Рекомендуемый способ задания проектной политики проверки качества — через секцию `[tool.codex-check]` в файле `pyproject.toml`:
44
+
45
+ ```toml
46
+ [tool.codex-check]
47
+ project-name = "my-project"
48
+ audit-flags = "--skip-editable"
49
+ run-lint = true
50
+ run-types = true
51
+ run-security = true
52
+ run-unit-tests = true
53
+ run-integration-tests = false
54
+ test-stages = ["unit", "integration"]
55
+ prompt-test-stages = ["integration"]
56
+ ```
57
+
58
+ Поддержка атрибутов класса в `CheckRunner` сохранена для обратной совместимости:
45
59
 
46
60
  ```python
47
61
  class CheckRunner(BaseCheckRunner):
48
- RUN_LINT = True
49
- RUN_TYPES = True
50
- RUN_SECURITY = True
51
- RUN_EXTRA_CHECKS = True
52
62
  RUN_UNIT_TESTS = True
53
63
  RUN_INTEGRATION_TESTS = False
54
64
  ```
55
65
 
56
- Это основной способ настройки, когда библиотеке не нужны все этапы проверки.
57
- Переопределяйте методы только тогда, когда нужно менять само поведение.
58
-
59
66
  ### Использование в проекте
60
67
 
61
68
  ```python
@@ -171,6 +178,29 @@ gen = ProjectTreeGenerator(
171
178
  }
172
179
  ```
173
180
 
181
+ Для JS также поддерживается dependency-aware режим:
182
+
183
+ ```json
184
+ {
185
+ "js": {
186
+ "app.js": {
187
+ "strategy": "dependency_graph",
188
+ "entry": ["app/entry.js"],
189
+ "roots": ["core", "widgets", "builders", "app"]
190
+ }
191
+ }
192
+ }
193
+ ```
194
+
195
+ В этом режиме компилятор читает metadata из JS-файлов:
196
+
197
+ ```js
198
+ /* @provides cabinet.widgets.client_lookup
199
+ @depends cabinet.core.dom, cabinet.core.events */
200
+ ```
201
+
202
+ И строит итоговый порядок автоматически. Старый ordered-mode через явный список файлов остаётся полностью поддержанным.
203
+
174
204
  Старый формат только для CSS поддерживается для обратной совместимости:
175
205
  ```json
176
206
  { "base.css": "app.css" }
@@ -110,3 +110,12 @@ exclude_lines = [
110
110
  [tool.bandit]
111
111
  exclude_dirs = ["tests"]
112
112
  skips = ["B602", "B605", "B607", "B404"]
113
+
114
+ [tool.codex-check]
115
+ project-name = "codex-core"
116
+ audit-flags = "--skip-editable --ignore-vuln CVE-2026-4539"
117
+ integration-requires = "Redis"
118
+ test-stages = ["unit", "integration"]
119
+ prompt-test-stages = ["integration"]
120
+ test-paths = ["tests"]
121
+ types-paths = ["src"]
@@ -1,6 +1,6 @@
1
1
  """Common utilities: logging, caching, phone normalization, text processing."""
2
2
 
3
- from .log_context import TaskLogContext
3
+ from .log_context import TaskLogContext, clear_log_context, get_log_context, set_log_context
4
4
  from .loguru_setup import (
5
5
  InterceptHandler,
6
6
  LoggingSettingsProtocol,
@@ -12,6 +12,9 @@ from .text import clean_string, normalize_name, sanitize_for_sms, transliterate
12
12
 
13
13
  __all__ = [
14
14
  "TaskLogContext",
15
+ "clear_log_context",
16
+ "get_log_context",
17
+ "set_log_context",
15
18
  "InterceptHandler",
16
19
  "LoggingSettingsProtocol",
17
20
  "setup_logging",
@@ -1,29 +1,63 @@
1
- """Structured logging context bound to a named task or worker.
1
+ """Structured logging context: per-request contextvars and per-task wrappers.
2
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).
3
+ Two complementary mechanisms:
7
4
 
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.
5
+ 1. **contextvars-based context** — ``set_log_context`` / ``clear_log_context`` /
6
+ ``get_log_context``. Designed for async request/task scoping: set fields once
7
+ at the start of a request or worker task, and every ``logger.*()`` call in that
8
+ async context automatically includes them via the Loguru patcher configured in
9
+ :func:`~codex_core.common.loguru_setup.setup_logging`.
12
10
 
13
- Example:
11
+ 2. **TaskLogContext** — a stateful wrapper around ``logging.Logger`` for code that
12
+ uses the standard library directly (see class docstring for details).
13
+
14
+ Example (contextvars):
15
+ ```python
16
+ from codex_core.common.log_context import set_log_context, clear_log_context
17
+
18
+ set_log_context(request_id="abc-123", char_id=42)
19
+ logger.info("CombatMoveSubmitted") # extra contains request_id, char_id
20
+ clear_log_context()
21
+ ```
22
+
23
+ Example (TaskLogContext):
14
24
  ```python
15
25
  from codex_core.common.log_context import TaskLogContext
16
26
 
17
27
  log = TaskLogContext("send_booking_notification", worker="notification_worker")
18
28
  log.info("Processing appointment", extra={"appointment_id": 123})
19
- # LogRecord contains: task="send_booking_notification",
20
- # worker="notification_worker", appointment_id=123
21
29
  ```
22
30
  """
23
31
 
32
+ import contextvars
24
33
  import logging
25
34
  from typing import Any
26
35
 
36
+ # ---------------------------------------------------------------------------
37
+ # Async-safe contextvars log context
38
+ # ---------------------------------------------------------------------------
39
+
40
+ _log_context: contextvars.ContextVar[dict[str, Any]] = contextvars.ContextVar(
41
+ "log_context", default={}
42
+ )
43
+
44
+
45
+ def set_log_context(**kwargs: Any) -> None:
46
+ """Merge *kwargs* into the current async-local log context."""
47
+ current = _log_context.get().copy()
48
+ current.update(kwargs)
49
+ _log_context.set(current)
50
+
51
+
52
+ def clear_log_context() -> None:
53
+ """Reset the async-local log context to empty."""
54
+ _log_context.set({})
55
+
56
+
57
+ def get_log_context() -> dict[str, Any]:
58
+ """Return a shallow copy of the current async-local log context."""
59
+ return _log_context.get().copy()
60
+
27
61
 
28
62
  class TaskLogContext:
29
63
  """Structured logging adapter that binds context fields to every record.
@@ -0,0 +1,307 @@
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
+ **Dev mode** (``debug=True``): three sinks — colourised stdout, rotating
9
+ plain-text ``debug.log``, and JSON ``errors.json``.
10
+
11
+ **Prod mode** (``debug=False``): a single JSON-serialised stdout sink
12
+ designed for ingestion by Grafana Alloy / Loki / ELK. File sinks are
13
+ omitted because container runtimes capture stdout natively.
14
+
15
+ Both modes inject a **patcher** that enriches every log record with:
16
+
17
+ - ``service`` — the service name passed to the setup function.
18
+ - Any fields set via :func:`~codex_core.common.log_context.set_log_context`
19
+ (request_id, char_id, correlation_id, etc.).
20
+
21
+ A built-in **healthcheck filter** suppresses records whose ``extra``
22
+ contains ``healthcheck=True``, preventing ``/health`` endpoint noise
23
+ from reaching log aggregators.
24
+
25
+ Standard-library ``logging`` records are bridged via
26
+ :class:`InterceptHandler` so that third-party libraries (SQLAlchemy,
27
+ httpx, aiogram, etc.) are automatically captured by Loguru.
28
+
29
+ Availability:
30
+ ``loguru`` is an **optional** dependency. Both :func:`setup_logging`
31
+ and :func:`setup_universal_logging` raise :exc:`ImportError` with an
32
+ actionable message when ``loguru`` is not installed.
33
+ """
34
+
35
+ import logging
36
+ import sys
37
+ from pathlib import Path
38
+ from typing import TYPE_CHECKING, Any, Protocol, runtime_checkable
39
+
40
+ from .log_context import get_log_context
41
+
42
+ if TYPE_CHECKING:
43
+ from types import FrameType
44
+
45
+ from loguru import Logger
46
+
47
+ logger: "Logger | None"
48
+ try:
49
+ from loguru import logger as _loguru_logger
50
+ except ImportError:
51
+ logger = None
52
+ else:
53
+ logger = _loguru_logger
54
+
55
+
56
+ # ---------------------------------------------------------------------------
57
+ # Helpers
58
+ # ---------------------------------------------------------------------------
59
+
60
+
61
+ def _make_context_patcher(service_name: str) -> Any:
62
+ """Return a Loguru patcher that injects service name and contextvars."""
63
+
64
+ def _patcher(record: dict[str, Any]) -> None:
65
+ record["extra"]["service"] = service_name
66
+ record["extra"].update(get_log_context())
67
+
68
+ return _patcher
69
+
70
+
71
+ def _healthcheck_filter(record: dict[str, Any]) -> bool:
72
+ """Suppress log records tagged with ``healthcheck=True``."""
73
+ return not record["extra"].get("healthcheck", False)
74
+
75
+
76
+ # ---------------------------------------------------------------------------
77
+ # InterceptHandler
78
+ # ---------------------------------------------------------------------------
79
+
80
+
81
+ class InterceptHandler(logging.Handler):
82
+ """Bridge standard-library ``logging`` records to the Loguru sink.
83
+
84
+ Install this handler on the root logger (or any named logger) to
85
+ forward all ``logging``-based records into Loguru transparently.
86
+ The handler resolves the correct call-stack depth so that Loguru
87
+ reports the *original* call site rather than the handler frame.
88
+
89
+ This class is stateless and thread-safe; a single instance may be
90
+ shared across all intercepted loggers.
91
+
92
+ Example:
93
+ ```python
94
+ import logging
95
+ from codex_core.common.loguru_setup import InterceptHandler
96
+
97
+ logging.basicConfig(handlers=[InterceptHandler()], level=0, force=True)
98
+ ```
99
+ """
100
+
101
+ def emit(self, record: logging.LogRecord) -> None:
102
+ if logger is None:
103
+ return
104
+
105
+ level: str | int
106
+ try:
107
+ level = logger.level(record.levelname).name
108
+ except ValueError:
109
+ level = record.levelno
110
+
111
+ frame: FrameType | None = logging.currentframe()
112
+ depth = 6
113
+ while frame and frame.f_code.co_filename == logging.__file__:
114
+ frame = frame.f_back
115
+ depth += 1
116
+
117
+ logger.opt(depth=depth, exception=record.exc_info).log(level, record.getMessage())
118
+
119
+
120
+ # ---------------------------------------------------------------------------
121
+ # setup_universal_logging (raw-args variant)
122
+ # ---------------------------------------------------------------------------
123
+
124
+ _CONSOLE_FORMAT = (
125
+ "<green>{time:YYYY-MM-DD HH:mm:ss}</green> | "
126
+ "<level>{level: <8}</level> | "
127
+ "<magenta>{extra[service]}</magenta> | "
128
+ "<cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> - "
129
+ "<level>{message}</level>"
130
+ )
131
+
132
+ _FILE_FORMAT = "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {name}:{function}:{line} - {message}"
133
+
134
+
135
+ def setup_universal_logging(
136
+ log_dir: Path,
137
+ service_name: str = "App",
138
+ console_level: str = "INFO",
139
+ file_level: str = "DEBUG",
140
+ rotation: str = "10 MB",
141
+ is_debug: bool = False,
142
+ ) -> None:
143
+ """Configure Loguru with three sinks and standard-library interception.
144
+
145
+ Intended for applications that do not use
146
+ :class:`~codex_core.settings.BaseCommonSettings` but still want the
147
+ full codex_core logging stack. Callers supply raw configuration
148
+ values directly rather than a settings object.
149
+
150
+ Args:
151
+ log_dir: Directory where log files are created.
152
+ service_name: Label embedded in the console format string.
153
+ console_level: Minimum level for stdout output.
154
+ file_level: Minimum level for the debug log file.
155
+ rotation: Loguru rotation threshold string.
156
+ is_debug: When ``True``, enables ``backtrace`` and ``diagnose``.
157
+
158
+ Raises:
159
+ ImportError: If ``loguru`` is not installed.
160
+ """
161
+ if logger is None:
162
+ raise ImportError("loguru is not installed. Please install it manually: pip install loguru")
163
+
164
+ logger.remove()
165
+ log_dir.mkdir(parents=True, exist_ok=True)
166
+
167
+ logger.configure(patcher=_make_context_patcher(service_name))
168
+
169
+ logger.add(
170
+ sink=sys.stdout,
171
+ level=console_level,
172
+ colorize=True,
173
+ filter=_healthcheck_filter,
174
+ format=_CONSOLE_FORMAT,
175
+ )
176
+
177
+ logger.add(
178
+ sink=str(log_dir / "debug.log"),
179
+ level=file_level,
180
+ rotation=rotation,
181
+ compression="zip",
182
+ format=_FILE_FORMAT,
183
+ enqueue=True,
184
+ backtrace=is_debug,
185
+ diagnose=is_debug,
186
+ )
187
+
188
+ logger.add(
189
+ sink=str(log_dir / "errors.json"),
190
+ level="ERROR",
191
+ serialize=True,
192
+ rotation=rotation,
193
+ compression="zip",
194
+ enqueue=True,
195
+ )
196
+
197
+ logging.basicConfig(handlers=[InterceptHandler()], level=0, force=True)
198
+
199
+ logger.info("LoguruSetupComplete log_dir={}", log_dir)
200
+
201
+
202
+ # ---------------------------------------------------------------------------
203
+ # LoggingSettingsProtocol
204
+ # ---------------------------------------------------------------------------
205
+
206
+
207
+ @runtime_checkable
208
+ class LoggingSettingsProtocol(Protocol):
209
+ """Structural protocol for the logging-related subset of settings."""
210
+
211
+ log_level_console: str
212
+ log_level_file: str
213
+ log_rotation: str
214
+ log_dir: str
215
+ debug: bool
216
+
217
+
218
+ # ---------------------------------------------------------------------------
219
+ # setup_logging (settings-based variant)
220
+ # ---------------------------------------------------------------------------
221
+
222
+
223
+ def setup_logging(
224
+ settings: LoggingSettingsProtocol,
225
+ service_name: str,
226
+ intercept_loggers: list[str] | None = None,
227
+ log_levels: dict[str, int] | None = None,
228
+ ) -> None:
229
+ """Configure Loguru from a settings object.
230
+
231
+ **Dev** (``settings.debug is True``): colourised console + file sinks.
232
+ **Prod** (``settings.debug is False``): JSON-serialised stdout only.
233
+
234
+ Both modes apply a patcher (service name + contextvars) and a
235
+ healthcheck filter.
236
+
237
+ Args:
238
+ settings: Object satisfying :class:`LoggingSettingsProtocol`.
239
+ service_name: Identifies the service in log output.
240
+ intercept_loggers: Logger names whose handlers are replaced
241
+ with :class:`InterceptHandler`.
242
+ log_levels: ``{logger_name: level_int}`` for silencing noisy
243
+ libraries.
244
+
245
+ Raises:
246
+ ImportError: If ``loguru`` is not installed.
247
+ """
248
+ if logger is None:
249
+ raise ImportError("loguru is not installed. Please install it manually: pip install loguru")
250
+
251
+ logger.remove()
252
+
253
+ logger.configure(patcher=_make_context_patcher(service_name))
254
+
255
+ if settings.debug:
256
+ # ── Dev: colourised console + file sinks ──
257
+ log_dir = Path(settings.log_dir) / service_name
258
+ log_dir.mkdir(parents=True, exist_ok=True)
259
+
260
+ logger.add(
261
+ sink=sys.stdout,
262
+ level=settings.log_level_console,
263
+ colorize=True,
264
+ filter=_healthcheck_filter,
265
+ format=_CONSOLE_FORMAT,
266
+ )
267
+
268
+ logger.add(
269
+ sink=str(log_dir / "debug.log"),
270
+ level=settings.log_level_file,
271
+ rotation=settings.log_rotation,
272
+ compression="zip",
273
+ format=_FILE_FORMAT,
274
+ enqueue=True,
275
+ backtrace=True,
276
+ diagnose=True,
277
+ )
278
+
279
+ logger.add(
280
+ sink=str(log_dir / "errors.json"),
281
+ level="ERROR",
282
+ serialize=True,
283
+ rotation=settings.log_rotation,
284
+ compression="zip",
285
+ enqueue=True,
286
+ )
287
+ else:
288
+ # ── Prod: JSON stdout only (Alloy / Loki / ELK ingestion) ──
289
+ logger.add(
290
+ sink=sys.stdout,
291
+ level=settings.log_level_console,
292
+ serialize=True,
293
+ colorize=False,
294
+ filter=_healthcheck_filter,
295
+ enqueue=True,
296
+ )
297
+
298
+ # Intercept standard logging
299
+ logging.basicConfig(handlers=[InterceptHandler()], level=0, force=True)
300
+
301
+ if intercept_loggers:
302
+ for name in intercept_loggers:
303
+ logging.getLogger(name).handlers = [InterceptHandler()]
304
+
305
+ if log_levels:
306
+ for name, level in log_levels.items():
307
+ logging.getLogger(name).setLevel(level)