codex-platform 0.2.0__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 (154) hide show
  1. {codex_platform-0.2.0 → codex_platform-0.4.0}/.github/workflows/ci.yml +3 -3
  2. {codex_platform-0.2.0 → codex_platform-0.4.0}/CHANGELOG.md +20 -0
  3. {codex_platform-0.2.0 → codex_platform-0.4.0}/PKG-INFO +10 -10
  4. {codex_platform-0.2.0 → codex_platform-0.4.0}/README.md +8 -8
  5. {codex_platform-0.2.0 → codex_platform-0.4.0}/docs/index.md +8 -8
  6. {codex_platform-0.2.0 → codex_platform-0.4.0}/pyproject.toml +12 -1
  7. codex_platform-0.4.0/redis-backends-recovery.md +376 -0
  8. {codex_platform-0.2.0 → codex_platform-0.4.0}/src/codex_platform/notifications/registry.py +1 -1
  9. {codex_platform-0.2.0 → codex_platform-0.4.0}/src/codex_platform/redis_service/base.py +31 -0
  10. codex_platform-0.4.0/src/codex_platform/redis_service/exceptions.py +20 -0
  11. {codex_platform-0.2.0 → codex_platform-0.4.0}/src/codex_platform/redis_service/operations/__init__.py +4 -0
  12. {codex_platform-0.2.0 → codex_platform-0.4.0}/src/codex_platform/redis_service/operations/hash.py +12 -1
  13. codex_platform-0.4.0/src/codex_platform/redis_service/operations/sync_hash.py +263 -0
  14. codex_platform-0.4.0/src/codex_platform/redis_service/operations/sync_string.py +187 -0
  15. {codex_platform-0.2.0 → codex_platform-0.4.0}/src/codex_platform/streams/__init__.py +2 -2
  16. {codex_platform-0.2.0 → codex_platform-0.4.0}/src/codex_platform/workers/arq/config.py +1 -1
  17. {codex_platform-0.2.0 → codex_platform-0.4.0}/tests/integration/test_docs_build.py +2 -1
  18. codex_platform-0.4.0/tests/unit/test_sync_operations.py +112 -0
  19. codex_platform-0.4.0/tools/dev/__init__.py +0 -0
  20. codex_platform-0.4.0/tools/dev/check.py +16 -0
  21. {codex_platform-0.2.0 → codex_platform-0.4.0}/uv.lock +4 -4
  22. codex_platform-0.2.0/src/codex_platform/redis_service/exceptions.py +0 -20
  23. codex_platform-0.2.0/tools/dev/check.py +0 -35
  24. {codex_platform-0.2.0 → codex_platform-0.4.0}/.github/workflows/docs.yml +0 -0
  25. {codex_platform-0.2.0 → codex_platform-0.4.0}/.github/workflows/publish.yml +0 -0
  26. {codex_platform-0.2.0 → codex_platform-0.4.0}/.gitignore +0 -0
  27. {codex_platform-0.2.0 → codex_platform-0.4.0}/.pre-commit-config.yaml +0 -0
  28. {codex_platform-0.2.0 → codex_platform-0.4.0}/.python-version +0 -0
  29. {codex_platform-0.2.0 → codex_platform-0.4.0}/docs/changelog.md +0 -0
  30. {codex_platform-0.2.0 → codex_platform-0.4.0}/docs/en/api/index.md +0 -0
  31. {codex_platform-0.2.0 → codex_platform-0.4.0}/docs/en/api/notifications/channels.md +0 -0
  32. {codex_platform-0.2.0 → codex_platform-0.4.0}/docs/en/api/notifications/delivery/arq.md +0 -0
  33. {codex_platform-0.2.0 → codex_platform-0.4.0}/docs/en/api/notifications/delivery/base.md +0 -0
  34. {codex_platform-0.2.0 → codex_platform-0.4.0}/docs/en/api/notifications/delivery/direct.md +0 -0
  35. {codex_platform-0.2.0 → codex_platform-0.4.0}/docs/en/api/notifications/dto.md +0 -0
  36. {codex_platform-0.2.0 → codex_platform-0.4.0}/docs/en/api/notifications/index.md +0 -0
  37. {codex_platform-0.2.0 → codex_platform-0.4.0}/docs/en/api/notifications/orchestrator.md +0 -0
  38. {codex_platform-0.2.0 → codex_platform-0.4.0}/docs/en/api/notifications/registry.md +0 -0
  39. {codex_platform-0.2.0 → codex_platform-0.4.0}/docs/en/api/notifications/renderer.md +0 -0
  40. {codex_platform-0.2.0 → codex_platform-0.4.0}/docs/en/api/redis_service/base.md +0 -0
  41. {codex_platform-0.2.0 → codex_platform-0.4.0}/docs/en/api/redis_service/index.md +0 -0
  42. {codex_platform-0.2.0 → codex_platform-0.4.0}/docs/en/api/redis_service/keys.md +0 -0
  43. {codex_platform-0.2.0 → codex_platform-0.4.0}/docs/en/api/redis_service/operations/hash.md +0 -0
  44. {codex_platform-0.2.0 → codex_platform-0.4.0}/docs/en/api/redis_service/operations/json_module.md +0 -0
  45. {codex_platform-0.2.0 → codex_platform-0.4.0}/docs/en/api/redis_service/operations/json_string.md +0 -0
  46. {codex_platform-0.2.0 → codex_platform-0.4.0}/docs/en/api/redis_service/operations/list_.md +0 -0
  47. {codex_platform-0.2.0 → codex_platform-0.4.0}/docs/en/api/redis_service/operations/pipeline.md +0 -0
  48. {codex_platform-0.2.0 → codex_platform-0.4.0}/docs/en/api/redis_service/operations/set_.md +0 -0
  49. {codex_platform-0.2.0 → codex_platform-0.4.0}/docs/en/api/redis_service/operations/string.md +0 -0
  50. {codex_platform-0.2.0 → codex_platform-0.4.0}/docs/en/api/redis_service/operations/zset.md +0 -0
  51. {codex_platform-0.2.0 → codex_platform-0.4.0}/docs/en/api/redis_service/service.md +0 -0
  52. {codex_platform-0.2.0 → codex_platform-0.4.0}/docs/en/api/streams/consumer.md +0 -0
  53. {codex_platform-0.2.0 → codex_platform-0.4.0}/docs/en/api/streams/dispatcher.md +0 -0
  54. {codex_platform-0.2.0 → codex_platform-0.4.0}/docs/en/api/streams/index.md +0 -0
  55. {codex_platform-0.2.0 → codex_platform-0.4.0}/docs/en/api/streams/processor.md +0 -0
  56. {codex_platform-0.2.0 → codex_platform-0.4.0}/docs/en/api/streams/producer.md +0 -0
  57. {codex_platform-0.2.0 → codex_platform-0.4.0}/docs/en/api/streams/router.md +0 -0
  58. {codex_platform-0.2.0 → codex_platform-0.4.0}/docs/en/api/workers/arq/base.md +0 -0
  59. {codex_platform-0.2.0 → codex_platform-0.4.0}/docs/en/api/workers/arq/config.md +0 -0
  60. {codex_platform-0.2.0 → codex_platform-0.4.0}/docs/en/api/workers/arq/index.md +0 -0
  61. {codex_platform-0.2.0 → codex_platform-0.4.0}/docs/en/api/workers/arq/task_utils.md +0 -0
  62. {codex_platform-0.2.0 → codex_platform-0.4.0}/docs/en/api/workers/arq/types.md +0 -0
  63. {codex_platform-0.2.0 → codex_platform-0.4.0}/docs/en/architecture/notifications/README.md +0 -0
  64. {codex_platform-0.2.0 → codex_platform-0.4.0}/docs/en/architecture/notifications/data_flow.md +0 -0
  65. {codex_platform-0.2.0 → codex_platform-0.4.0}/docs/en/architecture/redis_service/README.md +0 -0
  66. {codex_platform-0.2.0 → codex_platform-0.4.0}/docs/en/architecture/redis_service/data_flow.md +0 -0
  67. {codex_platform-0.2.0 → codex_platform-0.4.0}/docs/en/architecture/streams/README.md +0 -0
  68. {codex_platform-0.2.0 → codex_platform-0.4.0}/docs/en/architecture/streams/data_flow.md +0 -0
  69. {codex_platform-0.2.0 → codex_platform-0.4.0}/docs/en/architecture/workers/README.md +0 -0
  70. {codex_platform-0.2.0 → codex_platform-0.4.0}/docs/en/architecture/workers/data_flow.md +0 -0
  71. {codex_platform-0.2.0 → codex_platform-0.4.0}/docs/ru/architecture/notifications/README.md +0 -0
  72. {codex_platform-0.2.0 → codex_platform-0.4.0}/docs/ru/architecture/notifications/data_flow.md +0 -0
  73. {codex_platform-0.2.0 → codex_platform-0.4.0}/docs/ru/architecture/redis_service/README.md +0 -0
  74. {codex_platform-0.2.0 → codex_platform-0.4.0}/docs/ru/architecture/redis_service/data_flow.md +0 -0
  75. {codex_platform-0.2.0 → codex_platform-0.4.0}/docs/ru/architecture/streams/README.md +0 -0
  76. {codex_platform-0.2.0 → codex_platform-0.4.0}/docs/ru/architecture/streams/data_flow.md +0 -0
  77. {codex_platform-0.2.0 → codex_platform-0.4.0}/docs/ru/architecture/workers/README.md +0 -0
  78. {codex_platform-0.2.0 → codex_platform-0.4.0}/docs/ru/architecture/workers/data_flow.md +0 -0
  79. {codex_platform-0.2.0 → codex_platform-0.4.0}/docs/stylesheets/extra.css +0 -0
  80. {codex_platform-0.2.0 → codex_platform-0.4.0}/mkdocs.yml +0 -0
  81. {codex_platform-0.2.0 → codex_platform-0.4.0}/project_structure.txt +0 -0
  82. {codex_platform-0.2.0 → codex_platform-0.4.0}/src/codex_platform/__init__.py +0 -0
  83. {codex_platform-0.2.0 → codex_platform-0.4.0}/src/codex_platform/notifications/__init__.py +0 -0
  84. {codex_platform-0.2.0 → codex_platform-0.4.0}/src/codex_platform/notifications/channels.py +0 -0
  85. {codex_platform-0.2.0 → codex_platform-0.4.0}/src/codex_platform/notifications/clients/__init__.py +0 -0
  86. {codex_platform-0.2.0 → codex_platform-0.4.0}/src/codex_platform/notifications/clients/smtp.py +0 -0
  87. {codex_platform-0.2.0 → codex_platform-0.4.0}/src/codex_platform/notifications/delivery/__init__.py +0 -0
  88. {codex_platform-0.2.0 → codex_platform-0.4.0}/src/codex_platform/notifications/delivery/arq.py +0 -0
  89. {codex_platform-0.2.0 → codex_platform-0.4.0}/src/codex_platform/notifications/delivery/base.py +0 -0
  90. {codex_platform-0.2.0 → codex_platform-0.4.0}/src/codex_platform/notifications/delivery/direct.py +0 -0
  91. {codex_platform-0.2.0 → codex_platform-0.4.0}/src/codex_platform/notifications/dto.py +0 -0
  92. {codex_platform-0.2.0 → codex_platform-0.4.0}/src/codex_platform/notifications/interfaces.py +0 -0
  93. {codex_platform-0.2.0 → codex_platform-0.4.0}/src/codex_platform/notifications/orchestrator.py +0 -0
  94. {codex_platform-0.2.0 → codex_platform-0.4.0}/src/codex_platform/notifications/renderer.py +0 -0
  95. /codex_platform-0.2.0/tests/integration/__init__.py → /codex_platform-0.4.0/src/codex_platform/py.typed +0 -0
  96. {codex_platform-0.2.0 → codex_platform-0.4.0}/src/codex_platform/redis_service/__init__.py +0 -0
  97. {codex_platform-0.2.0 → codex_platform-0.4.0}/src/codex_platform/redis_service/keys.py +0 -0
  98. {codex_platform-0.2.0 → codex_platform-0.4.0}/src/codex_platform/redis_service/managers/__init__.py +0 -0
  99. {codex_platform-0.2.0 → codex_platform-0.4.0}/src/codex_platform/redis_service/managers/base_manager.py +0 -0
  100. {codex_platform-0.2.0 → codex_platform-0.4.0}/src/codex_platform/redis_service/managers/site_settings.py +0 -0
  101. {codex_platform-0.2.0 → codex_platform-0.4.0}/src/codex_platform/redis_service/operations/json_module.py +0 -0
  102. {codex_platform-0.2.0 → codex_platform-0.4.0}/src/codex_platform/redis_service/operations/json_string.py +0 -0
  103. {codex_platform-0.2.0 → codex_platform-0.4.0}/src/codex_platform/redis_service/operations/list_.py +0 -0
  104. {codex_platform-0.2.0 → codex_platform-0.4.0}/src/codex_platform/redis_service/operations/pipeline.py +0 -0
  105. {codex_platform-0.2.0 → codex_platform-0.4.0}/src/codex_platform/redis_service/operations/set_.py +0 -0
  106. {codex_platform-0.2.0 → codex_platform-0.4.0}/src/codex_platform/redis_service/operations/string.py +0 -0
  107. {codex_platform-0.2.0 → codex_platform-0.4.0}/src/codex_platform/redis_service/operations/zset.py +0 -0
  108. {codex_platform-0.2.0 → codex_platform-0.4.0}/src/codex_platform/redis_service/service.py +0 -0
  109. {codex_platform-0.2.0 → codex_platform-0.4.0}/src/codex_platform/streams/consumer.py +0 -0
  110. {codex_platform-0.2.0 → codex_platform-0.4.0}/src/codex_platform/streams/dispatcher.py +0 -0
  111. {codex_platform-0.2.0 → codex_platform-0.4.0}/src/codex_platform/streams/processor.py +0 -0
  112. {codex_platform-0.2.0 → codex_platform-0.4.0}/src/codex_platform/streams/producer.py +0 -0
  113. {codex_platform-0.2.0 → codex_platform-0.4.0}/src/codex_platform/streams/router.py +0 -0
  114. {codex_platform-0.2.0 → codex_platform-0.4.0}/src/codex_platform/workers/__init__.py +0 -0
  115. {codex_platform-0.2.0 → codex_platform-0.4.0}/src/codex_platform/workers/arq/__init__.py +0 -0
  116. {codex_platform-0.2.0 → codex_platform-0.4.0}/src/codex_platform/workers/arq/base.py +0 -0
  117. {codex_platform-0.2.0 → codex_platform-0.4.0}/src/codex_platform/workers/arq/task_utils.py +0 -0
  118. {codex_platform-0.2.0 → codex_platform-0.4.0}/src/codex_platform/workers/arq/types.py +0 -0
  119. {codex_platform-0.2.0/tests/unit → codex_platform-0.4.0/tests/integration}/__init__.py +0 -0
  120. {codex_platform-0.2.0 → codex_platform-0.4.0}/tests/integration/conftest.py +0 -0
  121. {codex_platform-0.2.0 → codex_platform-0.4.0}/tests/integration/test_redis_service.py +0 -0
  122. {codex_platform-0.2.0 → codex_platform-0.4.0}/tests/integration/test_streams.py +0 -0
  123. {codex_platform-0.2.0/tools → codex_platform-0.4.0/tests/unit}/__init__.py +0 -0
  124. {codex_platform-0.2.0 → codex_platform-0.4.0}/tests/unit/conftest.py +0 -0
  125. {codex_platform-0.2.0 → codex_platform-0.4.0}/tests/unit/test_arq_base.py +0 -0
  126. {codex_platform-0.2.0 → codex_platform-0.4.0}/tests/unit/test_arq_config.py +0 -0
  127. {codex_platform-0.2.0 → codex_platform-0.4.0}/tests/unit/test_arq_public_api.py +0 -0
  128. {codex_platform-0.2.0 → codex_platform-0.4.0}/tests/unit/test_arq_task_utils.py +0 -0
  129. {codex_platform-0.2.0 → codex_platform-0.4.0}/tests/unit/test_notification_adapters.py +0 -0
  130. {codex_platform-0.2.0 → codex_platform-0.4.0}/tests/unit/test_notification_channels.py +0 -0
  131. {codex_platform-0.2.0 → codex_platform-0.4.0}/tests/unit/test_notification_dto.py +0 -0
  132. {codex_platform-0.2.0 → codex_platform-0.4.0}/tests/unit/test_notification_orchestrator.py +0 -0
  133. {codex_platform-0.2.0 → codex_platform-0.4.0}/tests/unit/test_notification_registry.py +0 -0
  134. {codex_platform-0.2.0 → codex_platform-0.4.0}/tests/unit/test_notification_renderer.py +0 -0
  135. {codex_platform-0.2.0 → codex_platform-0.4.0}/tests/unit/test_notification_smtp.py +0 -0
  136. {codex_platform-0.2.0 → codex_platform-0.4.0}/tests/unit/test_redis_base.py +0 -0
  137. {codex_platform-0.2.0 → codex_platform-0.4.0}/tests/unit/test_redis_hash.py +0 -0
  138. {codex_platform-0.2.0 → codex_platform-0.4.0}/tests/unit/test_redis_keys.py +0 -0
  139. {codex_platform-0.2.0 → codex_platform-0.4.0}/tests/unit/test_redis_list.py +0 -0
  140. {codex_platform-0.2.0 → codex_platform-0.4.0}/tests/unit/test_redis_manager.py +0 -0
  141. {codex_platform-0.2.0 → codex_platform-0.4.0}/tests/unit/test_redis_pipeline.py +0 -0
  142. {codex_platform-0.2.0 → codex_platform-0.4.0}/tests/unit/test_redis_service.py +0 -0
  143. {codex_platform-0.2.0 → codex_platform-0.4.0}/tests/unit/test_redis_set.py +0 -0
  144. {codex_platform-0.2.0 → codex_platform-0.4.0}/tests/unit/test_redis_string.py +0 -0
  145. {codex_platform-0.2.0 → codex_platform-0.4.0}/tests/unit/test_redis_zset.py +0 -0
  146. {codex_platform-0.2.0 → codex_platform-0.4.0}/tests/unit/test_site_settings_manager.py +0 -0
  147. {codex_platform-0.2.0 → codex_platform-0.4.0}/tests/unit/test_stream_consumer.py +0 -0
  148. {codex_platform-0.2.0 → codex_platform-0.4.0}/tests/unit/test_stream_dispatcher.py +0 -0
  149. {codex_platform-0.2.0 → codex_platform-0.4.0}/tests/unit/test_stream_processor.py +0 -0
  150. {codex_platform-0.2.0 → codex_platform-0.4.0}/tests/unit/test_stream_producer.py +0 -0
  151. {codex_platform-0.2.0 → codex_platform-0.4.0}/tests/unit/test_stream_router.py +0 -0
  152. {codex_platform-0.2.0/tools/dev → codex_platform-0.4.0/tools}/__init__.py +0 -0
  153. {codex_platform-0.2.0 → codex_platform-0.4.0}/tools/dev/README.md +0 -0
  154. {codex_platform-0.2.0 → codex_platform-0.4.0}/tools/dev/generate_project_tree.py +0 -0
@@ -30,7 +30,7 @@ jobs:
30
30
  run: uv run pre-commit run --all-files
31
31
 
32
32
  - name: Run pip-audit
33
- run: uv run pip-audit
33
+ run: uv run pip-audit --skip-editable --ignore-vuln CVE-2026-4539
34
34
 
35
35
  - name: Run mypy
36
36
  run: uv run mypy src/
@@ -51,8 +51,8 @@ jobs:
51
51
  with:
52
52
  version: "0.10.7"
53
53
 
54
- - name: Sync dev environment
55
- run: uv sync --locked --extra dev
54
+ - name: Sync dev + docs environment
55
+ run: uv sync --locked --extra dev --extra docs
56
56
 
57
57
  - name: Run unit tests
58
58
  run: uv run pytest tests/ -m "unit" -v --tb=short
@@ -3,6 +3,26 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  Grouped by `Added` · `Changed` · `Deprecated` · `Removed` · `Fixed`.
5
5
 
6
+ ## [Unreleased]
7
+
8
+ ## [0.4.0] - 2026-04-22
9
+
10
+ ### Added
11
+ - Synchronous Redis operations: `SyncStringOperations` and `SyncHashOperations` using `redis.Redis`.
12
+ - Added `catch_redis_errors_sync` decorator for synchronous methods.
13
+ - Added `encoder` parameter to `HashOperations.set_fields` and `SyncHashOperations.set_fields`.
14
+ ### Added
15
+ - Added `py.typed` marker for PEP 561 compliance — downstream consumers now benefit from full type inference when using mypy or pyright.
16
+
17
+ ### Changed
18
+ - Translated all Russian comments and docstrings to English across `redis_service/exceptions.py`, `workers/arq/config.py`, `notifications/registry.py`, and `streams/__init__.py`.
19
+
20
+ ## [0.3.0] - 2026-04-05
21
+
22
+ ### Changed
23
+ - Raised the `codex-core` requirement to `>=0.3.0,<0.10.0` so the package tracks the new core baseline while remaining compatible through the `0.9.x` series.
24
+ - Updated the documented install examples to the `codex-platform 0.3.x` release line.
25
+
6
26
  ## [0.2.0] - 2026-03-29
7
27
 
8
28
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codex-platform
3
- Version: 0.2.0
3
+ Version: 0.4.0
4
4
  Summary: Modular async infrastructure (Redis, Streams, ARQ, Notifications) built for Codex, designed for independent use.
5
5
  Project-URL: Homepage, https://github.com/codexdlc/codex-platform
6
6
  Project-URL: Documentation, https://codexdlc.github.io/codex-platform/
@@ -15,7 +15,7 @@ Classifier: License :: OSI Approved :: Apache Software License
15
15
  Classifier: Programming Language :: Python :: 3.12
16
16
  Classifier: Programming Language :: Python :: 3.13
17
17
  Requires-Python: >=3.12
18
- Requires-Dist: codex-core<0.3.0,>=0.2.0
18
+ Requires-Dist: codex-core<0.10.0,>=0.3.0
19
19
  Requires-Dist: pydantic<3.0,>=2.0
20
20
  Provides-Extra: all
21
21
  Requires-Dist: aiosmtplib>=3.0; extra == 'all'
@@ -68,27 +68,27 @@ Description-Content-Type: text/markdown
68
68
  ## Install
69
69
 
70
70
  ```bash
71
- # codex-platform 0.2.x
72
- pip install "codex-platform>=0.2.0,<0.3.0"
71
+ # codex-platform 0.3.x
72
+ pip install "codex-platform>=0.3.0,<0.4.0"
73
73
 
74
74
  # With Redis support
75
- pip install "codex-platform[redis]>=0.2.0,<0.3.0"
75
+ pip install "codex-platform[redis]>=0.3.0,<0.4.0"
76
76
 
77
77
  # With ARQ background workers
78
- pip install "codex-platform[arq]>=0.2.0,<0.3.0"
78
+ pip install "codex-platform[arq]>=0.3.0,<0.4.0"
79
79
 
80
80
  # With async SMTP notifications
81
- pip install "codex-platform[notifications]>=0.2.0,<0.3.0"
81
+ pip install "codex-platform[notifications]>=0.3.0,<0.4.0"
82
82
 
83
83
  # Redis Streams
84
- pip install "codex-platform[streams]>=0.2.0,<0.3.0"
84
+ pip install "codex-platform[streams]>=0.3.0,<0.4.0"
85
85
 
86
86
  # Everything
87
- pip install "codex-platform[all]>=0.2.0,<0.3.0"
87
+ pip install "codex-platform[all]>=0.3.0,<0.4.0"
88
88
  ```
89
89
 
90
90
  Requires Python 3.12 or newer.
91
- Installs `codex-core>=0.2.0,<0.3.0` automatically as a dependency.
91
+ Installs `codex-core>=0.3.0,<0.10.0` automatically as a dependency.
92
92
 
93
93
  ## Development
94
94
 
@@ -12,27 +12,27 @@
12
12
  ## Install
13
13
 
14
14
  ```bash
15
- # codex-platform 0.2.x
16
- pip install "codex-platform>=0.2.0,<0.3.0"
15
+ # codex-platform 0.3.x
16
+ pip install "codex-platform>=0.3.0,<0.4.0"
17
17
 
18
18
  # With Redis support
19
- pip install "codex-platform[redis]>=0.2.0,<0.3.0"
19
+ pip install "codex-platform[redis]>=0.3.0,<0.4.0"
20
20
 
21
21
  # With ARQ background workers
22
- pip install "codex-platform[arq]>=0.2.0,<0.3.0"
22
+ pip install "codex-platform[arq]>=0.3.0,<0.4.0"
23
23
 
24
24
  # With async SMTP notifications
25
- pip install "codex-platform[notifications]>=0.2.0,<0.3.0"
25
+ pip install "codex-platform[notifications]>=0.3.0,<0.4.0"
26
26
 
27
27
  # Redis Streams
28
- pip install "codex-platform[streams]>=0.2.0,<0.3.0"
28
+ pip install "codex-platform[streams]>=0.3.0,<0.4.0"
29
29
 
30
30
  # Everything
31
- pip install "codex-platform[all]>=0.2.0,<0.3.0"
31
+ pip install "codex-platform[all]>=0.3.0,<0.4.0"
32
32
  ```
33
33
 
34
34
  Requires Python 3.12 or newer.
35
- Installs `codex-core>=0.2.0,<0.3.0` automatically as a dependency.
35
+ Installs `codex-core>=0.3.0,<0.10.0` automatically as a dependency.
36
36
 
37
37
  ## Development
38
38
 
@@ -9,27 +9,27 @@ Built to be dropped into any Python 3.12+ service as a set of composable, indepe
9
9
  ## Install
10
10
 
11
11
  ```bash
12
- # codex-platform 0.2.x
13
- pip install "codex-platform>=0.2.0,<0.3.0"
12
+ # codex-platform 0.3.x
13
+ pip install "codex-platform>=0.3.0,<0.4.0"
14
14
 
15
15
  # With Redis support
16
- pip install "codex-platform[redis]>=0.2.0,<0.3.0"
16
+ pip install "codex-platform[redis]>=0.3.0,<0.4.0"
17
17
 
18
18
  # With ARQ worker support
19
- pip install "codex-platform[arq]>=0.2.0,<0.3.0"
19
+ pip install "codex-platform[arq]>=0.3.0,<0.4.0"
20
20
 
21
21
  # With async SMTP notifications
22
- pip install "codex-platform[notifications]>=0.2.0,<0.3.0"
22
+ pip install "codex-platform[notifications]>=0.3.0,<0.4.0"
23
23
 
24
24
  # Redis Streams
25
- pip install "codex-platform[streams]>=0.2.0,<0.3.0"
25
+ pip install "codex-platform[streams]>=0.3.0,<0.4.0"
26
26
 
27
27
  # Everything
28
- pip install "codex-platform[all]>=0.2.0,<0.3.0"
28
+ pip install "codex-platform[all]>=0.3.0,<0.4.0"
29
29
  ```
30
30
 
31
31
  Requires Python 3.12 or newer.
32
- Installs `codex-core>=0.2.0,<0.3.0` automatically as a dependency.
32
+ Installs `codex-core>=0.3.0,<0.10.0` automatically as a dependency.
33
33
 
34
34
  ## Quick Start
35
35
 
@@ -20,7 +20,7 @@ classifiers = [
20
20
  ]
21
21
  dependencies = [
22
22
  "pydantic>=2.0,<3.0",
23
- "codex-core>=0.2.0,<0.3.0",
23
+ "codex-core>=0.3.0,<0.10.0",
24
24
  ]
25
25
 
26
26
  [project.urls]
@@ -132,3 +132,14 @@ exclude_lines = [
132
132
  [tool.bandit]
133
133
  exclude_dirs = ["tests"]
134
134
  skips = ["B602", "B605", "B607", "B404"]
135
+
136
+ [tool.codex-check]
137
+ project-name = "codex-platform"
138
+ audit-flags = "--skip-editable --ignore-vuln CVE-2026-4539"
139
+ integration-requires = "Redis"
140
+ run-extra-checks = false
141
+ test-stages = ["unit", "integration"]
142
+ prompt-test-stages = ["integration"]
143
+ no-cov-test-stages = ["integration"]
144
+ test-paths = ["tests"]
145
+ types-paths = ["src"]
@@ -0,0 +1,376 @@
1
+ # Plan: Fix codex_django Redis backends — client factory + sync/async split + feature flag rollout
2
+
3
+ > [!NOTE]
4
+ > **Project Focus:** This repository (**codex-platform**) is responsible for **Track A**.
5
+ > The full plan is included below to maintain context and avoid narrowing down data.
6
+
7
+ ## Context
8
+
9
+ Production `lily_website` ловил пять Redis-багов:
10
+
11
+ 1. **`AttributeError: 'DashboardRedisManager' object has no attribute '_client'`** на `GET /ru/cabinet/` — главный блокер.
12
+ 2. `RuntimeError: Event loop is closed` — admin login → `SessionStore.asave`.
13
+ 3. `RuntimeError: got Future attached to a different loop` — повторные `FixtureHashManager.get_hash/set_hash`.
14
+ 4. `redis.exceptions.DataError: Invalid input of type 'bool'` — `update_site_settings` / `update_all_content`.
15
+ 5. `Error fetching static page SEO for key team/contacts: Event loop is closed` — `codex_django.core.seo.selectors.get_static_page_seo`.
16
+
17
+ Плюс infra-инцидент: `The "ATTV6fkq" variable is not set` — `$` в Redis-пароле, docker-compose интерполирует. Надо проверить.
18
+
19
+ Prod hotfix'ом (`951a6c4`) переведён на `django_redis` + `sessions.backends.db`, entrypoint очищен. Цель — починить codex_django Redis engine и вернуть его через **feature flag** с возможностью отката.
20
+
21
+ ## Root causes
22
+
23
+ 1. **Несогласованный контракт менеджеров.** `BaseRedisManager` (codex-platform) и `BaseDjangoRedisManager` не присваивают `self._client`. `DashboardRedisManager` и SEO selector используют `self._client.*`, которого нет.
24
+ 2. **Живой async client переживает свой event loop.** `Redis.from_url(...)` создаётся один раз в `__init__`, `async_to_sync` создаёт новый loop на каждый sync-вызов — клиент держит Future'ы мёртвого loop.
25
+ 3. **Module-level instantiation** (`_manager = DashboardRedisManager()` в `cabinet/selector/dashboard.py:41`, аналогично в SEO).
26
+ 4. **CacheCoder не подключён в HSET-пути.** `DjangoSiteSettingsManager.asave_instance` (`core/redis/managers/settings.py:95-106`) пишет сырой `instance.to_dict()`. CacheCoder покрывает datetime/Decimal/UUID/bytes/set, но не bool/None/Enum/Path/Promise. В `lily_website/src/lily_backend/system/models/settings.py:89-98` дублирующий костыль.
27
+ 5. **Sync Django path завязан на async_to_sync** для cache/session/dashboard/SEO. Нужен отдельный sync-путь поверх `redis.Redis`.
28
+
29
+ ## Глобальные принципы (жёсткие)
30
+
31
+ Библиотеки в alpha, consumer один — делаем чистый breaking change, compat-слой не тащим.
32
+
33
+ - **Единственный публичный API менеджеров — 4 context manager'а**: `sync_string()`, `sync_hash()`, `async_string()`, `async_hash()`. Всё старое (`self._client`, `self.string`, `self.hash` как атрибуты) — **удаляется**, не заменяется alias'ом.
34
+ - **Sync code → sync_* методы → `redis.Redis`**. `async_to_sync` из sync Django hot paths запрещён.
35
+ - **Async code → async_* методы → `redis.asyncio.Redis`**.
36
+ - **Менеджер принимает `client_factory: Callable[[], Redis]`, а НЕ live client**. Default factory читает `settings.REDIS_URL`. Live client между вызовами не хранится.
37
+ - **New client per operation с `aclose()` / `close()` в `finally`** (Variant A). Pool / loop-local cache — follow-up при превышении p95-бюджета.
38
+ - **Grep-gate** (обязательная часть CI / ручной проверки перед мержем):
39
+ ```
40
+ rg "self\._client|\.string\.|\.hash\." src/codex_django src/codex_platform src/lily_backend
41
+ ```
42
+ Все находки либо удалены, либо явно внутри operation-классов codex-platform. Новый код с `self._client` / атрибутным доступом к `.string`/`.hash` не мержится.
43
+ - **Rollout через feature flag** `USE_CODEX_REDIS_BACKENDS`; `django-redis` оставляем в deps как fallback.
44
+ - **CacheCoder без маркеров**: `None→""`, `bool→"1"/"0"`, `Enum→CacheCoder.dump(value.value)`. Typed restore делает `DjangoSiteSettingsManager` на основе Django field types.
45
+
46
+ ## Треки (независимо разрабатываемые)
47
+
48
+ Ветка в каждом репо: `codex/fix-redis-backends`. Треки разделены так, чтобы A/B/C/D можно было вести параллельно разными исполнителями; E/F/G — ниже по зависимостям.
49
+
50
+ ---
51
+
52
+ ### Трек A — codex-platform: sync operations + encoder hook (без alias)
53
+
54
+ **Репо:** `C:\install\projects\codex_tools\codex-platform`
55
+
56
+ `BaseRedisManager` остаётся как есть для async service layer; он **не используется** как база `BaseDjangoRedisManager` (см. трек B) — таким образом в codex-platform нет необходимости в compat-alias.
57
+
58
+ A.1. `src/codex_platform/redis_service/operations/hash.py` — добавить `encoder: Callable | None = None` в `set_fields`. Default `None`, не ломает существующих consumers.
59
+
60
+ A.2. Новый модуль `src/codex_platform/redis_service/operations/sync_string.py`, `operations/sync_hash.py`:
61
+ - `SyncStringOperations(redis.Redis)` — `get/set/delete/mget/mset/incr/expire/ttl/exists`.
62
+ - `SyncHashOperations(redis.Redis)` — `hget/hset/hdel/hgetall/set_fields`.
63
+ - Минимальный набор, используемый codex-django.
64
+
65
+ A.3. Тесты `tests/unit/test_sync_operations.py` (round-trip, encoder hook).
66
+
67
+ **Зависимости:** нет. Релизится первым.
68
+
69
+ **Deliverable:** codex-platform version bump + changelog.
70
+
71
+ ---
72
+
73
+ ### Трек B — codex-django: client factory + sync/async context managers в `BaseDjangoRedisManager`
74
+
75
+ **Репо:** `C:\install\projects\codex_tools\codex-django`
76
+
77
+ **Depends on:** A (для sync operations) — либо делать только после A.3.
78
+
79
+ B.1. `src/codex_django/core/redis/managers/base.py` — переписать `BaseDjangoRedisManager` с нуля. **Не наследуется от `BaseRedisManager`** (codex-platform). Единственный публичный API — 4 context manager'а:
80
+
81
+ ```python
82
+ class BaseDjangoRedisManager:
83
+ def __init__(
84
+ self,
85
+ prefix: str = "",
86
+ *,
87
+ async_client_factory: "Callable[[], AsyncRedis] | None" = None,
88
+ sync_client_factory: "Callable[[], SyncRedis] | None" = None,
89
+ ):
90
+ self.prefix = prefix
91
+ self.project_name = get_project_name()
92
+ self._redis_url = get_redis_url_from_settings()
93
+ self._async_factory = async_client_factory or self._default_async_factory
94
+ self._sync_factory = sync_client_factory or self._default_sync_factory
95
+
96
+ def _default_async_factory(self) -> "AsyncRedis":
97
+ from redis.asyncio import Redis
98
+ return Redis.from_url(self._redis_url, decode_responses=True)
99
+
100
+ def _default_sync_factory(self) -> "SyncRedis":
101
+ import redis
102
+ return redis.Redis.from_url(self._redis_url, decode_responses=True)
103
+
104
+ @asynccontextmanager
105
+ async def async_string(self) -> "AsyncIterator[StringOperations]":
106
+ client = self._async_factory()
107
+ try:
108
+ yield StringOperations(client)
109
+ finally:
110
+ await client.aclose()
111
+
112
+ @asynccontextmanager
113
+ async def async_hash(self) -> "AsyncIterator[HashOperations]":
114
+ client = self._async_factory()
115
+ try:
116
+ yield HashOperations(client)
117
+ finally:
118
+ await client.aclose()
119
+
120
+ @contextmanager
121
+ def sync_string(self) -> "Iterator[SyncStringOperations]":
122
+ client = self._sync_factory()
123
+ try:
124
+ yield SyncStringOperations(client)
125
+ finally:
126
+ client.close()
127
+
128
+ @contextmanager
129
+ def sync_hash(self) -> "Iterator[SyncHashOperations]":
130
+ client = self._sync_factory()
131
+ try:
132
+ yield SyncHashOperations(client)
133
+ finally:
134
+ client.close()
135
+
136
+ def make_key(self, key: str) -> str: ...
137
+ ```
138
+
139
+ Правила (жёсткие):
140
+ - `self._client`, `self.string`, `self.hash` как **атрибуты** — удалены. Если найдутся в коде после миграции — не мержим.
141
+ - Все sync use-sites: `with mgr.sync_string() as s: s.get(...)`.
142
+ - Все async use-sites: `async with mgr.async_string() as s: await s.get(...)`.
143
+ - `async_to_sync` из sync Django hot paths удаляется — sync backend использует `sync_string/sync_hash` напрямую.
144
+
145
+ B.2. `src/codex_django/cache/backends/redis.py`:
146
+ - sync методы (`get/set/delete/has_key/clear/...`) — через `sync_string()`/`sync_hash()`.
147
+ - async методы (`aget/aset/...`) — через `async_string()`/`async_hash()`.
148
+ - `async_to_sync` удалить из sync path полностью.
149
+
150
+ B.3. `src/codex_django/sessions/backends/redis.py`:
151
+ - `save/load/exists/create/delete` — через `sync_string()`.
152
+ - `asave/aload/aexists/acreate/adelete` — через `async_string()`.
153
+
154
+ B.4. `src/codex_django/system/redis/managers/fixtures.py` — мигрировать на `sync_string()/async_string()`.
155
+
156
+ B.5. Тесты:
157
+ - `tests/unit/core/redis/test_base_manager_client_factory.py` — default async/sync factories, injection, `aclose/close` вызывается.
158
+ - `tests/unit/sessions/test_redis_backend_sync.py` — sync path.
159
+ - `tests/unit/cache/test_redis_backend_sync.py` — sync path.
160
+
161
+ **Deliverable:** codex-django version bump (minor, т.к. ломающее изменение в signature BaseDjangoRedisManager).
162
+
163
+ ---
164
+
165
+ ### Трек C — codex-django: CacheCoder расширение + `DjangoSiteSettingsManager` typed restore
166
+
167
+ **Репо:** codex-django. **Depends on:** B (использует новые `hash()`/`ahash()`).
168
+
169
+ C.1. `src/codex_django/cache/values.py` — расширить CacheCoder:
170
+ - `dump_bool(v) → "1"|"0"`; `load_bool(raw) → True|False`.
171
+ - `dump_none() → ""`; `load_none` — только явный вызов из typed restore (не автоматический).
172
+ - `dump_enum(v) → CacheCoder.dump(v.value)` (рекурсивно, т.к. value может быть dict/bool/None).
173
+ - `dump_path(v) → str(v)`.
174
+ - `dump_lazy(v) → force_str(v)` (`django.utils.functional.Promise`).
175
+ - В generic `dump()` — `isinstance(value, bool)` ДО `int`.
176
+
177
+ C.2. Тесты `tests/unit/cache/test_values.py` — round-trip всех типов. Для Promise — `override_settings(USE_I18N=True)` + `gettext_lazy`.
178
+
179
+ C.3. `src/codex_django/core/redis/managers/settings.py`:
180
+
181
+ **Запись** (sync):
182
+ ```python
183
+ data = instance.to_dict()
184
+ coded = {k: CacheCoder.dump(v) for k, v in data.items()}
185
+ with self.sync_hash() as h:
186
+ h.set_fields(self.make_key(...), coded)
187
+ ```
188
+
189
+ **Чтение** (sync):
190
+ ```python
191
+ with self.sync_hash() as h:
192
+ raw = h.hgetall(self.make_key(...))
193
+ return self._decode_fields(raw, instance_cls=SiteSettings)
194
+ ```
195
+
196
+ Async-пары (`asave_instance`, `aload_cached`) — через `async_hash()`.
197
+
198
+ Метод `_decode_fields(raw: dict[str,str], instance_cls)`:
199
+ - Идёт по `instance_cls._meta.get_fields()`.
200
+ - Для `BooleanField`:
201
+ - `raw[name] == "1"` → `True`;
202
+ - `"0"` → `False`;
203
+ - `""` + `field.null=True` → `None`;
204
+ - иначе fallback на дефолт поля.
205
+ - Для `CharField/TextField` — `raw[name]` as-is (пустая строка **не** превращается в None).
206
+ - Для nullable non-string полей (`IntegerField(null=True)`, `DateTimeField(null=True)` и т.п.) — `"" → None`.
207
+ - Для `DateTimeField/DateField/TimeField/DecimalField/UUIDField` — `CacheCoder.load_*`.
208
+ - Для полей, которые модель хранит как JSON (ForeignKey id / JSONField) — `json.loads` либо соответствующий loader.
209
+
210
+ C.4. Тесты:
211
+ - `tests/unit/core/redis/test_settings_manager_encoding.py` — write bool/None/datetime/Decimal/UUID → read с typed restore, включая `BooleanField(null=True)` edge cases.
212
+
213
+ ---
214
+
215
+ ### Трек D — codex-django: `DashboardRedisManager`, SEO selectors, остальные менеджеры
216
+
217
+ **Репо:** codex-django. **Depends on:** B.
218
+
219
+ D.1. `src/codex_django/cabinet/redis/managers/dashboard.py`:
220
+ - Заменить все `self._client.*` на `with self.sync_string() as s: s.get/set/delete(...)` (sync) и `async with self.async_string() as s: await s.get/set/delete(...)` (async).
221
+ - TTL — через `s.set(key, value, ex=ttl)`.
222
+
223
+ D.2. `src/codex_django/cabinet/selector/dashboard.py:41` — убрать `_manager = DashboardRedisManager()`; заменить на:
224
+ ```python
225
+ @lru_cache(maxsize=1)
226
+ def get_dashboard_redis_manager() -> DashboardRedisManager:
227
+ return DashboardRedisManager()
228
+ ```
229
+ Все использования `_manager` переписать на `get_dashboard_redis_manager()`.
230
+
231
+ D.3. `src/codex_django/core/seo/selectors.py` — аналогично:
232
+ - убрать module-level manager;
233
+ - `_client` удалить; мигрировать sync на `sync_string()`, async на `async_string()`.
234
+
235
+ D.4. Grep-gate (обязательно перед мержем в каждом треке B/C/D/E):
236
+ ```
237
+ rg "self\._client|\.string\.|\.hash\." src/codex_django src/codex_platform src/lily_backend
238
+ ```
239
+ Разрешённые находки — только внутри operation-классов codex-platform (`HashOperations`, `StringOperations`, `SyncHashOperations`, `SyncStringOperations`), где `self._client` — это injected redis client этого operation-класса (не менеджер). Всё остальное — мигрировать.
240
+
241
+ D.5. Проверка оставшихся менеджеров: `NotificationsCacheManager`, `DjangoSiteSettingsManager` (уже в треке C), `FixtureHashManager` (в треке B), локальные `ActionTokenRedisManager` (в lily_website — трек E).
242
+
243
+ D.6. Тесты:
244
+ - `tests/unit/cabinet/redis/test_dashboard_manager.py` — sync get/set/delete, async aget/aset/adelete.
245
+ - `tests/unit/core/seo/test_seo_selector_redis.py` — повторные вызовы в одном процессе.
246
+
247
+ ---
248
+
249
+ ### Трек E — lily_website: тесты, локальный workaround, local managers
250
+
251
+ **Репо:** `C:\install\projects\clients\lily_website`. **Depends on:** B, C, D через editable install из `C:\install\projects\codex_tools\`.
252
+
253
+ E.1. Удалить локальный bool/None workaround в `src/lily_backend/system/models/settings.py:89-98` — пусть `to_dict()` возвращает нативные Python-значения; CacheCoder применяется централизованно.
254
+
255
+ E.2. Локальные менеджеры в `src/lily_backend/system/redis.py` (`ActionTokenRedisManager`) — мигрировать на новый контракт (`sync_string()/async_string()`).
256
+
257
+ E.3. Regression tests в `tests/lily_backend/regression/`:
258
+ - `test_dashboard_manager_cache.py` — `Client.force_login(staff)`, `GET /ru/cabinet/` × 2 (первый → cache write, второй → cache read) с `cache_ttl=300`. Оба 200.
259
+ - `test_session_event_loop.py` — admin login + 3 GET.
260
+ - `test_fixture_hash_repeated.py` — `get_hash/set_hash` × 2.
261
+ - `test_site_settings_bool_hset.py` — `DjangoSiteSettingsManager.save_instance` с bool/None/datetime + typed restore.
262
+ - `test_seo_static_page.py` — `get_static_page_seo("team")` × 2.
263
+
264
+ E.4. Integration tests in `tests/lily_backend/integration/` (требуют Docker redis):
265
+ - `test_cache_get_set_repeat.py` — sync `cache.set/get/delete` × 10.
266
+ - `test_management_commands.py` — двойной `update_site_settings --force`, `update_all_content`, `load_catalog`.
267
+ - `test_admin_login_flow.py` — login через `Client` + несколько админ-страниц.
268
+
269
+ E.5. Unit tests:
270
+ - `tests/lily_backend/unit/test_cache_coder_extensions.py` — sanity check, что проектные модели сериализуются.
271
+
272
+ ---
273
+
274
+ ### Трек F — lily_website: feature flag rollout (staging → prod)
275
+
276
+ **Зависит от:** зелёных A/B/C/D/E и Docker smoke.
277
+
278
+ F.1. `src/lily_backend/core/settings/prod.py` — feature flag:
279
+ ```python
280
+ USE_CODEX_REDIS_BACKENDS = env.bool("USE_CODEX_REDIS_BACKENDS", default=False)
281
+
282
+ if USE_CODEX_REDIS_BACKENDS:
283
+ CACHES = {"default": {"BACKEND": "codex_django.cache.backends.redis.RedisCache", ...}}
284
+ SESSION_ENGINE = "codex_django.sessions.backends.redis"
285
+ else:
286
+ CACHES = {"default": {"BACKEND": "django_redis.cache.RedisCache", ...}}
287
+ SESSION_ENGINE = "django.contrib.sessions.backends.db"
288
+ ```
289
+
290
+ F.2. **`django-redis` оставить в deps** в этом релизе. Удалять — только после успешной работы с flag=True в prod ≥ 2 недели.
291
+
292
+ F.3. `deploy/lily_backend/entrypoint.sh` — **не** возвращать startup-команды. `update_all_content` — one-shot:
293
+ ```
294
+ docker compose -f docker-compose.prod.yml run --rm -T backend python manage.py update_all_content
295
+ ```
296
+
297
+ F.4. Rollout:
298
+ - staging: `USE_CODEX_REDIS_BACKENDS=true` → наблюдение логов ≥ 1 рабочий день.
299
+ - prod: включить flag; при ошибках — откат переменной окружения без редеплоя кода.
300
+
301
+ F.5. Runbook в `docs/redis-feature-flag.md`: включение/выключение, что мониторить, куда смотреть логи.
302
+
303
+ ---
304
+
305
+ ### Трек G — infra: Redis password escaping
306
+
307
+ G.1. Проверить `deploy/docker-compose.prod.yml` + `.env`:
308
+ ```
309
+ docker compose --env-file .env -f docker-compose.prod.yml config
310
+ ```
311
+ Если видим `The "ATTV6fkq" variable is not set` — `$` в пароле интерполируется. Фикс:
312
+ - экранировать как `$$` в `.env` / `docker-compose.yml` ИЛИ
313
+ - завернуть пароль в одинарные кавычки ИЛИ
314
+ - перегенерировать пароль без `$`.
315
+
316
+ G.2. Добавить guard в entrypoint: проверка, что `REDIS_URL` парсится и содержит непустой пароль; если нет — exit с понятным сообщением.
317
+
318
+ G.3. Не зависит от A-F. Можно делать параллельно.
319
+
320
+ ---
321
+
322
+ ## Verification (общая)
323
+
324
+ 1. `cd codex-platform && pytest` — трек A зелёный.
325
+ 2. `cd codex-django && pytest` — треки B/C/D зелёные.
326
+ 3. `cd lily_website && pytest tests/lily_backend/unit` — трек E unit зелёный.
327
+ 4. Поднять Docker redis, `pytest tests/lily_backend/integration tests/lily_backend/regression` — зелёные.
328
+ 5. Docker smoke:
329
+ ```
330
+ docker compose -f deploy/docker-compose.yml up -d redis backend
331
+ docker exec lily_website-backend python manage.py update_site_settings --force
332
+ docker exec lily_website-backend python manage.py update_site_settings --force # 2-й
333
+ docker exec lily_website-backend python manage.py update_all_content
334
+ docker exec lily_website-backend python manage.py load_catalog
335
+ docker exec lily_website-backend python manage.py shell -c \
336
+ "from django.core.cache import cache; cache.set('redis_check','ok',30); print(cache.get('redis_check'))"
337
+ # Browser: admin login + /ru/cabinet/ × 2 + /ru/team/ + /ru/contacts/
338
+ # Логи: без AttributeError, Event loop is closed, Future attached to different loop, DataError.
339
+ ```
340
+ 6. **Acceptance metric:** `/ru/cabinet/` p95 в staging с `USE_CODEX_REDIS_BACKENDS=true` не хуже чем с hotfix'ом на ≥50ms. При превышении — follow-up на connection pool / loop-local strong cache.
341
+ 7. Трек G: `docker compose config` без warnings.
342
+
343
+ ## Critical files (сводка)
344
+
345
+ **codex-platform** (`C:\install\projects\codex_tools\codex-platform\src\codex_platform\redis_service\`):
346
+ - `operations/hash.py` — `encoder` параметр.
347
+ - `operations/sync_string.py`, `operations/sync_hash.py` — новые sync-operations.
348
+ - `base.py` — без изменений; `BaseRedisManager` остаётся async-only service-layer базой и больше не используется codex-django.
349
+
350
+ **codex-django** (`C:\install\projects\codex_tools\codex-django\src\codex_django\`):
351
+ - `core/redis/managers/base.py` — `BaseDjangoRedisManager` переписан: client factory, 4 context manager'а (`sync_string`, `sync_hash`, `async_string`, `async_hash`), не наследуется от `BaseRedisManager`.
352
+ - `cache/backends/redis.py` — sync через `sync_string()/sync_hash()`.
353
+ - `sessions/backends/redis.py` — sync через `sync_string()`.
354
+ - `system/redis/managers/fixtures.py` — через `sync_string()/async_string()`.
355
+ - `cache/values.py` — CacheCoder extensions.
356
+ - `core/redis/managers/settings.py` — write с CacheCoder, read с typed restore.
357
+ - `cabinet/redis/managers/dashboard.py` — через `sync_string()/async_string()`.
358
+ - `cabinet/selector/dashboard.py:41` — lazy `get_dashboard_redis_manager()`.
359
+ - `core/seo/selectors.py` — то же.
360
+ - `core/redis/managers/notifications.py` — проверить, мигрировать.
361
+
362
+ **lily_website** (`C:\install\projects\clients\lily_website\src\lily_backend\`):
363
+ - `system/models/settings.py:89-98` — удалить workaround.
364
+ - `system/redis.py` — мигрировать `ActionTokenRedisManager`.
365
+ - `core/settings/prod.py` — feature flag `USE_CODEX_REDIS_BACKENDS`.
366
+
367
+ ## Risks / open questions
368
+
369
+ - **Performance:** new-client-per-operation. Acceptance metric p95 +50ms max — иначе follow-up. Оптимизация: connection pool на уровне factory (не client) — `redis.Redis(connection_pool=pool)` с общим pool; pool thread-safe, но async pool — нет. Для first-fix не делаем.
370
+ - **Breaking change BaseDjangoRedisManager:** публичный API полностью меняется (`self.string`/`self.hash`/`self._client` удалены). Т.к. библиотеки в alpha и единственный consumer — lily_website, compat-слой не тащим. Bump codex-django major (0.x → 0.(x+1)) + changelog с migration guide.
371
+ - **Grep-gate не проходит — не мержим.** Любое оставшееся `self._client` / `.string.` / `.hash.` вне operation-классов codex-platform = 500 в проде.
372
+ - **CharField "" vs None:** typed restore не превращает пустую строку в None для CharField/TextField.
373
+ - **Enum с non-primitive value:** `dump_enum` рекурсивно через `CacheCoder.dump(value.value)` — покрывает.
374
+ - **Windows vs Linux asyncio:** smoke только в Linux-контейнере.
375
+ - **Координация релизов:** codex-platform → codex-django → lily_website pin commit. Editable install in dev; pinned in prod.
376
+ - **django-redis как fallback:** оставляем в deps, не удаляем в этом PR. Удалять — отдельным cleanup после ≥ 2 недель успеха с flag=True.
@@ -8,7 +8,7 @@ Usage:
8
8
  registry.register("smtp", lambda cfg: SmtpChannel(cfg) if cfg.SMTP_HOST else None)
9
9
  registry.register("sendgrid", lambda cfg: SendGridChannel(cfg) if cfg.SENDGRID_API_KEY else None)
10
10
  channels = registry.build_channels(settings)
11
- # → [SmtpChannel, SendGridChannel] (только те, чей конфиг заполнен)
11
+ # → [SmtpChannel, SendGridChannel] (only those whose config is populated)
12
12
  """
13
13
 
14
14
  import logging
@@ -52,6 +52,37 @@ def catch_redis_errors(func: Callable[..., Any]) -> Callable[..., Any]:
52
52
  return wrapper
53
53
 
54
54
 
55
+ def catch_redis_errors_sync(func: Callable[..., Any]) -> Callable[..., Any]:
56
+ """Decorator that converts redis-py exceptions into typed domain exceptions for sync functions.
57
+
58
+ Never suppresses errors — always re-raises as a domain exception.
59
+ Preserves the original traceback via ``raise ... from e``.
60
+
61
+ Catches:
62
+ - ``ConnectionError``, ``TimeoutError`` → :exc:`RedisConnectionError`
63
+ - ``RedisError`` → :exc:`RedisServiceError`
64
+
65
+ Does NOT catch:
66
+ ``JSONDecodeError``, ``TypeError`` — data-specific errors are caught
67
+ in individual operations and re-raised as :exc:`RedisDataError`.
68
+ """
69
+
70
+ @wraps(func)
71
+ def wrapper(*args: Any, **kwargs: Any) -> Any:
72
+ try:
73
+ return func(*args, **kwargs)
74
+ except (ConnectionError, TimeoutError) as e:
75
+ msg = f"Network failure in {func.__name__}: {e}"
76
+ log.exception("Redis | error=connection_failed fn='%s'", func.__name__)
77
+ raise RedisConnectionError(msg) from e
78
+ except RedisError as e:
79
+ msg = f"Operation failed in {func.__name__}: {e}"
80
+ log.exception("Redis | error=operation_failed fn='%s'", func.__name__)
81
+ raise RedisServiceError(msg) from e
82
+
83
+ return wrapper
84
+
85
+
55
86
  class BaseRedisService:
56
87
  """Base class that holds a Redis connection.
57
88
 
@@ -0,0 +1,20 @@
1
+ """
2
+ codex_platform.redis_service.exceptions
3
+ =======================================
4
+ Custom exceptions for the Redis service layer.
5
+
6
+ Use these classes in business logic instead of redis.exceptions.*
7
+ to avoid coupling to redis-py internals.
8
+ """
9
+
10
+
11
+ class RedisServiceError(Exception):
12
+ """Base class for all Redis layer errors."""
13
+
14
+
15
+ class RedisConnectionError(RedisServiceError):
16
+ """Network errors: timeouts, server unavailability, connection drops."""
17
+
18
+
19
+ class RedisDataError(RedisServiceError):
20
+ """Content errors: malformed JSON, type mismatches."""