python-ddd-framework 0.7.3__py3-none-any.whl → 0.9.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. python_ddd_framework/__init__.py +2 -2
  2. python_ddd_framework/configuration/root.py +2 -0
  3. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/README.md +6 -0
  4. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/AGENTS.md +1 -0
  5. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/README.md +6 -0
  6. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/backend/src/host/module.py.jinja +0 -4
  7. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/docs/architecture.md +6 -0
  8. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/docs/development.md +62 -2
  9. python_ddd_framework/settings/__init__.py +3 -7
  10. python_ddd_framework/settings/application.py +53 -21
  11. python_ddd_framework/settings/binding.py +14 -0
  12. python_ddd_framework/settings/catalog.py +4 -59
  13. python_ddd_framework/settings/contracts.py +73 -2
  14. python_ddd_framework/settings/management.py +3 -13
  15. python_ddd_framework/settings/manager.py +17 -34
  16. python_ddd_framework/settings/module.py +21 -9
  17. python_ddd_framework/settings/provider.py +10 -3
  18. python_ddd_framework/settings/values.py +0 -8
  19. {python_ddd_framework-0.7.3.dist-info → python_ddd_framework-0.9.0.dist-info}/METADATA +27 -3
  20. {python_ddd_framework-0.7.3.dist-info → python_ddd_framework-0.9.0.dist-info}/RECORD +24 -31
  21. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/setting_handlers/__init__.py.jinja +0 -1
  22. python_ddd_framework/settings/changes.py +0 -14
  23. python_ddd_framework/settings/declarations.py +0 -18
  24. python_ddd_framework/settings/handlers.py +0 -43
  25. python_ddd_framework/settings/notifications.py +0 -41
  26. python_ddd_framework/settings/refresh.py +0 -140
  27. python_ddd_framework/settings/refresh_module.py +0 -52
  28. {python_ddd_framework-0.7.3.dist-info → python_ddd_framework-0.9.0.dist-info}/WHEEL +0 -0
  29. {python_ddd_framework-0.7.3.dist-info → python_ddd_framework-0.9.0.dist-info}/entry_points.txt +0 -0
  30. {python_ddd_framework-0.7.3.dist-info → python_ddd_framework-0.9.0.dist-info}/licenses/LICENSE +0 -0
  31. {python_ddd_framework-0.7.3.dist-info → python_ddd_framework-0.9.0.dist-info}/licenses/src/python_ddd_framework/background_jobs/pgqueuer/UPSTREAM_LICENSE.txt +0 -0
@@ -1,140 +0,0 @@
1
- """单进程 Settings 刷新 owner:合并读取、串行应用、失败后按周期重试。"""
2
-
3
- from __future__ import annotations
4
-
5
- import asyncio
6
- import logging
7
- from contextlib import suppress
8
- from dataclasses import dataclass
9
- from typing import Any, ClassVar
10
-
11
- from pydantic import ConfigDict, Field
12
-
13
- from ..hosted_services import HostedService, HostedServiceContext
14
- from ..invocation.runtime import _InvocationRuntime
15
- from ..lifecycle import ShutdownReason
16
- from ..observability.context import capture_trace_context, invocation_span
17
- from ..options import BaseOptions, Options
18
- from .contracts import SettingDefinition
19
- from .handlers import SettingApplicationStatus, SettingHandler
20
- from .provider import SettingProvider
21
- from .values import SettingSnapshot, SettingValueSource
22
-
23
- _LOGGER = logging.getLogger(__name__)
24
-
25
-
26
- class SettingsRefreshOptions(BaseOptions):
27
- model_config = ConfigDict(extra="forbid")
28
-
29
- reconciliation_interval_seconds: float = Field(default=30.0, gt=0)
30
-
31
-
32
- @dataclass(slots=True)
33
- class _ApplicationRecord:
34
- handler: SettingHandler[Any]
35
- last_applied: SettingSnapshot[Any] | None = None
36
- error_type: str | None = None
37
-
38
-
39
- class SettingsRefreshRuntime(HostedService):
40
- run_in_background_processes: ClassVar[bool] = True
41
-
42
- def __init__(
43
- self,
44
- settings: SettingProvider,
45
- options: Options[SettingsRefreshOptions],
46
- runtime: _InvocationRuntime,
47
- ) -> None:
48
- self._settings = settings
49
- self._options = options.value
50
- self._runtime = runtime
51
- self._definitions: dict[str, SettingDefinition[Any]] = {}
52
- self._records: dict[str, list[_ApplicationRecord]] = {}
53
- self._dirty: dict[str, dict[str, str]] = {}
54
- self._wakeup = asyncio.Event()
55
- self._task: asyncio.Task[None] | None = None
56
-
57
- def _register(self, definition: SettingDefinition[Any], handler: SettingHandler[Any]) -> None:
58
- self._definitions[definition.name] = definition
59
- self._records.setdefault(definition.name, []).append(_ApplicationRecord(handler))
60
-
61
- def request_refresh(self, name: str | None = None) -> None:
62
- if name is None:
63
- for key in self._definitions:
64
- self._dirty.setdefault(key, {})
65
- elif name in self._definitions:
66
- self._dirty[name] = capture_trace_context(self._runtime.tracer)
67
- if self._dirty:
68
- self._wakeup.set()
69
-
70
- @property
71
- def statuses(self) -> tuple[SettingApplicationStatus, ...]:
72
- return tuple(
73
- SettingApplicationStatus(
74
- name,
75
- f"{type(record.handler).__module__}.{type(record.handler).__qualname__}",
76
- record.last_applied,
77
- record.error_type,
78
- )
79
- for name, records in self._records.items()
80
- for record in records
81
- )
82
-
83
- async def start(self, context: HostedServiceContext) -> None:
84
- self.request_refresh()
85
- self._task = asyncio.create_task(self._run(), name=context.name)
86
-
87
- async def stop(self, reason: ShutdownReason) -> None:
88
- if self._task is not None:
89
- self._task.cancel()
90
- with suppress(asyncio.CancelledError):
91
- await self._task
92
-
93
- async def _run(self) -> None:
94
- if not self._definitions:
95
- await asyncio.Future[None]()
96
- while True:
97
- try:
98
- await asyncio.wait_for(
99
- self._wakeup.wait(), timeout=self._options.reconciliation_interval_seconds
100
- )
101
- except TimeoutError:
102
- self.request_refresh()
103
- # 取走本轮名字后立即清理;await 期间到达的新请求进入下一轮,不能被误清除。
104
- requests = tuple(self._dirty.items())
105
- self._dirty.clear()
106
- self._wakeup.clear()
107
- for name, carrier in requests:
108
- with invocation_span(
109
- self._runtime.tracer, f"refresh setting {name}", carrier=carrier
110
- ):
111
- await self._refresh(name)
112
-
113
- async def _refresh(self, name: str) -> None:
114
- try:
115
- latest = await self._settings.snapshot(self._definitions[name])
116
- except Exception as error:
117
- for record in self._records[name]:
118
- record.error_type = type(error).__name__
119
- _LOGGER.error("Settings refresh failed for %s: %s", name, type(error).__name__)
120
- return
121
- for record in self._records[name]:
122
- previous = record.last_applied
123
- if (
124
- previous is not None
125
- and record.error_type is None
126
- and previous.value == latest.value
127
- and (previous.source is SettingValueSource.UNSET)
128
- == (latest.source is SettingValueSource.UNSET)
129
- ):
130
- continue
131
- try:
132
- with invocation_span(self._runtime.tracer, f"apply setting {name}"):
133
- await record.handler.apply(latest)
134
- except Exception as error:
135
- # 失败不能覆盖成功记录,下一次校对仍会按最后成功应用的值决定重试。
136
- record.error_type = type(error).__name__
137
- _LOGGER.error("Settings handler failed for %s: %s", name, record.error_type)
138
- else:
139
- record.last_applied = latest
140
- record.error_type = None
@@ -1,52 +0,0 @@
1
- """只组装正式 handler 与刷新运行时;组件应用规则由 handler 自己拥有。"""
2
-
3
- from __future__ import annotations
4
-
5
- from dishka import DEFAULT_COMPONENT, DependencyKey
6
-
7
- from python_ddd_framework import ConfigureContext
8
-
9
- from ..hosted_services.declarations import HostedServices
10
- from ..lifecycle import PreInitializeContext, ShutdownContext
11
- from ..modularity import AppModule, ModuleRef
12
- from .catalog import SettingCatalog
13
- from .manager import SettingManager
14
- from .module import SettingsModule
15
- from .refresh import SettingsRefreshOptions, SettingsRefreshRuntime
16
-
17
-
18
- class SettingsRefreshModule(AppModule):
19
- dependencies = (
20
- SettingsModule,
21
- ModuleRef("python_ddd_framework.hosted_services.module:HostedServicesModule"),
22
- )
23
-
24
- def __init__(self) -> None:
25
- self._manager: SettingManager | None = None
26
-
27
- def configure(self, context: ConfigureContext) -> None:
28
- context.configure(SettingsRefreshOptions, section="settings_refresh")
29
-
30
- hosted_services = HostedServices((SettingsRefreshRuntime,))
31
-
32
- async def pre_initialize(self, context: PreInitializeContext) -> None:
33
- catalog = await context.container.get(SettingCatalog)
34
- refresh = await context.container.get(SettingsRefreshRuntime)
35
- for definition in catalog.handlers:
36
- handler = await context.container.get(definition.handler_type)
37
- refresh._register(definition.setting, handler)
38
- # Manager 固定归 APP;查询最终原生图,含 alias,不构造服务或维护第二清单。
39
- # 无存储仍应用只读值;已注册服务由原生 get 解析,构造失败不能降级。
40
- if (
41
- context.container.registry.get_factory(DependencyKey(SettingManager, DEFAULT_COMPONENT))
42
- is not None
43
- ):
44
- settings = await context.container.get(SettingManager)
45
- settings._refresh = refresh
46
- self._manager = settings
47
-
48
- async def shutdown(self, context: ShutdownContext) -> None:
49
- # 只解除本实例完成的接线;启动部分失败后的关闭不得重新构造依赖。
50
- if self._manager is not None:
51
- self._manager._refresh = None
52
- self._manager = None