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.
- python_ddd_framework/application_services/bindings.py +0 -64
- python_ddd_framework/application_services/dispatcher.py +1 -1
- python_ddd_framework/application_services/invocation.py +7 -563
- python_ddd_framework/application_services/module.py +16 -5
- python_ddd_framework/authorization/checking.py +35 -0
- python_ddd_framework/authorization/module.py +1 -1
- python_ddd_framework/background_jobs/execution.py +1 -1
- python_ddd_framework/background_jobs/pgqueuer/enqueue.py +1 -1
- python_ddd_framework/background_workers/execution.py +1 -1
- python_ddd_framework/background_workers/runtime.py +1 -1
- python_ddd_framework/caching/unit_of_work.py +2 -4
- python_ddd_framework/cli/project.py +11 -10
- python_ddd_framework/developer_kit/generation.py +44 -43
- python_ddd_framework/developer_kit/publication.py +442 -0
- python_ddd_framework/developer_kit/templates/module/cookiecutter.json +1 -1
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/README.md +34 -44
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/module.py.jinja +5 -72
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/module.py.jinja +1 -3
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/module.py.jinja +2 -3
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/module.py.jinja +8 -62
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/module.py.jinja +1 -2
- python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/AGENTS.md +23 -8
- python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/README.md +22 -10
- python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/backend/src/host/main.py.jinja +4 -1
- python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/docs/architecture.md +55 -5
- python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/docs/development.md +233 -25
- python_ddd_framework/diagnostics/source.py +14 -1
- python_ddd_framework/domain/aggregates.py +5 -12
- python_ddd_framework/domain/event_values.py +124 -0
- python_ddd_framework/events/runtime.py +2 -7
- python_ddd_framework/events/unit_of_work.py +1 -4
- python_ddd_framework/fastapi/__init__.py +2 -0
- python_ddd_framework/fastapi/access.py +127 -0
- python_ddd_framework/fastapi/access_options.py +13 -0
- python_ddd_framework/fastapi/action.py +8 -1
- python_ddd_framework/fastapi/adapter.py +11 -117
- python_ddd_framework/fastapi/application_services.py +17 -402
- python_ddd_framework/fastapi/error_handlers.py +128 -0
- python_ddd_framework/fastapi/manual_action.py +33 -2
- python_ddd_framework/fastapi/module.py +2 -0
- python_ddd_framework/fastapi/realtime/authentication.py +21 -10
- python_ddd_framework/fastapi/realtime/options.py +0 -1
- python_ddd_framework/fastapi/realtime/runtime.py +42 -27
- python_ddd_framework/fastapi/request_context.py +12 -7
- python_ddd_framework/fastapi/routing.py +16 -15
- python_ddd_framework/fastapi/server.py +9 -0
- python_ddd_framework/fastapi/service_endpoints.py +187 -0
- python_ddd_framework/fastapi/service_routes.py +237 -0
- python_ddd_framework/fastapi/streaming.py +95 -0
- python_ddd_framework/fastapi/transfer.py +139 -4
- python_ddd_framework/hosted_services/bridge.py +2 -6
- python_ddd_framework/hosted_services/runtime.py +1 -1
- python_ddd_framework/identity/__init__.py +21 -13
- python_ddd_framework/identity/application.py +12 -66
- python_ddd_framework/identity/contracts.py +38 -411
- python_ddd_framework/identity/domain_rules.py +17 -0
- python_ddd_framework/identity/dtos.py +152 -0
- python_ddd_framework/identity/extensions.py +9 -0
- python_ddd_framework/identity/management.py +28 -25
- python_ddd_framework/identity/models.py +86 -0
- python_ddd_framework/identity/module.py +3 -4
- python_ddd_framework/identity/options.py +54 -0
- python_ddd_framework/identity/seeding.py +59 -0
- python_ddd_framework/identity/services.py +1 -1
- python_ddd_framework/identity/sqlalchemy/module.py +9 -6
- python_ddd_framework/identity/sqlalchemy/repository.py +4 -5
- python_ddd_framework/identity/sqlalchemy/session_security.py +1 -3
- python_ddd_framework/identity/sqlalchemy/stores.py +35 -47
- python_ddd_framework/identity/stores.py +120 -0
- python_ddd_framework/identity/tokens.py +1 -1
- python_ddd_framework/invocation/callables.py +1 -1
- python_ddd_framework/invocation/dispatcher.py +3 -25
- python_ddd_framework/invocation/entrypoints.py +2 -1
- python_ddd_framework/invocation/function_runtime.py +2 -2
- python_ddd_framework/invocation/interception.py +1 -1
- python_ddd_framework/invocation/managed_proxy.py +2 -1
- python_ddd_framework/invocation/managed_services.py +8 -14
- python_ddd_framework/invocation/methods.py +2 -7
- python_ddd_framework/invocation/module.py +1 -1
- python_ddd_framework/invocation/runtime.py +483 -0
- python_ddd_framework/invocation/scopes.py +93 -0
- python_ddd_framework/messaging/channel.py +1 -1
- python_ddd_framework/messaging/runtime.py +1 -1
- python_ddd_framework/modularity/discovery.py +2 -7
- python_ddd_framework/modularity/graph.py +2 -8
- python_ddd_framework/observability/formatting.py +1 -0
- python_ddd_framework/observability/logging.py +1 -1
- python_ddd_framework/observability/tracing.py +1 -1
- python_ddd_framework/redis/cache.py +2 -0
- python_ddd_framework/redis/notification_runtime.py +1 -1
- python_ddd_framework/services/arbitration.py +1 -20
- python_ddd_framework/services/composition.py +71 -2
- python_ddd_framework/services/provider.py +23 -1
- python_ddd_framework/services/relocation.py +96 -0
- python_ddd_framework/settings/refresh.py +1 -1
- python_ddd_framework/settings/sqlalchemy/store.py +1 -1
- python_ddd_framework/sqlalchemy/metadata.py +11 -413
- python_ddd_framework/sqlalchemy/metadata_builder.py +234 -0
- python_ddd_framework/sqlalchemy/metadata_fingerprint.py +108 -0
- python_ddd_framework/sqlalchemy/metadata_ownership.py +73 -0
- python_ddd_framework/sqlalchemy/migration.py +2 -1
- python_ddd_framework/sqlalchemy/migration_sources.py +38 -0
- python_ddd_framework/sqlalchemy/module.py +2 -1
- python_ddd_framework/sqlalchemy/unit_of_work.py +1 -1
- python_ddd_framework/unit_of_work/contracts.py +68 -7
- python_ddd_framework/unit_of_work/manager.py +15 -9
- python_ddd_framework/unit_of_work/module.py +1 -1
- {python_ddd_framework-0.5.1.dist-info → python_ddd_framework-0.7.0.dist-info}/METADATA +101 -10
- {python_ddd_framework-0.5.1.dist-info → python_ddd_framework-0.7.0.dist-info}/RECORD +113 -141
- {python_ddd_framework-0.5.1.dist-info → python_ddd_framework-0.7.0.dist-info}/WHEEL +1 -1
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_jobs/order_approval/__init__.py.jinja +0 -1
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_jobs/order_approval/handler.py.jinja +0 -45
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_jobs/order_approval/payload.py.jinja +0 -8
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_jobs/order_statistics/__init__.py.jinja +0 -1
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_jobs/order_statistics/handler.py.jinja +0 -35
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_jobs/order_statistics/payload.py.jinja +0 -5
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_jobs/order_statistics/schedule.py.jinja +0 -15
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_workers/order_maintenance_worker.py.jinja +0 -28
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_workers/order_statistics_worker.py.jinja +0 -26
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/caching/order_cache.py.jinja +0 -9
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/caching/statistics_cache.py.jinja +0 -9
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/event_handlers/order_changed_handler.py.jinja +0 -23
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/hosted_services/order_integration_service.py.jinja +0 -71
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/hosted_services/order_observation_handler.py.jinja +0 -16
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/integration_services/order_reporting_service.py.jinja +0 -16
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/interceptors/order_timing_interceptor.py.jinja +0 -17
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/options/order_options.py.jinja +0 -7
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/services/order_approval_service.py.jinja +0 -65
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/services/order_management_service.py.jinja +0 -35
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/services/order_query_service.py.jinja +0 -41
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/setting_handlers/approval_setting_observer.py.jinja +0 -17
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/inputs/approve_order.py.jinja +0 -6
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/inputs/create_order.py.jinja +0 -7
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/integration_services/order_reporting_service.py.jinja +0 -7
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/services/order_approval_service.py.jinja +0 -13
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/services/order_management_service.py.jinja +0 -10
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/services/order_query_service.py.jinja +0 -12
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/views/order_statistics_snapshot.py.jinja +0 -9
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/views/order_view.py.jinja +0 -12
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/entities/order.py.jinja +0 -48
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/events/order_changed.py.jinja +0 -13
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/repositories/order_repository.py.jinja +0 -17
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/seeding/order_seed_contributor.py.jinja +0 -22
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/services/order_approval_service.py.jinja +0 -23
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/settings/approval_settings.py.jinja +0 -17
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/value_objects/order_title.py.jinja +0 -10
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/constants/order_constants.py.jinja +0 -3
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/enums/order_status.py.jinja +0 -6
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/errors/order_errors.py.jinja +0 -11
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/messages/order_messages.py.jinja +0 -26
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/messages/order_observation.py.jinja +0 -13
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/permissions/order_permission_provider.py.jinja +0 -19
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/permissions/order_permissions.py.jinja +0 -7
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/value_objects/money.py.jinja +0 -18
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/filters/export_filter.py.jinja +0 -15
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/models/refresh_orders.py.jinja +0 -5
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/routers/order_files.py.jinja +0 -76
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/websockets/order_socket.py.jinja +0 -44
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/models/order_model.py.jinja +0 -29
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/repositories/order_repository.py.jinja +0 -79
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/tests/test_domain.py.jinja +0 -19
- {python_ddd_framework-0.5.1.dist-info → python_ddd_framework-0.7.0.dist-info}/entry_points.txt +0 -0
- {python_ddd_framework-0.5.1.dist-info → python_ddd_framework-0.7.0.dist-info}/licenses/LICENSE +0 -0
- {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
|
@@ -1,98 +1,31 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""应用层拥有用例编排;按真实业务需要声明能力依赖,provider 由 Host 选择。"""
|
|
2
2
|
|
|
3
|
-
from dishka import Scope
|
|
4
|
-
from modules.{{ cookiecutter.module_name }}.application.background_workers.order_maintenance_worker import (
|
|
5
|
-
OrderMaintenanceWorker,
|
|
6
|
-
)
|
|
7
|
-
from modules.{{ cookiecutter.module_name }}.application.background_workers.order_statistics_worker import (
|
|
8
|
-
OrderStatisticsWorker,
|
|
9
|
-
)
|
|
10
|
-
from modules.{{ cookiecutter.module_name }}.application.interceptors.order_timing_interceptor import (
|
|
11
|
-
OrderTimingInterceptor,
|
|
12
|
-
)
|
|
13
|
-
from modules.{{ cookiecutter.module_name }}.application.options.order_options import {{ cookiecutter.class_prefix }}Options
|
|
14
|
-
from modules.{{ cookiecutter.module_name }}.application.setting_handlers.approval_setting_observer import (
|
|
15
|
-
ApprovalSettingObserver,
|
|
16
|
-
)
|
|
17
|
-
from modules.{{ cookiecutter.module_name }}.application_contracts.module import (
|
|
18
|
-
{{ cookiecutter.class_prefix }}ApplicationContractsModule,
|
|
19
|
-
)
|
|
20
|
-
from modules.{{ cookiecutter.module_name }}.application_contracts.services.order_approval_service import (
|
|
21
|
-
{{ cookiecutter.class_prefix }}ApprovalApplicationService,
|
|
22
|
-
)
|
|
23
|
-
from modules.{{ cookiecutter.module_name }}.application_contracts.services.order_management_service import (
|
|
24
|
-
{{ cookiecutter.class_prefix }}ManagementApplicationService,
|
|
25
|
-
)
|
|
26
|
-
from modules.{{ cookiecutter.module_name }}.application_contracts.services.order_query_service import (
|
|
27
|
-
{{ cookiecutter.class_prefix }}QueryApplicationService,
|
|
28
|
-
)
|
|
29
3
|
from modules.{{ cookiecutter.module_name }}.domain.module import {{ cookiecutter.class_prefix }}DomainModule
|
|
30
|
-
from modules.{{ cookiecutter.module_name }}.
|
|
31
|
-
from modules.{{ cookiecutter.module_name }}.domain_shared.constants.order_constants import MODULE_NAME
|
|
4
|
+
from modules.{{ cookiecutter.module_name }}.application_contracts.module import {{ cookiecutter.class_prefix }}ApplicationContractsModule
|
|
32
5
|
|
|
33
6
|
from python_ddd_framework import (
|
|
34
|
-
BackgroundExecutionModule,
|
|
35
|
-
BackgroundWorkers,
|
|
36
|
-
BackgroundSchedules,
|
|
37
|
-
HostedServices,
|
|
38
|
-
LocalEventsModule,
|
|
39
|
-
SettingHandlers,
|
|
40
|
-
add_interceptor,
|
|
41
7
|
AppModule,
|
|
42
8
|
ConfigureContext,
|
|
43
9
|
InitializeContext,
|
|
44
|
-
InterceptionStage,
|
|
45
10
|
PostConfigureContext,
|
|
46
11
|
PostInitializeContext,
|
|
47
12
|
PreConfigureContext,
|
|
48
13
|
PreInitializeContext,
|
|
49
14
|
ShutdownContext,
|
|
50
15
|
)
|
|
51
|
-
from python_ddd_framework.caching.module import CachingModule
|
|
52
|
-
from python_ddd_framework.settings import SettingHandlerDefinition
|
|
53
16
|
|
|
54
17
|
|
|
55
18
|
class {{ cookiecutter.class_prefix }}ApplicationModule(AppModule):
|
|
56
|
-
dependencies = (
|
|
57
|
-
{{ cookiecutter.class_prefix }}DomainModule,
|
|
58
|
-
{{ cookiecutter.class_prefix }}ApplicationContractsModule,
|
|
59
|
-
CachingModule,
|
|
60
|
-
BackgroundExecutionModule,
|
|
61
|
-
LocalEventsModule,
|
|
62
|
-
)
|
|
19
|
+
dependencies = ({{ cookiecutter.class_prefix }}DomainModule, {{ cookiecutter.class_prefix }}ApplicationContractsModule)
|
|
63
20
|
scan_packages = (__package__,)
|
|
64
|
-
background_workers = BackgroundWorkers((OrderStatisticsWorker, OrderMaintenanceWorker))
|
|
65
|
-
|
|
66
|
-
# 显式启用时导入 hosted_services.order_integration_service.OrderIntegrationService 并加入元组。
|
|
67
|
-
hosted_services = HostedServices(())
|
|
68
|
-
# 可选消息示例见项目 docs/development.md:选择 MessagingModule、声明 OrderObservation
|
|
69
|
-
# 通道并注册 hosted_services.order_observation_handler;不会自动替换线程 callback。
|
|
70
|
-
# 显式启用时导入 background_jobs.order_statistics.schedule.STATISTICS_SCHEDULE 并加入元组。
|
|
71
|
-
background_job_schedules = BackgroundSchedules(())
|
|
72
|
-
setting_handler_definitions = SettingHandlers((
|
|
73
|
-
SettingHandlerDefinition(APPROVAL_ENABLED, ApprovalSettingObserver),
|
|
74
|
-
))
|
|
75
21
|
|
|
76
22
|
def pre_configure(self, context: PreConfigureContext) -> None:
|
|
77
23
|
# 贡献影响后续注册的 PreOptions;此阶段不启动资源。
|
|
78
24
|
pass
|
|
79
25
|
|
|
80
26
|
def configure(self, context: ConfigureContext) -> None:
|
|
81
|
-
#
|
|
82
|
-
|
|
83
|
-
context.services.add_scoped(OrderTimingInterceptor, scope=Scope.REQUEST)
|
|
84
|
-
add_interceptor(context,
|
|
85
|
-
OrderTimingInterceptor,
|
|
86
|
-
match=lambda method: (
|
|
87
|
-
method.service
|
|
88
|
-
in (
|
|
89
|
-
{{ cookiecutter.class_prefix }}QueryApplicationService,
|
|
90
|
-
{{ cookiecutter.class_prefix }}ManagementApplicationService,
|
|
91
|
-
{{ cookiecutter.class_prefix }}ApprovalApplicationService,
|
|
92
|
-
)
|
|
93
|
-
),
|
|
94
|
-
before=InterceptionStage.METHOD,
|
|
95
|
-
)
|
|
27
|
+
# 注册服务和普通 Options;业务执行留在所属能力。
|
|
28
|
+
pass
|
|
96
29
|
|
|
97
30
|
def post_configure(self, context: PostConfigureContext) -> None:
|
|
98
31
|
# 读取已冻结 PreOptions,补充 Options 或覆盖已有服务注册。
|
|
@@ -4,8 +4,6 @@ from modules.{{ cookiecutter.module_name }}.domain_shared.module import {{ cooki
|
|
|
4
4
|
|
|
5
5
|
from python_ddd_framework import (
|
|
6
6
|
AppModule,
|
|
7
|
-
SettingsModule,
|
|
8
|
-
DataSeedingModule,
|
|
9
7
|
ConfigureContext,
|
|
10
8
|
InitializeContext,
|
|
11
9
|
PostConfigureContext,
|
|
@@ -17,7 +15,7 @@ from python_ddd_framework import (
|
|
|
17
15
|
|
|
18
16
|
|
|
19
17
|
class {{ cookiecutter.class_prefix }}DomainModule(AppModule):
|
|
20
|
-
dependencies = ({{ cookiecutter.class_prefix }}DomainSharedModule,
|
|
18
|
+
dependencies = ({{ cookiecutter.class_prefix }}DomainSharedModule,)
|
|
21
19
|
scan_packages = (__package__,)
|
|
22
20
|
|
|
23
21
|
def pre_configure(self, context: PreConfigureContext) -> None:
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""共享层拥有跨层契约值;按实际声明增加能力依赖,不预设业务规则。"""
|
|
2
2
|
|
|
3
3
|
from python_ddd_framework import (
|
|
4
4
|
AppModule,
|
|
5
|
-
AuthorizationModule,
|
|
6
5
|
ConfigureContext,
|
|
7
6
|
InitializeContext,
|
|
8
7
|
PostConfigureContext,
|
|
@@ -14,7 +13,7 @@ from python_ddd_framework import (
|
|
|
14
13
|
|
|
15
14
|
|
|
16
15
|
class {{ cookiecutter.class_prefix }}DomainSharedModule(AppModule):
|
|
17
|
-
dependencies = (
|
|
16
|
+
dependencies = ()
|
|
18
17
|
scan_packages = (__package__,)
|
|
19
18
|
|
|
20
19
|
def pre_configure(self, context: PreConfigureContext) -> None:
|
|
@@ -1,21 +1,6 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""HTTP 仅暴露本模块公开契约;空契约不产生业务端点。"""
|
|
2
2
|
|
|
3
|
-
from
|
|
4
|
-
from modules.{{ cookiecutter.module_name }}.application_contracts.module import (
|
|
5
|
-
{{ cookiecutter.class_prefix }}ApplicationContractsModule,
|
|
6
|
-
)
|
|
7
|
-
from modules.{{ cookiecutter.module_name }}.application_contracts.services.order_approval_service import (
|
|
8
|
-
{{ cookiecutter.class_prefix }}ApprovalApplicationService,
|
|
9
|
-
)
|
|
10
|
-
from modules.{{ cookiecutter.module_name }}.application_contracts.services.order_management_service import (
|
|
11
|
-
{{ cookiecutter.class_prefix }}ManagementApplicationService,
|
|
12
|
-
)
|
|
13
|
-
from modules.{{ cookiecutter.module_name }}.application_contracts.services.order_query_service import (
|
|
14
|
-
{{ cookiecutter.class_prefix }}QueryApplicationService,
|
|
15
|
-
)
|
|
16
|
-
from modules.{{ cookiecutter.module_name }}.http_api.filters.export_filter import ExportFilter
|
|
17
|
-
from modules.{{ cookiecutter.module_name }}.http_api.routers.order_files import router as files_router
|
|
18
|
-
from modules.{{ cookiecutter.module_name }}.http_api.websockets.order_socket import router as realtime_router
|
|
3
|
+
from modules.{{ cookiecutter.module_name }}.application_contracts.module import {{ cookiecutter.class_prefix }}ApplicationContractsModule
|
|
19
4
|
|
|
20
5
|
from python_ddd_framework import (
|
|
21
6
|
AppModule,
|
|
@@ -27,60 +12,21 @@ from python_ddd_framework import (
|
|
|
27
12
|
PreInitializeContext,
|
|
28
13
|
ShutdownContext,
|
|
29
14
|
)
|
|
30
|
-
from python_ddd_framework.fastapi import (
|
|
31
|
-
ApplicationServiceRouteOverride,
|
|
32
|
-
FastApiRealtimeModule,
|
|
33
|
-
contributes_routers,
|
|
34
|
-
exposes_application_services,
|
|
35
|
-
)
|
|
36
15
|
|
|
16
|
+
from python_ddd_framework.fastapi import FastApiModule, exposes_application_services
|
|
37
17
|
|
|
38
|
-
|
|
39
|
-
@exposes_application_services(
|
|
40
|
-
{{ cookiecutter.class_prefix }}ApplicationContractsModule,
|
|
41
|
-
overrides=(
|
|
42
|
-
ApplicationServiceRouteOverride(
|
|
43
|
-
service={{ cookiecutter.class_prefix }}QueryApplicationService,
|
|
44
|
-
method={{ cookiecutter.class_prefix }}QueryApplicationService.get,
|
|
45
|
-
path="{{ cookiecutter.http_service_path }}/{id}",
|
|
46
|
-
operation_id="{{ cookiecutter.http_operation_prefix }}get",
|
|
47
|
-
),
|
|
48
|
-
ApplicationServiceRouteOverride(
|
|
49
|
-
service={{ cookiecutter.class_prefix }}QueryApplicationService,
|
|
50
|
-
method={{ cookiecutter.class_prefix }}QueryApplicationService.get_list,
|
|
51
|
-
path="{{ cookiecutter.http_service_path }}",
|
|
52
|
-
operation_id="{{ cookiecutter.http_operation_prefix }}get_list",
|
|
53
|
-
),
|
|
54
|
-
ApplicationServiceRouteOverride(
|
|
55
|
-
service={{ cookiecutter.class_prefix }}ManagementApplicationService,
|
|
56
|
-
method={{ cookiecutter.class_prefix }}ManagementApplicationService.create,
|
|
57
|
-
path="{{ cookiecutter.http_service_path }}",
|
|
58
|
-
operation_id="{{ cookiecutter.http_operation_prefix }}create",
|
|
59
|
-
),
|
|
60
|
-
ApplicationServiceRouteOverride(
|
|
61
|
-
service={{ cookiecutter.class_prefix }}ApprovalApplicationService,
|
|
62
|
-
method={{ cookiecutter.class_prefix }}ApprovalApplicationService.approve,
|
|
63
|
-
path="{{ cookiecutter.http_service_path }}/{id}/approve",
|
|
64
|
-
operation_id="{{ cookiecutter.http_operation_prefix }}approve",
|
|
65
|
-
),
|
|
66
|
-
ApplicationServiceRouteOverride(
|
|
67
|
-
service={{ cookiecutter.class_prefix }}ApprovalApplicationService,
|
|
68
|
-
method={{ cookiecutter.class_prefix }}ApprovalApplicationService.queue_approval,
|
|
69
|
-
path="{{ cookiecutter.http_service_path }}/{id}/queue-approval",
|
|
70
|
-
operation_id="{{ cookiecutter.http_operation_prefix }}queue_approval",
|
|
71
|
-
),
|
|
72
|
-
),
|
|
73
|
-
)
|
|
18
|
+
|
|
19
|
+
@exposes_application_services({{ cookiecutter.class_prefix }}ApplicationContractsModule)
|
|
74
20
|
class {{ cookiecutter.class_prefix }}HttpApiModule(AppModule):
|
|
75
|
-
dependencies = (
|
|
21
|
+
dependencies = (FastApiModule,)
|
|
76
22
|
|
|
77
23
|
def pre_configure(self, context: PreConfigureContext) -> None:
|
|
78
24
|
# 贡献影响后续注册的 PreOptions;此阶段不启动资源。
|
|
79
25
|
pass
|
|
80
26
|
|
|
81
27
|
def configure(self, context: ConfigureContext) -> None:
|
|
82
|
-
#
|
|
83
|
-
|
|
28
|
+
# 注册服务和普通 Options;业务执行留在所属能力。
|
|
29
|
+
pass
|
|
84
30
|
|
|
85
31
|
def post_configure(self, context: PostConfigureContext) -> None:
|
|
86
32
|
# 读取已冻结 PreOptions,补充 Options 或覆盖已有服务注册。
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
"""provider 依赖领域契约;Host 显式选择此实现。"""
|
|
2
2
|
|
|
3
3
|
from modules.{{ cookiecutter.module_name }}.domain.module import {{ cookiecutter.class_prefix }}DomainModule
|
|
4
|
-
from modules.{{ cookiecutter.module_name }}.domain_shared.constants.order_constants import MODULE_NAME
|
|
5
4
|
from modules.{{ cookiecutter.module_name }}.sqlalchemy.models.base import {{ cookiecutter.class_prefix }}Base
|
|
6
5
|
|
|
7
6
|
from python_ddd_framework import (
|
|
@@ -26,7 +25,7 @@ from python_ddd_framework.sqlalchemy import (
|
|
|
26
25
|
models_package=__package__ + ".models",
|
|
27
26
|
metadata={{ cookiecutter.class_prefix }}Base.metadata,
|
|
28
27
|
migrations_package=__package__ + ".migrations",
|
|
29
|
-
branch_label=
|
|
28
|
+
branch_label="{{ cookiecutter.module_name }}",
|
|
30
29
|
)
|
|
31
30
|
)
|
|
32
31
|
class {{ cookiecutter.class_prefix }}SqlAlchemyModule(AppModule):
|
python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/AGENTS.md
CHANGED
|
@@ -5,22 +5,37 @@ These instructions apply to this application and its business modules. Follow th
|
|
|
5
5
|
## Start with the affected capability
|
|
6
6
|
|
|
7
7
|
1. Check the branch, worktree, requested outcome, and existing changes before editing. Analysis and review requests are read-only unless implementation is requested.
|
|
8
|
-
2.
|
|
9
|
-
3.
|
|
10
|
-
4. Read versions and dependencies from `backend/pyproject.toml
|
|
8
|
+
2. Read the applicable project requirements and accepted decisions, then [architecture](docs/architecture.md) for ownership and dependencies and the relevant section of [development](docs/development.md) for framework usage. Use [README](README.md) for setup and operations. This reading order is required before implementation; reuse existing conclusions for a small change.
|
|
9
|
+
3. Before changing a business module, read its `backend/src/modules/<name>/README.md`, then inspect the affected source and direct callers. Use the workflow below to choose the layer and responsibility directory. Expand the investigation only when those facts reveal another affected boundary.
|
|
10
|
+
4. Read versions and dependencies from `backend/pyproject.toml` and exact resolutions from `backend/uv.lock`. Follow [installed framework lookup](docs/development.md#match-the-installed-framework) for the actual package, bundled examples, CLI, and any user-provided source reference. Check the consumed artifact before relying on a checkout; equal version numbers do not prove identical contents. Generated guidance records the template version; upgrades do not update it automatically.
|
|
11
11
|
|
|
12
12
|
Python code, metadata, configuration, environment, and deployment files belong under `backend/`. Root `scripts/` contains consumer-owned scripts. Run installed `pddd` commands from any directory in the application; run native uv/build/test and Docker commands from `backend/`.
|
|
13
13
|
|
|
14
|
+
Use the [official CLI entry points](docs/development.md#cli-operations) for operations it provides, including project/module generation, development startup, infrastructure preparation, migrations, seeding, and inspection. Do not replace them with handwritten generation or direct calls to migration/seed internals. Build and test still use native tools.
|
|
15
|
+
|
|
16
|
+
## Work from business rules to framework code
|
|
17
|
+
|
|
18
|
+
1. Describe the requested use case in business terms: caller, command or query, expected outcome, allowed state changes, and failure conditions. Reuse the module's recorded language and rules. The DDD skeleton supplies structure without business rules; documentation recipes are illustrations, and actual requirements come from the application.
|
|
19
|
+
2. For a domain change, identify the rule owner, aggregate boundary, consistency needs, and collaboration with other modules using [business-change planning](docs/development.md#plan-a-business-change). Keep confirmed decisions in the owning module README. Ask about missing rules that affect correctness; label proposals and open questions instead of presenting assumptions as approved rules.
|
|
20
|
+
3. Before the first edit, identify **Owner -> layer -> target directory -> verified framework API**, with the relevant requirement and source/example evidence, using the [design and delivery checks](docs/development.md#design-and-delivery-checks). Choose the existing `basic` or `ddd` layout using the [directory rules](docs/development.md#module-layout-and-coding-rules), then map needed capabilities through the [framework selection table](docs/development.md#choose-a-framework-capability). Check the installed API, Module declarations, and existing usage before implementing. Mark an API check as not applicable when appropriate; do not invent an abstraction to fill the record.
|
|
21
|
+
4. Implement the authorized use case through its public contract, domain behavior, and existing persistence/invocation boundaries. Reuse existing validation and [verification commands](docs/development.md#verification); state which domain rules, composition paths, and infrastructure behavior the checks actually cover. Scale this process to the change: a local correction can reuse the existing model directly.
|
|
22
|
+
|
|
14
23
|
## Preserve ownership and contracts
|
|
15
24
|
|
|
16
25
|
- The Host composes modules, selects providers, and supplies configuration. Keep business state and rules in their owning modules.
|
|
17
26
|
- Domain and application contracts stay independent of Host, HTTP, and persistence implementations. Cross-module calls use explicitly declared dependencies and public contracts; do not access another module's implementation, ORM models, or tables directly.
|
|
18
27
|
- Use framework application services, repositories, Options, permissions, units of work, events, jobs, and lifecycle hooks before adding custom infrastructure. Do not introduce a parallel DI container, service locator, transaction manager, or wrapper without a missing business capability.
|
|
19
28
|
- Keep public contracts, mutable state, orchestration, and external I/O under clear owners. Split for independent responsibilities, not a fixed line count. Avoid generic `common`, `utils`, or `helpers` collections and speculative abstractions.
|
|
20
|
-
-
|
|
29
|
+
- If requirements, accepted decisions, documented responsibilities/contracts, an approved plan, or implementation conflict, pause affected edits and commits. Present both statements with file locations, current behavior, impact, and the decision needed from the user. Obtain an explicit ruling before proceeding; silence or a general instruction to continue does not resolve an unstated boundary change. Never rewrite documentation to retroactively authorize an incorrect implementation.
|
|
21
30
|
- Confirm changes to dependencies, public contracts, persistent data, external protocols, or deployment infrastructure when they extend beyond the requested task. Internal simplification does not authorize breaking consumers or stored data.
|
|
22
31
|
|
|
23
|
-
Use the documented responsibility directories for every new capability. Propose a standards change before adding a responsibility category or changing dependency direction. The `ddd` template intentionally
|
|
32
|
+
Use the documented responsibility directories for every new capability. Propose a standards change before adding a responsibility category or changing dependency direction. The `ddd` template intentionally retains the full responsibility directories, package markers, Module hooks, ORM Base, and model/migration registration. It generates no business implementations or test cases; this directory exception applies to DDD templates and generated projects, not speculative framework runtime layers. The `basic` template generates only package markers, `module.py`, `contracts/conversion_service.py`, `services/default_conversion_service.py`, and its README. It uses ordinary transient DI, without Domain, persistence, HTTP, interception, or empty layers. Keep `module.py` limited to dependencies, registration, and lifecycle hooks, and keep package `__init__.py` free of business code and compatibility exports.
|
|
33
|
+
|
|
34
|
+
## Enforce DDD rules
|
|
35
|
+
|
|
36
|
+
For a modeled domain capability, apply the [mandatory DDD rules](docs/architecture.md#mandatory-ddd-rules) during implementation and review. Reject application code that assigns aggregate business state directly, writes aggregate children outside the root's business operations, or leaks ORM/Session types through domain repository contracts. Before a cross-aggregate write, record its consistency, failure, and idempotency decisions. Keep each changed invariant's owner, operation, and rejection example in the module README and verify them with the [business-change checks](docs/development.md#check-a-domain-change). These rules do not require a `basic` service to acquire unused DDD layers.
|
|
37
|
+
|
|
38
|
+
Code and architecture changes must also satisfy the [seven design principles](docs/architecture.md#mandatory-design-principles). Review the applicable principles, DDD boundaries, imports, contracts, state ownership, and resource lifetimes on the final diff. Follow the [delivery checks](docs/development.md#design-and-delivery-checks) and cite actual evidence. Correct confirmed violations within the authorized scope or report the blocking decision; passing builds/tests alone cannot justify completion. These requirements do not authorize new business rules, dependencies, tests, automated gates, infrastructure, or framework changes.
|
|
24
39
|
|
|
25
40
|
## Use the framework safely
|
|
26
41
|
|
|
@@ -28,8 +43,8 @@ Use the documented responsibility directories for every new capability. Propose
|
|
|
28
43
|
|
|
29
44
|
- Invoke application services through their injected public contracts or the framework functions `invoke(application, ...)` / `call(application, ...)`. Manual construction and self-calls do not create a new managed invocation.
|
|
30
45
|
- Ordinary DI services opt into interception with `@unit_of_work`, `@authorize`, or `ValidationEnabled`; declare the corresponding capability dependency on their owning Module. Keep marked methods async instance methods and preserve native scope/cache. These declarations do not add HTTP exposure or require ApplicationService inheritance.
|
|
31
|
-
- Keep transactions short. Never carry a Session
|
|
32
|
-
- Keep aggregate rules and optimistic concurrency checks intact. Use repository operations to stage changes and collect events; do not publish the same aggregate events twice.
|
|
46
|
+
- Keep transactions short. Never carry a business Session or UoW across a network wait or stream `yield`, or transfer execution scopes into ordinary child tasks or external threads. Managed HTTP streaming retains only its transport-owned REQUEST until cleanup; follow the [streaming boundaries](docs/architecture.md#http-streaming-and-continuous-access) and [complete example](docs/development.md#http-generators-and-continuous-access).
|
|
47
|
+
- Keep aggregate rules and optimistic concurrency checks intact. Construct aggregate events from frozen records and stable nested values as described in [architecture](docs/architecture.md#calls-scopes-and-transactions). Use repository operations to stage changes and collect events; do not publish the same aggregate events twice.
|
|
33
48
|
- Review generated migrations. Do not edit applied revisions, bypass migration guards, or treat Host startup as migration or seeding.
|
|
34
49
|
- Keep background side effects idempotent. Local events and WebSocket messages do not provide durable distributed delivery; distributed locks do not replace database concurrency or idempotency.
|
|
35
50
|
- Validate external inputs, propagate meaningful failures, and preserve cancellation and cleanup. Add defaults, retries, or fallbacks only for an identified requirement.
|
|
@@ -51,7 +66,7 @@ and message examples in the project development guide.
|
|
|
51
66
|
## Verify and maintain
|
|
52
67
|
|
|
53
68
|
- Preserve unrelated changes. Do not edit the installed framework, generated dependency files by hand, or another repository to make an application change pass.
|
|
54
|
-
- Use the smallest relevant existing checks
|
|
69
|
+
- Use the smallest relevant existing checks and authorized build. New test files, cases, or test dependencies require explicit approval; explain their scope and verification benefit first. Read fixtures before running infrastructure tests and keep them within the task's authorization. See [verification](docs/development.md#verification) for available commands.
|
|
55
70
|
- Distinguish source checks, unit tests, real infrastructure tests, and distribution validation. Report actual results and anything not verified; never claim success from a mock or an unexecuted command.
|
|
56
71
|
- Update the owning module README when business rules or public behavior change. Update architecture for dependency or ownership changes, development for usage changes, and README for setup or operations changes. Replace outdated statements rather than appending a work log.
|
|
57
72
|
- Documentation guides development; it is not an executable architecture gate. Use type checks and appropriate boundary tests when the project has them.
|
python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/README.md
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
# {{ cookiecutter.project_name }}
|
|
2
2
|
|
|
3
|
-
A modular Python application generated with Python DDD Framework {{ cookiecutter.framework_version }}. The initial project contains a Host; add business modules with `pddd add module` and
|
|
3
|
+
A modular Python application generated with Python DDD Framework {{ cookiecutter.framework_version }}. The initial project contains a Host; add business modules with `pddd add module` and implement their business behavior from confirmed requirements.
|
|
4
4
|
|
|
5
5
|
| Document | Purpose |
|
|
6
6
|
| --- | --- |
|
|
7
|
-
| [AGENTS.md](AGENTS.md) |
|
|
8
|
-
| [Architecture](docs/architecture.md) |
|
|
9
|
-
| [Development guide](docs/development.md) |
|
|
7
|
+
| [AGENTS.md](AGENTS.md) | Mandatory reading, implementation, and delivery checks for Codex and other contributors. |
|
|
8
|
+
| [Architecture](docs/architecture.md) | Design principles, DDD ownership, dependency direction, transactions, streaming, and lifecycle boundaries. |
|
|
9
|
+
| [Development guide](docs/development.md) | CLI entry points, artifact/source lookup, design checks, and framework usage recipes. |
|
|
10
10
|
|
|
11
11
|
Each generated business module also has its own README under `backend/src/modules/<name>/`. This README owns application setup and operations.
|
|
12
12
|
|
|
@@ -26,14 +26,14 @@ pddd dev-init
|
|
|
26
26
|
|
|
27
27
|
This starts or reuses PostgreSQL and Redis from the Host's development configuration and preserves data. It does not start the application, migrate, seed, or create an `.env` file. The bundled Compose setup requires local URLs with explicit ports; its Redis service does not configure authentication or TLS.
|
|
28
28
|
|
|
29
|
-
To add
|
|
29
|
+
To add a DDD skeleton or a minimal ordinary service:
|
|
30
30
|
|
|
31
31
|
```sh
|
|
32
32
|
pddd add module orders --template ddd
|
|
33
33
|
pddd add module conversions --template basic
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
-
Read `backend/src/modules/orders/README.md` before
|
|
36
|
+
The `orders` name here selects a package, not an order implementation. Read `backend/src/modules/orders/README.md` before adding business code; all responsibility directories are present, but no business service, table, route, or test is generated. The project is also usable as a pure Host without this step.
|
|
37
37
|
|
|
38
38
|
## Initialize the database
|
|
39
39
|
|
|
@@ -47,13 +47,13 @@ pddd db upgrade --module background_jobs
|
|
|
47
47
|
pddd db seed --module identity
|
|
48
48
|
```
|
|
49
49
|
|
|
50
|
-
|
|
50
|
+
After implementing actual models in a DDD module named `orders`, generate its first revision. An empty skeleton retains model and migration registration but has no business tables or revisions. Migration commands still use the configured database:
|
|
51
51
|
|
|
52
52
|
```sh
|
|
53
53
|
pddd db revision --module orders
|
|
54
54
|
```
|
|
55
55
|
|
|
56
|
-
Review the generated file in `backend/src/modules/orders/sqlalchemy/migrations/`, then apply
|
|
56
|
+
Review the generated file in `backend/src/modules/orders/sqlalchemy/migrations/`, then apply it; run seed only when that module declares the intended contributors:
|
|
57
57
|
|
|
58
58
|
```sh
|
|
59
59
|
pddd db upgrade --module orders
|
|
@@ -75,7 +75,7 @@ Open **http://127.0.0.1:8000/docs**. Call `/api/auth/login` using the generated
|
|
|
75
75
|
|
|
76
76
|
Identity includes paged user/role management, password operations, and version-protected assignments. Before using it, apply the framework's Identity migrations; preserve the returned `concurrency_version` in updates. See [Identity management](docs/development.md#identity-management) for API usage, session invalidation, and Host authorization options.
|
|
77
77
|
|
|
78
|
-
|
|
78
|
+
Adding a DDD skeleton creates no business HTTP endpoints. Implement the public contracts and use cases first, then inspect OpenAPI and update the module README with their actual behavior.
|
|
79
79
|
|
|
80
80
|
`http.api_prefix` controls business HTTP addresses and defaults to `/api`; `/ws`, `/health/live`, `/health/ready`, and `/docs` retain independent paths. Readiness reports Application lifecycle state, not continuous infrastructure health.
|
|
81
81
|
|
|
@@ -95,7 +95,7 @@ uv run pytest
|
|
|
95
95
|
uv build --no-sources
|
|
96
96
|
```
|
|
97
97
|
|
|
98
|
-
The application tests include real Host infrastructure and require Docker.
|
|
98
|
+
The application tests include real Host infrastructure and require Docker. A DDD skeleton has no test cases; after authorized domain tests exist, run them separately; see [verification](docs/development.md#verification). Module tests are excluded from production wheels and service discovery.
|
|
99
99
|
|
|
100
100
|
## Deploy
|
|
101
101
|
|
|
@@ -135,3 +135,15 @@ uv build --no-sources
|
|
|
135
135
|
```
|
|
136
136
|
|
|
137
137
|
A framework upgrade does not regenerate or overwrite application source, migrations, or these documents. A global CLI upgrade also does not update this project. Apply required adaptations and update the relevant application and module documentation deliberately.
|
|
138
|
+
|
|
139
|
+
<a id="streaming-and-access-migration-07-unreleased"></a>
|
|
140
|
+
|
|
141
|
+
### Streaming and access migration (0.7)
|
|
142
|
+
|
|
143
|
+
These notes describe the current 0.7.0 implementation; they are not a release announcement. Check the consumed artifact using [installed framework lookup](docs/development.md#match-the-installed-framework) before using these APIs.
|
|
144
|
+
|
|
145
|
+
- Move `fastapi_realtime.authentication_refresh_seconds` to `fastapi_access.authentication_refresh_seconds` in existing environment files and deployment overrides. Rename `PDDD_FASTAPI_REALTIME__AUTHENTICATION_REFRESH_SECONDS` to `PDDD_FASTAPI_ACCESS__AUTHENTICATION_REFRESH_SECONDS` when set. The old realtime field is rejected. `FastApiAccessOptions` belongs to `FastApiModule` and covers managed HTTP streams and WebSocket connections; its default is 60 seconds and minimum is 1. An omitted section uses these defaults; generation does not need an extra configuration block.
|
|
146
|
+
- Existing HTTP streaming code must follow the [preparation, transmission, and cleanup boundaries](docs/architecture.md#http-streaming-and-continuous-access). The [complete generator example](docs/development.md#http-generators-and-continuous-access) demonstrates native SSE, JSONL, and byte streaming. Keep ApplicationService return contracts unchanged and move checks that need ordinary error responses into preparation.
|
|
147
|
+
- `run_host(..., timeout_graceful_shutdown=10)` forwards a non-negative integer number of seconds to Uvicorn. At the deadline Uvicorn requests cancellation; framework drain still waits for streams, subscriptions, and REQUEST resources to actually release. This is not a hard process-exit deadline. A synchronous generator's running `next()` must return; its thread is not abandoned. Applications running Uvicorn directly must configure its native timeout themselves.
|
|
148
|
+
|
|
149
|
+
Review applicable [design principles](docs/architecture.md#mandatory-design-principles) and [delivery checks](docs/development.md#design-and-delivery-checks) when adapting existing consumers. Template examples do not authorize application business, schema, dependency, or infrastructure changes.
|
|
@@ -31,10 +31,13 @@ def create_application(
|
|
|
31
31
|
def create_web() -> FastAPI:
|
|
32
32
|
# HTTP 组装扩展:在这里设置站点信息和 Host middleware,并在 install 前完成需要的配置。
|
|
33
33
|
application = create_application()
|
|
34
|
+
api_prefix = application.configuration.get_path("http.api_prefix")
|
|
35
|
+
if not isinstance(api_prefix, str):
|
|
36
|
+
raise TypeError("http.api_prefix must be a string")
|
|
34
37
|
# 全局 middleware 使用原生 Starlette 实现,在 adapter.install 前完成配置。
|
|
35
38
|
adapter = FastApiAdapter(
|
|
36
39
|
application, middleware=(Middleware(GZipMiddleware),),
|
|
37
|
-
api_prefix=
|
|
40
|
+
api_prefix=api_prefix,
|
|
38
41
|
)
|
|
39
42
|
web = FastAPI(
|
|
40
43
|
title="{{ cookiecutter.project_name }}", version=version("{{ cookiecutter.project_name }}"), lifespan=adapter.lifespan
|
|
@@ -6,7 +6,7 @@ This application was generated with Python DDD Framework {{ cookiecutter.framewo
|
|
|
6
6
|
|
|
7
7
|
## Composition and ownership
|
|
8
8
|
|
|
9
|
-
The initial project is a Host without business modules. `pddd add module <name>` adds
|
|
9
|
+
The initial project is a Host without business modules. `pddd add module <name>` adds a DDD skeleton with the full responsibility directories and necessary composition. Business rules, services, tables, endpoints, and tests are added only from confirmed requirements; no order example is generated.
|
|
10
10
|
|
|
11
11
|
| Owner | Responsibility |
|
|
12
12
|
| --- | --- |
|
|
@@ -51,9 +51,37 @@ Immutable values shared by domain objects and public DTOs belong in `domain_shar
|
|
|
51
51
|
|
|
52
52
|
Cross-module access uses public service contracts and explicit Module dependencies. A module owns its own data and migrations. Shared database deployment does not authorize another module to query its tables. Keep internal collaboration contracts separate from end-user service contracts where the caller and authorization semantics differ; `IntegrationService` requires its own contract and is not exposed over HTTP.
|
|
53
53
|
|
|
54
|
-
The [development guide](development.md#module-layout-and-coding-rules) owns the complete directory and naming rules.
|
|
54
|
+
The [development guide](development.md#module-layout-and-coding-rules) owns the complete directory and naming rules. When those use cases exist, separate query, management, approval, and item contracts by caller needs. Internal reporting uses a separate integration contract and implementation when its caller and authorization boundary differ. HTTP keeps its existing paths and operation identities through public route overrides when Python services move.
|
|
55
55
|
|
|
56
|
-
Business Modules explicitly depend on the framework capabilities they use. Domain seed contributors depend on `DataSeedingModule`, permission definitions on `AuthorizationModule`, application services on `ApplicationServicesModule`, and HTTP declarations on `FastApiModule`. Host provider selection does not repair a missing business-module dependency. Capability definitions use typed module declarations; their catalogs and runtime state belong to each Application. Every Host and six-layer Module explicitly declares the seven framework hooks. Synchronous configuration phases only compose the application; asynchronous initialization phases follow dependency-first barriers, and shutdown closes resources in reverse dependency order. Empty hooks use `pass`.
|
|
56
|
+
Business Modules explicitly depend on the framework capabilities they use. Domain seed contributors depend on `DataSeedingModule`, permission definitions on `AuthorizationModule`, application services on `ApplicationServicesModule`, and HTTP declarations on `FastApiModule`. Host provider selection does not repair a missing business-module dependency. Capability definitions use typed module declarations; their catalogs and runtime state belong to each Application. Every Host and six-layer Module explicitly declares the seven framework hooks. Synchronous configuration phases only compose the application; asynchronous initialization phases follow dependency-first barriers, and shutdown closes resources in reverse dependency order. Empty hooks use `pass`. The skeleton has no workers, hosted services, or schedules to enable.
|
|
57
|
+
|
|
58
|
+
## Mandatory design principles
|
|
59
|
+
|
|
60
|
+
Apply all seven principles that are relevant to a code or architecture change during implementation and review, together with the DDD rules below. An abstraction must serve a real responsibility, public contract, variation point, or reuse need. Counts of files, classes, or interfaces are not evidence of good design.
|
|
61
|
+
|
|
62
|
+
| Principle | Required implementation and review behavior |
|
|
63
|
+
| --- | --- |
|
|
64
|
+
| Single responsibility | Organize by stable capability, owner, and reason to change. Give public contracts/values, mutable state, orchestration, external I/O, and resource lifecycles explicit owners. Split independent responsibilities; related immutable values, enums, and exceptions may remain together. Do not centralize unrelated state or lifetimes in a generic service. |
|
|
65
|
+
| Open/closed | Use existing Module declarations, DI, and native extension points for implementations that satisfy the current contract. Avoid accumulating provider branches in core use cases. A business-semantic change still needs contract review and authorization; do not anticipate hypothetical changes with plugin layers or compatibility paths. |
|
|
66
|
+
| Liskov substitution | Implementations of a contract must preserve its preconditions, postconditions, and invariants, including errors, cancellation, idempotency, and cleanup. Do not require stronger inputs, weaken outcomes, or return success for unsupported behavior. Review replacements against the caller's contract, not just their signatures. |
|
|
67
|
+
| Interface segregation | Separate public contracts by caller use case, permissions, and lifecycle needs. A caller should depend only on the capability it needs. Avoid an all-purpose service interface and mechanical one-method interfaces. |
|
|
68
|
+
| Dependency inversion | Domain and use cases depend on contracts owned by the relevant capability; persistence and external integrations implement those boundaries. The Host selects providers and composes them. Domain/Contracts must not depend on Host, HTTP, or concrete persistence. Reuse framework abstractions directly. |
|
|
69
|
+
| Law of Demeter | Obtain facts and request operations through direct collaborators' public contracts. Do not reach through another module into its implementation, ORM, tables, or mutable internals. Aggregate changes pass through the root. Do not add a forwarding facade merely to shorten a call chain. |
|
|
70
|
+
| Composition reuse | Prefer injected collaborators, composition, and existing framework capabilities. Use inheritance for a real type relationship or native extension point. Avoid base classes combining permissions, transactions, domain state, and external communication. |
|
|
71
|
+
|
|
72
|
+
State belongs to its domain or runtime owner; orchestration coordinates those owners without absorbing their rules. A lifecycle owner must await actual release of its resources. The Host remains a composition root, and stable contracts never depend back on orchestration or infrastructure. Use a class for identity, state, lifecycle, ownership, or a real variation point; keep a stateless internal algorithm private to its capability. These constraints do not require extra layers, wrappers, or unused interfaces. Record and review changes using the [design and delivery checks](development.md#design-and-delivery-checks).
|
|
73
|
+
|
|
74
|
+
## Mandatory DDD rules
|
|
75
|
+
|
|
76
|
+
These constraints apply when a capability models aggregates. The module README owns its actual business language and invariants; documentation examples do not define its requirements.
|
|
77
|
+
|
|
78
|
+
| Boundary | Required behavior and review criterion |
|
|
79
|
+
| --- | --- |
|
|
80
|
+
| Application and aggregate | Application services authorize, load, coordinate, and save through existing contracts. State transitions and entity/value invariants must run through domain operations. Do not assign aggregate business fields or reproduce their transition rules in Application. Caller permissions and expected-version checks remain Application responsibilities. |
|
|
81
|
+
| Aggregate root and children | Change an aggregate's internal entities and collections through the root's business operations so it can enforce the aggregate's invariants. Do not return mutable internals that let callers bypass those operations. Persistence may reconstitute stored state through the domain construction boundary; reconstitution must not manufacture new creation events or become a business-write shortcut. |
|
|
82
|
+
| Domain repository and persistence | Design domain repository operations around the aggregate's identity and lifecycle, not one repository per table. Domain contracts use domain values and aggregates; keep ORM rows, Session, SQL expressions, and driver-specific values in persistence. Map database scalar subclasses to the native domain value at that boundary. Keep query projections in their declared read responsibility rather than weakening the aggregate contract. |
|
|
83
|
+
| Cross-aggregate consistency | Before implementing a write spanning aggregates, identify the participating owners, what commits atomically, and any later effects. Record concurrency, failure-after-commit, duplicate/retry, and idempotency behavior. A local UoW may coordinate multiple aggregates when that is the confirmed requirement; do not impose one aggregate per transaction mechanically. Cross-module collaboration still uses public contracts, and local events do not promise durable distributed delivery. |
|
|
84
|
+
| Business evidence | For each changed invariant, the module README must identify its rule owner, business operation, and a concrete rejection example, linking the relevant source and verification. Record unverified cases and open decisions explicitly. Directory placement, type checks, and passing unrelated tests do not by themselves prove a business rule. |
|
|
57
85
|
|
|
58
86
|
## Calls, scopes, and transactions
|
|
59
87
|
|
|
@@ -67,8 +95,30 @@ Repositories map aggregates and ORM models explicitly. Their `SqlAlchemySessionP
|
|
|
67
95
|
|
|
68
96
|
Local transactional events run in this order: DOMAIN handlers, flush/version checks, database commit, then AFTER_COMMIT handlers. Outside a UoW, `await events.publish(message)` dispatches immediately and does not claim a transaction was committed. Nontransactional UoWs reject explicit and automatic events before repository writes. A post-commit failure does not roll back committed data. Local events provide neither distributed delivery nor crash recovery.
|
|
69
97
|
|
|
98
|
+
`AggregateRoot.raise_local_event` validates actual field values before enqueueing. The root and nested records must be frozen dataclasses or frozen Pydantic models. Fields may contain native scalar values (`None`, bool, int, float, str, bytes, Decimal, UUID, date, datetime, time, timedelta), stable Enum values, tuples, frozensets, and NamedTuples without extra state. Datetime/time zones must be native timezone/ZoneInfo instances or None. Mutable containers and their subclasses, mapping views, entity references, unsupported custom objects, scalar subclasses, cycles, missing fields, and undeclared instance state are rejected. A mutable-container subclass remains rejected as a root or nested value even when decorated with `frozen=True`, because inherited container operations can still change its contents. Dataclass subclasses must themselves be decorated with `frozen=True`; records cannot declare cached properties or extra slots. Pydantic models cannot use `extra="allow"` or PrivateAttr. Shared immutable children are allowed. Invalid events raise TypeError without changing the queue; valid events are retained without copying. The publisher must respect normal frozen-value semantics. Direct event-bus publication and repository entity-change notifications keep their existing contracts, including entity references.
|
|
99
|
+
|
|
70
100
|
Typed caches inject `DistributedCache[Item]` and take keys/values directly. The value type owns its cache name; the Host owns the Redis prefix and global defaults. Entry options replace the whole default object. `consider_uow=True` stages changes in the concrete UoW and applies them after successful completion; rollback discards them. Redis and PostgreSQL do not share an atomic commit.
|
|
71
101
|
|
|
102
|
+
Batch cache I/O remains serial. The first unhidden Redis error, invalid payload, or cancellation can stop later commands; reads may refresh expiry or delete expired entries. After a database commit, ordinary cache errors allow other pending writes to run, while cancellation stops later commands. `hide_errors` only hides Redis errors.
|
|
103
|
+
|
|
104
|
+
## HTTP streaming and continuous access
|
|
105
|
+
|
|
106
|
+
`HttpRouter` supports synchronous and asynchronous generator endpoints with native FastAPI SSE, JSONL, and `StreamingResponse`. ApplicationService methods retain their existing return contract: they may return a native streaming Response, but do not become generator methods. Native FastAPI/Starlette own encoding and transmission; the framework manages invocation and resource boundaries.
|
|
107
|
+
|
|
108
|
+
| Phase | Owner and invariant |
|
|
109
|
+
| --- | --- |
|
|
110
|
+
| Preparation | Authorization precedes input binding. Dependencies and Filters perform checks and short business calls needed before sending. Function-scoped dependencies exit, business work commits, and preparation ACTION/UoW resources are released before transmission. The framework does not read the first generator item here; code before the first `yield` still runs during iteration. |
|
|
111
|
+
| Transmission | The HTTP transport owner retains REQUEST and supervises production, sending, and access checks. Framework-supervised reads receive the owning task's request identity and invocation permission, without inheriting preparation ACTION/UoW state. Further database work uses an independent short service call or UoW that ends before `yield`. Ordinary child tasks and external threads do not gain that permission. |
|
|
112
|
+
| Cleanup | After production exits, close the source, release preparation subscriptions, and then release REQUEST. Acquire subscriptions through a native request-scoped yield dependency so they are released even if the response generator never starts. Its unstarted `finally` cannot own a subscription acquired elsewhere. Repeated cancellation must not end the wait for actual cleanup; cleanup failures retain their cause. A synchronous generator's running `next()` must eventually return. |
|
|
113
|
+
|
|
114
|
+
Never carry a business Session, transaction, or AnyIO cancellation scope across a response `yield`. A generator's asynchronous `finally` must shield its own awaited cleanup locally; framework-managed close does not protect arbitrary awaits inside an already-unwinding generator. See the [complete example](development.md#http-generators-and-continuous-access).
|
|
115
|
+
|
|
116
|
+
Managed HTTP streams with authorization requirements check access before sending and periodically afterwards, including while idle or blocked by a slow client. Each check uses an independent short REQUEST, refreshes identity and permissions, and releases its services/UoW before returning. HTTP authenticators and permission checkers can inject the native `Request` and make short managed calls; they do not share the long request's cached services or transaction. Checks for one connection are serialized; a due send waits for the check. User/session changes invalidate access. Failure is recorded before sending is cancelled; WebSocket transport also discards queued business frames. WebSocket continuous checks concern its connection endpoint declarations, while `invoke` authorizes each service call separately without accumulating historical permissions.
|
|
117
|
+
|
|
118
|
+
Routes with no authorization requirement or `allow_anonymous` do not opt into HTTP continuous access checks. Shared authorization short-circuits these cases and `authorization.always_allow` before reading the final identity. `always_allow` does not fabricate a user or accept an invalid token; authentication and real-user requirements remain active. An authenticator may call managed methods admitted by these short-circuits, but reading final `CurrentUser` during authentication still raises the authentication-cycle error. An endpoint-injected `CurrentUser` remains its immutable preparation snapshot; new short calls during streaming use the latest checked snapshot.
|
|
119
|
+
|
|
120
|
+
Actual `http.response.start` is the error boundary. Before it, preparation/access failures can use normal error handlers. After it, terminate and record the failure; do not attempt a replacement JSON response. Shared refresh configuration and Host shutdown timeout belong to [README operations and migration](../README.md#streaming-and-access-migration-07-unreleased).
|
|
121
|
+
|
|
72
122
|
## Extension ownership
|
|
73
123
|
|
|
74
124
|
Identity owns core user/role fields, edit versions, and their base migrations. `concurrency_version` protects edits and relationship replacements; `permission_version` only tracks permission state. Password changes, deactivation, and deletion revoke sessions in the same transaction. Login and refresh coordinate through the user's transaction lock; refresh families retain their own lock-before-row mutation order. Consumer modules own only their declared extension columns/indexes and project-local revisions.
|
|
@@ -100,12 +150,12 @@ Module initialization precedes selected hosted services and background execution
|
|
|
100
150
|
## Background work and external boundaries
|
|
101
151
|
|
|
102
152
|
- Durable jobs use typed, versioned payloads and the PostgreSQL provider. Transactional enqueue must use the same configured connection as the surrounding unit of work. External side effects remain the job's idempotency responsibility.
|
|
103
|
-
- Periodic workers are explicitly registered.
|
|
153
|
+
- Periodic workers are explicitly registered. No workers are generated. When adding one, configuration cannot enable a worker disabled in its class declaration.
|
|
104
154
|
- Execution modes are `host`, `shared`, and `exclusive`. Spawned processes compose their own Application; they do not inherit a live Session or container.
|
|
105
155
|
- A `HostedService` owns long-lived SDKs or threads, their failures, recovery, and actual shutdown. It is opt-in; background-process instances need explicit selection. Shutdown has global barriers: close ordinary admission, await `stopping`, close remaining admission, drain accepted work, then release resources in `stop` before Module/container cleanup. Only successfully started services prepare; all touched services clean up. External callbacks use the separate, temporary stopping context, never an upgraded startup context.
|
|
106
156
|
- Redis business locks are leases. Preserve cancellation and cleanup when ownership is lost; a lease is not a transaction, fencing guarantee, or exactly-once guarantee.
|
|
107
157
|
- In-process channels preserve SDK input during stopping only for declared `stopping_owners` and a current hook context passed to `send/submit`. The channel Module depends on the service owner. Admission permissions are Application-local and temporary; accepted work retains its reservation until execution or a reported non-execution outcome, with unchanged capacity and per-message scopes.
|
|
108
|
-
- WebSocket delivery targets currently connected clients. The
|
|
158
|
+
- WebSocket delivery targets currently connected clients. The native process-local transport has no cross-process backplane, offline replay, or delivery acknowledgment. HTTP streams follow the [preparation, transmission, and cleanup boundaries](#http-streaming-and-continuous-access).
|
|
109
159
|
- Liveness and readiness report lifecycle state, not continuous infrastructure availability. Trace export is disabled in the generated development configuration until explicitly configured.
|
|
110
160
|
|
|
111
161
|
## Documentation ownership
|