python-ddd-framework 0.5.1__py3-none-any.whl → 0.7.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 (164) hide show
  1. python_ddd_framework/application_services/bindings.py +0 -64
  2. python_ddd_framework/application_services/dispatcher.py +1 -1
  3. python_ddd_framework/application_services/invocation.py +7 -563
  4. python_ddd_framework/application_services/module.py +16 -5
  5. python_ddd_framework/authorization/checking.py +35 -0
  6. python_ddd_framework/authorization/module.py +1 -1
  7. python_ddd_framework/background_jobs/execution.py +1 -1
  8. python_ddd_framework/background_jobs/pgqueuer/enqueue.py +1 -1
  9. python_ddd_framework/background_workers/execution.py +1 -1
  10. python_ddd_framework/background_workers/runtime.py +1 -1
  11. python_ddd_framework/caching/unit_of_work.py +2 -4
  12. python_ddd_framework/cli/project.py +11 -10
  13. python_ddd_framework/developer_kit/generation.py +44 -43
  14. python_ddd_framework/developer_kit/publication.py +442 -0
  15. python_ddd_framework/developer_kit/templates/module/cookiecutter.json +1 -1
  16. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/README.md +34 -44
  17. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/module.py.jinja +5 -72
  18. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/module.py.jinja +1 -3
  19. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/module.py.jinja +2 -3
  20. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/module.py.jinja +8 -62
  21. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/module.py.jinja +1 -2
  22. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/AGENTS.md +23 -8
  23. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/README.md +22 -10
  24. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/backend/src/host/main.py.jinja +4 -1
  25. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/docs/architecture.md +55 -5
  26. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/docs/development.md +233 -25
  27. python_ddd_framework/diagnostics/source.py +14 -1
  28. python_ddd_framework/domain/aggregates.py +5 -12
  29. python_ddd_framework/domain/event_values.py +124 -0
  30. python_ddd_framework/events/runtime.py +2 -7
  31. python_ddd_framework/events/unit_of_work.py +1 -4
  32. python_ddd_framework/fastapi/__init__.py +2 -0
  33. python_ddd_framework/fastapi/access.py +127 -0
  34. python_ddd_framework/fastapi/access_options.py +13 -0
  35. python_ddd_framework/fastapi/action.py +8 -1
  36. python_ddd_framework/fastapi/adapter.py +11 -117
  37. python_ddd_framework/fastapi/application_services.py +17 -402
  38. python_ddd_framework/fastapi/error_handlers.py +128 -0
  39. python_ddd_framework/fastapi/manual_action.py +33 -2
  40. python_ddd_framework/fastapi/module.py +2 -0
  41. python_ddd_framework/fastapi/realtime/authentication.py +21 -10
  42. python_ddd_framework/fastapi/realtime/options.py +0 -1
  43. python_ddd_framework/fastapi/realtime/runtime.py +42 -27
  44. python_ddd_framework/fastapi/request_context.py +12 -7
  45. python_ddd_framework/fastapi/routing.py +16 -15
  46. python_ddd_framework/fastapi/server.py +9 -0
  47. python_ddd_framework/fastapi/service_endpoints.py +187 -0
  48. python_ddd_framework/fastapi/service_routes.py +237 -0
  49. python_ddd_framework/fastapi/streaming.py +95 -0
  50. python_ddd_framework/fastapi/transfer.py +139 -4
  51. python_ddd_framework/hosted_services/bridge.py +2 -6
  52. python_ddd_framework/hosted_services/runtime.py +1 -1
  53. python_ddd_framework/identity/__init__.py +21 -13
  54. python_ddd_framework/identity/application.py +12 -66
  55. python_ddd_framework/identity/contracts.py +38 -411
  56. python_ddd_framework/identity/domain_rules.py +17 -0
  57. python_ddd_framework/identity/dtos.py +152 -0
  58. python_ddd_framework/identity/extensions.py +9 -0
  59. python_ddd_framework/identity/management.py +28 -25
  60. python_ddd_framework/identity/models.py +86 -0
  61. python_ddd_framework/identity/module.py +3 -4
  62. python_ddd_framework/identity/options.py +54 -0
  63. python_ddd_framework/identity/seeding.py +59 -0
  64. python_ddd_framework/identity/services.py +1 -1
  65. python_ddd_framework/identity/sqlalchemy/module.py +9 -6
  66. python_ddd_framework/identity/sqlalchemy/repository.py +4 -5
  67. python_ddd_framework/identity/sqlalchemy/session_security.py +1 -3
  68. python_ddd_framework/identity/sqlalchemy/stores.py +35 -47
  69. python_ddd_framework/identity/stores.py +120 -0
  70. python_ddd_framework/identity/tokens.py +1 -1
  71. python_ddd_framework/invocation/callables.py +1 -1
  72. python_ddd_framework/invocation/dispatcher.py +3 -25
  73. python_ddd_framework/invocation/entrypoints.py +2 -1
  74. python_ddd_framework/invocation/function_runtime.py +2 -2
  75. python_ddd_framework/invocation/interception.py +1 -1
  76. python_ddd_framework/invocation/managed_proxy.py +2 -1
  77. python_ddd_framework/invocation/managed_services.py +8 -14
  78. python_ddd_framework/invocation/methods.py +2 -7
  79. python_ddd_framework/invocation/module.py +1 -1
  80. python_ddd_framework/invocation/runtime.py +483 -0
  81. python_ddd_framework/invocation/scopes.py +93 -0
  82. python_ddd_framework/messaging/channel.py +1 -1
  83. python_ddd_framework/messaging/runtime.py +1 -1
  84. python_ddd_framework/modularity/discovery.py +2 -7
  85. python_ddd_framework/modularity/graph.py +2 -8
  86. python_ddd_framework/observability/formatting.py +1 -0
  87. python_ddd_framework/observability/logging.py +1 -1
  88. python_ddd_framework/observability/tracing.py +1 -1
  89. python_ddd_framework/redis/cache.py +2 -0
  90. python_ddd_framework/redis/notification_runtime.py +1 -1
  91. python_ddd_framework/services/arbitration.py +1 -20
  92. python_ddd_framework/services/composition.py +71 -2
  93. python_ddd_framework/services/provider.py +23 -1
  94. python_ddd_framework/services/relocation.py +96 -0
  95. python_ddd_framework/settings/refresh.py +1 -1
  96. python_ddd_framework/settings/sqlalchemy/store.py +1 -1
  97. python_ddd_framework/sqlalchemy/metadata.py +11 -413
  98. python_ddd_framework/sqlalchemy/metadata_builder.py +234 -0
  99. python_ddd_framework/sqlalchemy/metadata_fingerprint.py +108 -0
  100. python_ddd_framework/sqlalchemy/metadata_ownership.py +73 -0
  101. python_ddd_framework/sqlalchemy/migration.py +2 -1
  102. python_ddd_framework/sqlalchemy/migration_sources.py +38 -0
  103. python_ddd_framework/sqlalchemy/module.py +2 -1
  104. python_ddd_framework/sqlalchemy/unit_of_work.py +1 -1
  105. python_ddd_framework/unit_of_work/contracts.py +68 -7
  106. python_ddd_framework/unit_of_work/manager.py +15 -9
  107. python_ddd_framework/unit_of_work/module.py +1 -1
  108. {python_ddd_framework-0.5.1.dist-info → python_ddd_framework-0.7.0.dist-info}/METADATA +101 -10
  109. {python_ddd_framework-0.5.1.dist-info → python_ddd_framework-0.7.0.dist-info}/RECORD +113 -141
  110. {python_ddd_framework-0.5.1.dist-info → python_ddd_framework-0.7.0.dist-info}/WHEEL +1 -1
  111. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_jobs/order_approval/__init__.py.jinja +0 -1
  112. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_jobs/order_approval/handler.py.jinja +0 -45
  113. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_jobs/order_approval/payload.py.jinja +0 -8
  114. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_jobs/order_statistics/__init__.py.jinja +0 -1
  115. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_jobs/order_statistics/handler.py.jinja +0 -35
  116. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_jobs/order_statistics/payload.py.jinja +0 -5
  117. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_jobs/order_statistics/schedule.py.jinja +0 -15
  118. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_workers/order_maintenance_worker.py.jinja +0 -28
  119. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_workers/order_statistics_worker.py.jinja +0 -26
  120. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/caching/order_cache.py.jinja +0 -9
  121. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/caching/statistics_cache.py.jinja +0 -9
  122. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/event_handlers/order_changed_handler.py.jinja +0 -23
  123. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/hosted_services/order_integration_service.py.jinja +0 -71
  124. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/hosted_services/order_observation_handler.py.jinja +0 -16
  125. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/integration_services/order_reporting_service.py.jinja +0 -16
  126. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/interceptors/order_timing_interceptor.py.jinja +0 -17
  127. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/options/order_options.py.jinja +0 -7
  128. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/services/order_approval_service.py.jinja +0 -65
  129. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/services/order_management_service.py.jinja +0 -35
  130. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/services/order_query_service.py.jinja +0 -41
  131. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/setting_handlers/approval_setting_observer.py.jinja +0 -17
  132. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/inputs/approve_order.py.jinja +0 -6
  133. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/inputs/create_order.py.jinja +0 -7
  134. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/integration_services/order_reporting_service.py.jinja +0 -7
  135. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/services/order_approval_service.py.jinja +0 -13
  136. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/services/order_management_service.py.jinja +0 -10
  137. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/services/order_query_service.py.jinja +0 -12
  138. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/views/order_statistics_snapshot.py.jinja +0 -9
  139. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/views/order_view.py.jinja +0 -12
  140. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/entities/order.py.jinja +0 -48
  141. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/events/order_changed.py.jinja +0 -13
  142. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/repositories/order_repository.py.jinja +0 -17
  143. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/seeding/order_seed_contributor.py.jinja +0 -22
  144. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/services/order_approval_service.py.jinja +0 -23
  145. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/settings/approval_settings.py.jinja +0 -17
  146. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/value_objects/order_title.py.jinja +0 -10
  147. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/constants/order_constants.py.jinja +0 -3
  148. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/enums/order_status.py.jinja +0 -6
  149. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/errors/order_errors.py.jinja +0 -11
  150. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/messages/order_messages.py.jinja +0 -26
  151. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/messages/order_observation.py.jinja +0 -13
  152. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/permissions/order_permission_provider.py.jinja +0 -19
  153. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/permissions/order_permissions.py.jinja +0 -7
  154. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/value_objects/money.py.jinja +0 -18
  155. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/filters/export_filter.py.jinja +0 -15
  156. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/models/refresh_orders.py.jinja +0 -5
  157. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/routers/order_files.py.jinja +0 -76
  158. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/websockets/order_socket.py.jinja +0 -44
  159. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/models/order_model.py.jinja +0 -29
  160. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/repositories/order_repository.py.jinja +0 -79
  161. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/tests/test_domain.py.jinja +0 -19
  162. {python_ddd_framework-0.5.1.dist-info → python_ddd_framework-0.7.0.dist-info}/entry_points.txt +0 -0
  163. {python_ddd_framework-0.5.1.dist-info → python_ddd_framework-0.7.0.dist-info}/licenses/LICENSE +0 -0
  164. {python_ddd_framework-0.5.1.dist-info → python_ddd_framework-0.7.0.dist-info}/licenses/src/python_ddd_framework/background_jobs/pgqueuer/UPSTREAM_LICENSE.txt +0 -0
@@ -25,15 +25,16 @@ from ..services.convention_contracts import (
25
25
  ConventionDiscoveryKind,
26
26
  _ServiceDefinition,
27
27
  )
28
+ from ..services.relocation import _BindingRelocation
28
29
  from ..settings.module import SettingsModule
29
30
  from ..unit_of_work.module import UnitOfWorkModule
30
31
  from .bindings import (
31
32
  _bind_application_services,
32
- _runtime_providers,
33
33
  _validate_application_service_bindings,
34
34
  _validate_application_service_dependencies,
35
35
  )
36
36
  from .catalog import (
37
+ _APPLICATION_SERVICE_COMPONENT,
37
38
  ApplicationServiceCatalog,
38
39
  _build_application_service_catalog,
39
40
  _DiscoveredApplicationService,
@@ -130,10 +131,20 @@ class ApplicationServicesModule(AppModule):
130
131
  context.modules.instance(InvocationModule).rules,
131
132
  context.modules.instance(ExtensionsModule).catalog,
132
133
  )
133
- # 只重定位仲裁后的原生绑定,保留其他能力已经生成的目录 Provider。
134
- for provider in context.arbitration.providers:
135
- context._providers.remove(provider)
136
- context._providers[0:0] = _runtime_providers(context.arbitration, bound)
134
+ # 能力只决定私有实现 key;来源、工厂搬迁和贡献指纹仍由 services 管理。
135
+ context._relocate_bindings(
136
+ tuple(
137
+ _BindingRelocation(
138
+ item.winner,
139
+ DependencyKey(
140
+ item.binding.discovery.definition.contract
141
+ or item.binding.discovery.service_type,
142
+ _APPLICATION_SERVICE_COMPONENT,
143
+ ),
144
+ )
145
+ for item in bound
146
+ )
147
+ )
137
148
  provider = Provider()
138
149
  provider.provide(lambda: self.catalog, provides=ApplicationServiceCatalog, scope=Scope.APP)
139
150
  provider.provide(_ApplicationServiceDispatcher, scope=Scope.REQUEST)
@@ -0,0 +1,35 @@
1
+ """所有调用与持续访问边界共用的授权规则;不拥有传输或调用 scope。"""
2
+
3
+ from collections.abc import Callable
4
+
5
+ from dishka import AsyncContainer
6
+ from dishka.exceptions import NoFactoryError
7
+
8
+ from ..options import Options
9
+ from .contracts import CurrentUser, PermissionChecker, _AuthorizationRequirement
10
+ from .errors import PermissionCheckerUnavailableError, PermissionDeniedError, UnauthenticatedError
11
+ from .options import AuthorizationOptions
12
+
13
+
14
+ async def _check_authorization(
15
+ container: AsyncContainer,
16
+ get_user: Callable[[], CurrentUser],
17
+ requirement: _AuthorizationRequirement,
18
+ ) -> None:
19
+ if requirement.allows_anonymous or not requirement.requires_authenticated_user:
20
+ return
21
+ if (await container.get(Options[AuthorizationOptions])).value.always_allow:
22
+ return
23
+ # 认证阶段也可调用无需授权的服务;只有实际校验身份时才读取最终 CurrentUser。
24
+ user = get_user()
25
+ if not user.is_authenticated:
26
+ raise UnauthenticatedError
27
+ if not requirement.permissions:
28
+ return
29
+ try:
30
+ checker = await container.get(PermissionChecker)
31
+ except NoFactoryError:
32
+ raise PermissionCheckerUnavailableError from None
33
+ for permission in requirement.permissions:
34
+ if not await checker.is_granted(user, permission):
35
+ raise PermissionDeniedError(permission=str(permission))
@@ -2,8 +2,8 @@
2
2
 
3
3
  from dishka import Provider, Scope
4
4
 
5
- from ..application_services.invocation import _InvocationRuntime
6
5
  from ..invocation.module import InvocationModule
6
+ from ..invocation.runtime import _InvocationRuntime
7
7
  from ..lifecycle import ConfigureContext
8
8
  from ..modularity import AppModule
9
9
  from ..services.composition import (
@@ -12,9 +12,9 @@ from uuid import uuid4
12
12
  from dishka import AsyncContainer, Scope
13
13
  from pydantic import BaseModel, ValidationError
14
14
 
15
- from ..application_services.invocation import _InvocationRuntime
16
15
  from ..authorization import CurrentUser
17
16
  from ..invocation import BackgroundSystemCaller
17
+ from ..invocation.runtime import _InvocationRuntime
18
18
  from ..observability.context import invocation_span
19
19
  from .catalog import BackgroundJobCatalog
20
20
  from .contracts import BackgroundJobContext, BackgroundJobDefinition
@@ -8,7 +8,7 @@ from typing import Any, TypeVar
8
8
  import asyncpg # type: ignore[import-untyped]
9
9
  from pydantic import BaseModel
10
10
 
11
- from ...application_services.invocation import _InvocationRuntime
11
+ from ...invocation.runtime import _InvocationRuntime
12
12
  from ...observability.context import capture_trace_context
13
13
  from ...options import Options
14
14
  from ...sqlalchemy import SqlAlchemySessionProvider
@@ -10,10 +10,10 @@ from uuid import uuid4
10
10
  from dishka import AsyncContainer, Scope
11
11
 
12
12
  from ..application_services.errors import ApplicationInvocationRejectedError
13
- from ..application_services.invocation import _InvocationRuntime
14
13
  from ..authorization import CurrentUser
15
14
  from ..background_execution.lifecycle import _TaskExecution
16
15
  from ..invocation import BackgroundSystemCaller
16
+ from ..invocation.runtime import _InvocationRuntime
17
17
  from ..unit_of_work.options import _DEFAULT_CONNECTION_NAME
18
18
  from .contracts import BackgroundWorkerContext, BackgroundWorkerDefinition
19
19
 
@@ -9,7 +9,6 @@ from collections.abc import Mapping
9
9
  from dishka import AsyncContainer
10
10
 
11
11
  from ..application.build_spec import _ApplicationBuildSpec
12
- from ..application_services.invocation import _InvocationRuntime
13
12
  from ..background_execution import (
14
13
  BackgroundExecutionMode,
15
14
  BackgroundJobOptions,
@@ -27,6 +26,7 @@ from ..background_execution.processes import _ProcessSlot
27
26
  from ..background_jobs import BackgroundJobCatalog
28
27
  from ..errors.lifecycle import ApplicationRuntimeError
29
28
  from ..invocation import BackgroundSystemCaller
29
+ from ..invocation.runtime import _InvocationRuntime
30
30
  from ..lifecycle import RuntimeParticipant, ShutdownReason
31
31
  from .catalog import BackgroundWorkerCatalog
32
32
  from .contracts import BackgroundExecutionOptions, BackgroundExecutionProfile
@@ -21,6 +21,7 @@ class _CachePending:
21
21
  async def after_commit(self) -> None:
22
22
  failures: list[Exception] = []
23
23
  try:
24
+ # 普通错误继续后项,取消立即传播;逐项 await 保留尚未发送命令的边界。
24
25
  for _, apply in self.writes.values():
25
26
  try:
26
27
  await apply()
@@ -40,7 +41,4 @@ class _CacheLifecycleFactory:
40
41
 
41
42
 
42
43
  def _pending(work: UnitOfWork) -> _CachePending:
43
- for lifecycle in work._lifecycles:
44
- if isinstance(lifecycle, _CachePending):
45
- return lifecycle
46
- raise RuntimeError("Caching lifecycle is not configured for this UnitOfWork")
44
+ return work._lifecycle(_CachePending)
@@ -18,6 +18,16 @@ from .errors import CommandError
18
18
 
19
19
  HOST_ENTRY_POINT_GROUP = "python_ddd_framework.hosts"
20
20
  MODULE_ENTRY_POINT_GROUP = "python_ddd_framework.modules"
21
+ _PROJECT_ENVIRONMENT_OVERRIDES = (
22
+ "VIRTUAL_ENV",
23
+ "PYTHONPATH",
24
+ "PYTHONHOME",
25
+ "UV_PROJECT_ENVIRONMENT",
26
+ "UV_PROJECT",
27
+ "UV_WORKING_DIR",
28
+ "UV_WORKING_DIRECTORY",
29
+ "UV_PYTHON",
30
+ )
21
31
 
22
32
 
23
33
  def read_project(path: Path) -> dict[str, Any]:
@@ -117,15 +127,6 @@ class Project:
117
127
  def project_environment() -> dict[str, str]:
118
128
  # 父进程可能来自 ROS、uv tool 或另一个项目;后端环境由本项目声明和 uv 选择。
119
129
  environment = dict(os.environ)
120
- for key in (
121
- "VIRTUAL_ENV",
122
- "PYTHONPATH",
123
- "PYTHONHOME",
124
- "UV_PROJECT_ENVIRONMENT",
125
- "UV_PROJECT",
126
- "UV_WORKING_DIR",
127
- "UV_WORKING_DIRECTORY",
128
- "UV_PYTHON",
129
- ):
130
+ for key in _PROJECT_ENVIRONMENT_OVERRIDES:
130
131
  environment.pop(key, None)
131
132
  return environment
@@ -26,6 +26,7 @@ from ..cli import ENVIRONMENT_VARIABLE
26
26
  from ..cli.errors import CommandError
27
27
  from ..cli.project import Project, project_environment, read_project
28
28
  from .project_metadata import add_module_metadata
29
+ from .publication import _ModulePublication
29
30
  from .source import framework_source
30
31
  from .wiring import add_dependency
31
32
 
@@ -41,8 +42,11 @@ def _name(value: str) -> tuple[str, str]:
41
42
  return package, "".join(part.capitalize() for part in package.split("_"))
42
43
 
43
44
 
44
- def _render(template: str, context: dict[str, Any]) -> Path:
45
- staging = Path(tempfile.mkdtemp(prefix="python-ddd-framework-generation-"))
45
+ def _render(template: str, context: dict[str, Any], *, staging: Path | None = None) -> Path:
46
+ if staging is None:
47
+ staging = Path(tempfile.mkdtemp(prefix="python-ddd-framework-generation-"))
48
+ else:
49
+ staging.mkdir()
46
50
  root = resources.files(__package__).joinpath("templates").joinpath(template)
47
51
  with resources.as_file(root) as template_path:
48
52
  # Cookiecutter 用相对路径加载;安装目录较深时先复制到短 staging,避免 Windows MAX_PATH。
@@ -152,6 +156,26 @@ def new_project(
152
156
  def add_module(name: str, dry_run: bool, *, template: Literal["basic", "ddd"] = "ddd") -> None:
153
157
  package, class_prefix = _name(name)
154
158
  project = Project.current()
159
+ if dry_run:
160
+ _add_module(project, package, class_prefix, template, None)
161
+ else:
162
+ with _ModulePublication(project.root, package, template) as publication:
163
+ _add_module(project, package, class_prefix, template, publication)
164
+
165
+
166
+ def _add_module(
167
+ project: Project,
168
+ package: str,
169
+ class_prefix: str,
170
+ template: Literal["basic", "ddd"],
171
+ publication: _ModulePublication | None,
172
+ ) -> None:
173
+ destination = project.root / "src" / "modules" / package
174
+ if destination.exists() or destination.is_symlink():
175
+ raise CommandError(
176
+ f"Module destination already exists: {destination}; review retained operations in "
177
+ f"{project.root / '.python-ddd-framework' / 'generation'}"
178
+ )
155
179
  application = project.host("application").load()(environment="development")
156
180
  if not isinstance(application, Application):
157
181
  raise CommandError("The application entry point must return Application")
@@ -166,11 +190,8 @@ def add_module(name: str, dry_run: bool, *, template: Literal["basic", "ddd"] =
166
190
  raise CommandError(
167
191
  "Startup Module source must belong to the current project's src directory"
168
192
  )
169
- modules = project.root / "src" / "modules"
170
- destination = modules / package
171
- if destination.exists():
172
- raise CommandError(f"Module destination already exists: {destination}")
173
- source = source_path.read_text(encoding="utf-8")
193
+ source_bytes = source_path.read_bytes()
194
+ source = source_bytes.decode("utf-8")
174
195
  updated = source
175
196
  entries = (
176
197
  ((f"modules.{package}.module", class_prefix + "Module"),)
@@ -187,49 +208,29 @@ def add_module(name: str, dry_run: bool, *, template: Literal["basic", "ddd"] =
187
208
  for import_path, class_name in entries:
188
209
  updated = add_dependency(updated, startup.__name__, import_path, class_name)
189
210
  metadata_path = project.root / "pyproject.toml"
190
- metadata_source = metadata_path.read_text(encoding="utf-8")
211
+ metadata_bytes = metadata_path.read_bytes()
212
+ metadata_source = metadata_bytes.decode("utf-8")
191
213
  alias_path, alias_class = entries[0 if template == "basic" else 1]
192
214
  updated_metadata = add_module_metadata(metadata_source, package, f"{alias_path}:{alias_class}")
193
215
  ast.parse(updated)
194
- if dry_run:
216
+ if publication is None:
195
217
  print(
196
218
  f"Would add {destination}, wire modules into {source_path} and update {metadata_path}"
197
219
  )
198
220
  return
199
221
  context = {"module_name": package, "class_prefix": class_prefix}
200
- if template == "ddd":
201
- from ..fastapi.application_services import _operation_id, _service_path
202
-
203
- # HTTP 身份由正式约定推导;basic 不读取或生成传输层配置。
204
- http_identity = type(class_prefix + "ApplicationService", (), {})
205
- context.update(
206
- http_service_path=_service_path("", http_identity),
207
- http_operation_prefix=_operation_id(http_identity, ""),
222
+ with publication.step("render"):
223
+ staged = _render(
224
+ "basic" if template == "basic" else "module", context, staging=publication.rendering
208
225
  )
209
- staged = _render(
210
- "basic" if template == "basic" else "module",
211
- context,
212
- )
213
- # 先验证全部新文件和接线;已有 startup 文件仅在内容仍等于读取快照时替换。
214
- if (
215
- source_path.read_text(encoding="utf-8") != source
216
- or metadata_path.read_text(encoding="utf-8") != metadata_source
217
- ):
218
- raise CommandError("Startup source changed during generation; retry after reviewing it")
219
- modules.mkdir(exist_ok=True)
220
- if not modules.joinpath("__init__.py").exists():
221
- modules.joinpath("__init__.py").touch()
222
- shutil.copytree(staged, destination)
223
- with tempfile.NamedTemporaryFile(
224
- mode="w", encoding="utf-8", dir=source_path.parent, delete=False
225
- ) as pending:
226
- pending.write(updated)
227
- Path(pending.name).replace(source_path)
228
- metadata_path.write_text(updated_metadata, encoding="utf-8")
229
- subprocess.run(
230
- ["uv", "sync", "--directory", str(project.root)],
231
- cwd=project.root,
232
- env=project_environment(),
233
- check=True,
234
- )
226
+ with publication.step("prepare"):
227
+ publication.prepare(
228
+ staged,
229
+ (
230
+ ("startup", source_path, source_bytes, updated.encode("utf-8")),
231
+ ("metadata", metadata_path, metadata_bytes, updated_metadata.encode("utf-8")),
232
+ ),
233
+ )
234
+ publication.publish(staged)
235
+ publication.sync()
235
236
  print(f"Added {package}; staging retained at {staged}")