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
|
@@ -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
|
-
#
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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 ...
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
170
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
201
|
-
|
|
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
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
)
|
|
218
|
-
|
|
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}")
|