python-ddd-framework 0.6.0__py3-none-any.whl → 0.7.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. python_ddd_framework/authorization/checking.py +35 -0
  2. python_ddd_framework/developer_kit/generation.py +0 -9
  3. python_ddd_framework/developer_kit/templates/module/cookiecutter.json +1 -1
  4. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/README.md +29 -77
  5. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/module.py.jinja +5 -72
  6. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/module.py.jinja +1 -3
  7. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/module.py.jinja +2 -3
  8. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/module.py.jinja +8 -62
  9. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/module.py.jinja +1 -2
  10. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/AGENTS.md +12 -8
  11. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/README.md +22 -10
  12. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/backend/src/host/main.py.jinja +4 -1
  13. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/docs/architecture.md +40 -6
  14. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/docs/development.md +177 -30
  15. python_ddd_framework/fastapi/__init__.py +2 -0
  16. python_ddd_framework/fastapi/access.py +127 -0
  17. python_ddd_framework/fastapi/access_options.py +13 -0
  18. python_ddd_framework/fastapi/action.py +6 -0
  19. python_ddd_framework/fastapi/adapter.py +25 -10
  20. python_ddd_framework/fastapi/health.py +20 -11
  21. python_ddd_framework/fastapi/manual_action.py +33 -2
  22. python_ddd_framework/fastapi/module.py +2 -0
  23. python_ddd_framework/fastapi/realtime/authentication.py +21 -10
  24. python_ddd_framework/fastapi/realtime/options.py +0 -1
  25. python_ddd_framework/fastapi/realtime/runtime.py +41 -26
  26. python_ddd_framework/fastapi/request_context.py +10 -2
  27. python_ddd_framework/fastapi/routing.py +13 -6
  28. python_ddd_framework/fastapi/server.py +9 -0
  29. python_ddd_framework/fastapi/service_endpoints.py +5 -0
  30. python_ddd_framework/fastapi/streaming.py +95 -0
  31. python_ddd_framework/fastapi/transfer.py +138 -3
  32. python_ddd_framework/invocation/dispatcher.py +2 -24
  33. python_ddd_framework/invocation/scopes.py +5 -3
  34. python_ddd_framework/observability/formatting.py +1 -0
  35. python_ddd_framework/sqlalchemy/migration.py +17 -1
  36. python_ddd_framework/unit_of_work/manager.py +11 -2
  37. {python_ddd_framework-0.6.0.dist-info → python_ddd_framework-0.7.1.dist-info}/METADATA +38 -9
  38. {python_ddd_framework-0.6.0.dist-info → python_ddd_framework-0.7.1.dist-info}/RECORD +42 -89
  39. {python_ddd_framework-0.6.0.dist-info → python_ddd_framework-0.7.1.dist-info}/WHEEL +1 -1
  40. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_jobs/order_approval/__init__.py.jinja +0 -1
  41. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_jobs/order_approval/handler.py.jinja +0 -45
  42. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_jobs/order_approval/payload.py.jinja +0 -8
  43. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_jobs/order_statistics/__init__.py.jinja +0 -1
  44. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_jobs/order_statistics/handler.py.jinja +0 -35
  45. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_jobs/order_statistics/payload.py.jinja +0 -5
  46. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_jobs/order_statistics/schedule.py.jinja +0 -15
  47. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_workers/order_maintenance_worker.py.jinja +0 -28
  48. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_workers/order_statistics_worker.py.jinja +0 -26
  49. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/caching/order_cache.py.jinja +0 -9
  50. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/caching/statistics_cache.py.jinja +0 -9
  51. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/event_handlers/order_changed_handler.py.jinja +0 -23
  52. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/hosted_services/order_integration_service.py.jinja +0 -71
  53. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/hosted_services/order_observation_handler.py.jinja +0 -16
  54. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/integration_services/order_reporting_service.py.jinja +0 -16
  55. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/interceptors/order_timing_interceptor.py.jinja +0 -17
  56. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/options/order_options.py.jinja +0 -7
  57. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/services/order_approval_service.py.jinja +0 -65
  58. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/services/order_management_service.py.jinja +0 -35
  59. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/services/order_query_service.py.jinja +0 -41
  60. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/setting_handlers/approval_setting_observer.py.jinja +0 -17
  61. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/inputs/approve_order.py.jinja +0 -6
  62. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/inputs/create_order.py.jinja +0 -7
  63. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/integration_services/order_reporting_service.py.jinja +0 -7
  64. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/services/order_approval_service.py.jinja +0 -13
  65. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/services/order_management_service.py.jinja +0 -10
  66. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/services/order_query_service.py.jinja +0 -12
  67. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/views/order_statistics_snapshot.py.jinja +0 -9
  68. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/views/order_view.py.jinja +0 -12
  69. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/entities/order.py.jinja +0 -48
  70. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/events/order_changed.py.jinja +0 -13
  71. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/repositories/order_repository.py.jinja +0 -17
  72. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/seeding/order_seed_contributor.py.jinja +0 -22
  73. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/services/order_approval_service.py.jinja +0 -23
  74. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/settings/approval_settings.py.jinja +0 -17
  75. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/value_objects/order_title.py.jinja +0 -10
  76. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/constants/order_constants.py.jinja +0 -3
  77. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/enums/order_status.py.jinja +0 -6
  78. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/errors/order_errors.py.jinja +0 -11
  79. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/messages/order_messages.py.jinja +0 -26
  80. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/messages/order_observation.py.jinja +0 -13
  81. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/permissions/order_permission_provider.py.jinja +0 -19
  82. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/permissions/order_permissions.py.jinja +0 -7
  83. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/value_objects/money.py.jinja +0 -18
  84. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/filters/export_filter.py.jinja +0 -15
  85. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/models/refresh_orders.py.jinja +0 -5
  86. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/routers/order_files.py.jinja +0 -76
  87. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/websockets/order_socket.py.jinja +0 -44
  88. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/models/order_model.py.jinja +0 -29
  89. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/repositories/order_repository.py.jinja +0 -81
  90. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/tests/test_domain.py.jinja +0 -19
  91. {python_ddd_framework-0.6.0.dist-info → python_ddd_framework-0.7.1.dist-info}/entry_points.txt +0 -0
  92. {python_ddd_framework-0.6.0.dist-info → python_ddd_framework-0.7.1.dist-info}/licenses/LICENSE +0 -0
  93. {python_ddd_framework-0.6.0.dist-info → python_ddd_framework-0.7.1.dist-info}/licenses/src/python_ddd_framework/background_jobs/pgqueuer/UPSTREAM_LICENSE.txt +0 -0
@@ -1,26 +0,0 @@
1
- import logging
2
- from datetime import timedelta
3
-
4
- from modules.{{ cookiecutter.module_name }}.application_contracts.integration_services.order_reporting_service import (
5
- OrderReportingService,
6
- )
7
- from modules.{{ cookiecutter.module_name }}.domain_shared.constants.order_constants import MODULE_NAME
8
-
9
- from python_ddd_framework import BackgroundWorker, BackgroundWorkerContext
10
-
11
- logger = logging.getLogger(__name__)
12
-
13
-
14
- class OrderStatisticsWorker(BackgroundWorker):
15
- name = f"{MODULE_NAME}.statistics"
16
- enabled = False
17
- interval = timedelta(minutes=5)
18
- iteration_timeout = timedelta(seconds=10)
19
-
20
- def __init__(self, reporting: OrderReportingService) -> None:
21
- self._reporting = reporting
22
-
23
- async def run_iteration(self, context: BackgroundWorkerContext) -> None:
24
- # 业务扩展:这里是一轮周期统计的处理体;替换统计/汇总逻辑,不自行启动 while 循环或线程。
25
- # 启用这个默认关闭的示例 Worker 前,先确定自己的执行周期和失败处理要求。
26
- logger.info("Order count: %s", await self._reporting.get_count())
@@ -1,9 +0,0 @@
1
- from modules.{{ cookiecutter.module_name }}.application_contracts.views.order_view import OrderView
2
- from modules.{{ cookiecutter.module_name }}.domain_shared.constants.order_constants import MODULE_NAME
3
-
4
- from python_ddd_framework.caching import cache_name
5
-
6
-
7
- @cache_name(f"{MODULE_NAME}.view")
8
- class OrderCacheItem(OrderView):
9
- pass
@@ -1,9 +0,0 @@
1
- from modules.{{ cookiecutter.module_name }}.application_contracts.views.order_statistics_snapshot import OrderStatisticsSnapshot
2
- from modules.{{ cookiecutter.module_name }}.domain_shared.constants.order_constants import MODULE_NAME
3
-
4
- from python_ddd_framework.caching import cache_name
5
-
6
-
7
- @cache_name(f"{MODULE_NAME}.statistics")
8
- class OrderStatisticsCacheItem(OrderStatisticsSnapshot):
9
- pass
@@ -1,23 +0,0 @@
1
- from modules.{{ cookiecutter.module_name }}.application.caching.order_cache import OrderCacheItem
2
- from modules.{{ cookiecutter.module_name }}.domain.events.order_changed import OrderChanged
3
- from modules.{{ cookiecutter.module_name }}.domain_shared.messages.order_messages import OrderChangedMessage
4
-
5
- from python_ddd_framework import LocalEventPhase, local_event_handler
6
- from python_ddd_framework.caching import DistributedCache
7
- from python_ddd_framework.realtime import RealtimePublisher
8
-
9
-
10
- @local_event_handler(phase=LocalEventPhase.AFTER_COMMIT)
11
- class OrderChangedHandler:
12
- def __init__(self, cache: DistributedCache[OrderCacheItem], realtime: RealtimePublisher) -> None:
13
- self._cache, self._realtime = cache, realtime
14
-
15
- async def handle(self, event: OrderChanged) -> None:
16
- # 业务扩展:这里处理提交后的缓存失效/在线通知,替换自己的事件、缓存定义和消息载荷。
17
- # 必须随业务一起回滚的规则应放在聚合或 DOMAIN 阶段;这里失败不会撤销已经提交的数据。
18
- await self._cache.remove(str(event.order_id))
19
- if event.user_id is not None:
20
- await self._realtime.send_to_user(
21
- event.user_id,
22
- OrderChangedMessage(id=event.order_id, status=event.status),
23
- )
@@ -1,71 +0,0 @@
1
- """可选外部线程集成示例;替换线程生产逻辑即可接入 SDK,不另建容器或任务执行器。"""
2
-
3
- import asyncio
4
- import logging
5
- from threading import Event, Thread
6
-
7
- from python_ddd_framework import (
8
- HostedService,
9
- HostedServiceContext,
10
- HostedServiceInvocationRejectedError,
11
- ShutdownReason,
12
- )
13
-
14
- from modules.{{ cookiecutter.module_name }}.application_contracts.integration_services.order_reporting_service import (
15
- OrderReportingService,
16
- )
17
-
18
-
19
- async def observe_integration(reporting: OrderReportingService) -> None:
20
- count = await reporting.get_count()
21
- logging.getLogger(__name__).info("External integration callback observed %s orders", count)
22
-
23
-
24
- class OrderIntegrationService(HostedService):
25
- def __init__(self) -> None:
26
- self._stop = Event()
27
- self._prepare = Event()
28
- self._callback_done = Event()
29
- self._stopping_context: HostedServiceContext | None = None
30
- self._callback_error: BaseException | None = None
31
- self._thread: Thread | None = None
32
-
33
- async def start(self, context: HostedServiceContext) -> None:
34
- await context.call(observe_integration)
35
- self._thread = Thread(target=self._receive, args=(context,), name=__name__)
36
- self._thread.start()
37
-
38
- def _receive(self, context: HostedServiceContext) -> None:
39
- # submit_call 只能来自外部线程;Future.result 等待同一正式 invocation 完成。
40
- try:
41
- try:
42
- context.submit_call(observe_integration).result()
43
- except HostedServiceInvocationRejectedError:
44
- pass # 普通准入可能已经关闭;收尾只使用 stopping 新提供的 context。
45
- self._prepare.wait()
46
- stopping = self._stopping_context
47
- if stopping is not None:
48
- stopping.submit_call(observe_integration).result()
49
- except BaseException as error: # noqa: BLE001 -- 线程故障转交 stopping 重新抛出。
50
- self._callback_error = error
51
- finally:
52
- self._callback_done.set()
53
- self._stop.wait()
54
-
55
- async def stopping(self, context: HostedServiceContext, reason: ShutdownReason) -> None:
56
- # 独立业务调用完成后再等 SDK 回调,不跨线程传 scope,也不跨等待持有事务。
57
- await context.call(observe_integration)
58
- self._stopping_context = context
59
- self._prepare.set()
60
- await asyncio.to_thread(self._callback_done.wait)
61
- if self._callback_error is not None:
62
- raise RuntimeError("External integration callback failed") from self._callback_error
63
-
64
- async def stop(self, reason: ShutdownReason) -> None:
65
- # start 部分失败时 stopping 不会执行;仍须唤醒并等待实际线程退出。
66
- self._stop.set()
67
- self._prepare.set()
68
- if self._thread is not None:
69
- await asyncio.to_thread(self._thread.join)
70
- self._thread = None
71
- logging.getLogger(__name__).info("External integration thread stopped")
@@ -1,16 +0,0 @@
1
- """可选线程集成的快照消费者;通过消息能力进入一次受管理调用。"""
2
-
3
- import logging
4
-
5
- from python_ddd_framework import MessageHandler, unit_of_work
6
-
7
- from modules.{{ cookiecutter.module_name }}.domain_shared.messages.order_observation import (
8
- OrderObservation,
9
- )
10
-
11
-
12
- class OrderObservationHandler(MessageHandler[OrderObservation]):
13
- @unit_of_work(disabled=True)
14
- async def handle(self, message: OrderObservation) -> None:
15
- # 替换为快照应用逻辑;若需要保存数据,显式选择事务而非把接收等待放入 UoW。
16
- logging.getLogger(__name__).info("Observed order count: %s", message.count)
@@ -1,16 +0,0 @@
1
- """内部统计使用正式 IntegrationService 调用链,不开放 HTTP。"""
2
-
3
- from modules.{{ cookiecutter.module_name }}.application_contracts.integration_services.order_reporting_service import (
4
- OrderReportingService,
5
- )
6
- from modules.{{ cookiecutter.module_name }}.domain.repositories.order_repository import OrderRepository
7
-
8
- from python_ddd_framework import IntegrationService
9
-
10
-
11
- class _OrderReportingService(IntegrationService, OrderReportingService):
12
- def __init__(self, repository: OrderRepository) -> None:
13
- self._repository = repository
14
-
15
- async def get_count(self) -> int:
16
- return await self._repository.get_count()
@@ -1,17 +0,0 @@
1
- """Record invocation timing through the framework's interceptor extension."""
2
-
3
- import logging
4
- from time import monotonic
5
-
6
- from python_ddd_framework import Interceptor, MethodInvocation
7
-
8
-
9
- class OrderTimingInterceptor(Interceptor):
10
- async def intercept(self, invocation: MethodInvocation) -> object:
11
- started = monotonic()
12
- try:
13
- return await invocation.proceed()
14
- finally:
15
- logging.getLogger(__name__).info(
16
- "Order invocation completed in %.4fs", monotonic() - started
17
- )
@@ -1,7 +0,0 @@
1
- """Typed startup options bound to the module's configuration section."""
2
-
3
- from python_ddd_framework import BaseOptions
4
-
5
-
6
- class {{ cookiecutter.class_prefix }}Options(BaseOptions):
7
- allow_background_approval: bool = True
@@ -1,65 +0,0 @@
1
- import logging
2
- from uuid import UUID
3
-
4
- from modules.{{ cookiecutter.module_name }}.application.background_jobs.order_approval.handler import (
5
- ApprovalJob,
6
- )
7
- from modules.{{ cookiecutter.module_name }}.application.background_jobs.order_approval.payload import (
8
- ApprovalPayload,
9
- )
10
- from modules.{{ cookiecutter.module_name }}.application_contracts.inputs.approve_order import ApproveOrder
11
- from modules.{{ cookiecutter.module_name }}.application_contracts.services.order_approval_service import (
12
- {{ cookiecutter.class_prefix }}ApprovalApplicationService,
13
- )
14
- from modules.{{ cookiecutter.module_name }}.application_contracts.views.order_view import OrderView
15
- from modules.{{ cookiecutter.module_name }}.domain.repositories.order_repository import OrderRepository
16
- from modules.{{ cookiecutter.module_name }}.domain.services.order_approval_service import OrderApprovalService
17
- from modules.{{ cookiecutter.module_name }}.domain_shared.permissions.order_permissions import ORDERS_WRITE
18
-
19
- from python_ddd_framework import ApplicationService, CurrentUser
20
- from python_ddd_framework.authorization import authorize
21
- from python_ddd_framework.background_jobs import BackgroundJobEnqueuer
22
- from python_ddd_framework.errors.base import ResourceConflictError, ResourceNotFoundError
23
- from python_ddd_framework.fastapi import http
24
-
25
- logger = logging.getLogger(__name__)
26
-
27
-
28
- class _{{ cookiecutter.class_prefix }}ApprovalApplicationService(
29
- ApplicationService, {{ cookiecutter.class_prefix }}ApprovalApplicationService
30
- ):
31
- def __init__(
32
- self,
33
- repository: OrderRepository,
34
- user: CurrentUser,
35
- jobs: BackgroundJobEnqueuer,
36
- approval: OrderApprovalService,
37
- ) -> None:
38
- self._repository = repository
39
- self._user = user
40
- self._jobs = jobs
41
- self._approval = approval
42
-
43
- @authorize(ORDERS_WRITE)
44
- async def approve(self, id: UUID, command: ApproveOrder) -> OrderView:
45
- # 业务扩展:这里编排“读取聚合 → 核对版本 → 领域服务检查策略并执行动作 → 保存”。
46
- # 审批只是动作示例;替换为入库等用例时保留权限/并发边界,把状态规则放进领域方法。
47
- order = await self._repository.find(id)
48
- if order is None:
49
- raise ResourceNotFoundError("Order not found")
50
- if order.version != command.expected_version:
51
- raise ResourceConflictError("Order version changed")
52
- await self._approval.approve(order, user_id=self._user.user_id)
53
- await self._repository.save(order)
54
- logger.info("Order approved")
55
- return OrderView.model_validate(order)
56
-
57
- @authorize(ORDERS_WRITE)
58
- @http.post(status_code=202)
59
- async def queue_approval(self, id: UUID) -> str:
60
- # 业务扩展:这里校验入队条件并构造自己的任务载荷;实际后台业务写在对应 Job.execute。
61
- # Job 和载荷类型一起替换,通过注入的入队服务调用,不自行创建消费者或数据库连接。
62
- if await self._repository.find(id) is None:
63
- raise ResourceNotFoundError("Order not found")
64
- # 入队参与本方法原生事务;失败时订单业务与队列写入一起回滚。
65
- return await self._jobs.enqueue(ApprovalJob, ApprovalPayload(order_id=id))
@@ -1,35 +0,0 @@
1
- import logging
2
- from uuid import uuid4
3
-
4
- from modules.{{ cookiecutter.module_name }}.application_contracts.inputs.create_order import CreateOrder
5
- from modules.{{ cookiecutter.module_name }}.application_contracts.services.order_management_service import (
6
- {{ cookiecutter.class_prefix }}ManagementApplicationService,
7
- )
8
- from modules.{{ cookiecutter.module_name }}.application_contracts.views.order_view import OrderView
9
- from modules.{{ cookiecutter.module_name }}.domain.entities.order import Order
10
- from modules.{{ cookiecutter.module_name }}.domain.repositories.order_repository import OrderRepository
11
- from modules.{{ cookiecutter.module_name }}.domain.value_objects.order_title import OrderTitle
12
- from modules.{{ cookiecutter.module_name }}.domain_shared.permissions.order_permissions import ORDERS_WRITE
13
-
14
- from python_ddd_framework import ApplicationService
15
- from python_ddd_framework.authorization import authorize
16
- from python_ddd_framework.fastapi import http
17
-
18
- logger = logging.getLogger(__name__)
19
-
20
-
21
- class _{{ cookiecutter.class_prefix }}ManagementApplicationService(
22
- ApplicationService, {{ cookiecutter.class_prefix }}ManagementApplicationService
23
- ):
24
- def __init__(self, repository: OrderRepository) -> None:
25
- self._repository = repository
26
-
27
- @authorize(ORDERS_WRITE)
28
- @http.post(status_code=201)
29
- async def create(self, command: CreateOrder) -> OrderView:
30
- # 业务扩展:这里编排创建用例,把输入 DTO 转成领域对象,调用领域方法,再交给仓储保存。
31
- # 将 Order/OrderTitle 换成自己的聚合和值对象;领域规则写进聚合,事务由正式调用管线完成。
32
- order = Order.create(uuid4(), OrderTitle(value=command.title))
33
- await self._repository.save(order)
34
- logger.info("Order created")
35
- return OrderView.model_validate(order)
@@ -1,41 +0,0 @@
1
- import logging
2
- from uuid import UUID
3
-
4
- from modules.{{ cookiecutter.module_name }}.application.caching.order_cache import OrderCacheItem
5
- from modules.{{ cookiecutter.module_name }}.application_contracts.services.order_query_service import (
6
- {{ cookiecutter.class_prefix }}QueryApplicationService,
7
- )
8
- from modules.{{ cookiecutter.module_name }}.application_contracts.views.order_view import OrderView
9
- from modules.{{ cookiecutter.module_name }}.domain.repositories.order_repository import OrderRepository
10
- from modules.{{ cookiecutter.module_name }}.domain_shared.permissions.order_permissions import ORDERS_READ
11
-
12
- from python_ddd_framework import ApplicationService
13
- from python_ddd_framework.authorization import authorize
14
- from python_ddd_framework.caching import DistributedCache
15
- from python_ddd_framework.errors.base import ResourceNotFoundError
16
-
17
- logger = logging.getLogger(__name__)
18
-
19
-
20
- class _{{ cookiecutter.class_prefix }}QueryApplicationService(ApplicationService, {{ cookiecutter.class_prefix }}QueryApplicationService):
21
- def __init__(self, repository: OrderRepository, cache: DistributedCache[OrderCacheItem]) -> None:
22
- self._repository = repository
23
- self._cache = cache
24
-
25
- @authorize(ORDERS_READ)
26
- async def get(self, id: UUID) -> OrderView:
27
- # 业务扩展:这里组织单条查询及 DTO 返回;保留缓存时,定义和变更后的失效逻辑要一起调整。
28
- cached = await self._cache.get(str(id))
29
- if cached is not None:
30
- return cached
31
- order = await self._repository.find(id)
32
- if order is None:
33
- raise ResourceNotFoundError("Order not found")
34
- view = OrderCacheItem.model_validate(order)
35
- await self._cache.set(str(id), view)
36
- return view
37
-
38
- @authorize(ORDERS_READ)
39
- async def get_list(self) -> tuple[OrderView, ...]:
40
- # 业务扩展:这里接收实际业务的查询条件;需要筛选/分页时同步扩展 DTO 和仓储查询契约。
41
- return tuple(OrderView.model_validate(order) for order in await self._repository.get_list())
@@ -1,17 +0,0 @@
1
- """Application-owned setting snapshot refreshed by the framework."""
2
-
3
- import logging
4
- import os
5
-
6
- from python_ddd_framework.settings import SettingSnapshot
7
-
8
-
9
- class ApprovalSettingObserver:
10
- def __init__(self) -> None:
11
- self.snapshot: SettingSnapshot[bool] | None = None
12
-
13
- async def apply(self, snapshot: SettingSnapshot[bool]) -> None:
14
- self.snapshot = snapshot
15
- logging.getLogger(__name__).info(
16
- "Approval setting applied: %s pid=%s", snapshot.value, os.getpid()
17
- )
@@ -1,6 +0,0 @@
1
- from pydantic import BaseModel, ConfigDict, Field
2
-
3
-
4
- class ApproveOrder(BaseModel):
5
- model_config = ConfigDict(frozen=True, extra="forbid")
6
- expected_version: int = Field(ge=1)
@@ -1,7 +0,0 @@
1
- from modules.{{ cookiecutter.module_name }}.domain_shared.constants.order_constants import TITLE_MAX_LENGTH
2
- from pydantic import BaseModel, ConfigDict, Field
3
-
4
-
5
- class CreateOrder(BaseModel):
6
- model_config = ConfigDict(frozen=True, extra="forbid")
7
- title: str = Field(min_length=1, max_length=TITLE_MAX_LENGTH)
@@ -1,7 +0,0 @@
1
- from typing import Protocol
2
-
3
- from python_ddd_framework import ApplicationServiceContract
4
-
5
-
6
- class OrderReportingService(ApplicationServiceContract, Protocol):
7
- async def get_count(self) -> int: ...
@@ -1,13 +0,0 @@
1
- from typing import Protocol
2
- from uuid import UUID
3
-
4
- from modules.{{ cookiecutter.module_name }}.application_contracts.inputs.approve_order import ApproveOrder
5
- from modules.{{ cookiecutter.module_name }}.application_contracts.views.order_view import OrderView
6
-
7
- from python_ddd_framework import ApplicationServiceContract
8
-
9
-
10
- class {{ cookiecutter.class_prefix }}ApprovalApplicationService(ApplicationServiceContract, Protocol):
11
- async def approve(self, id: UUID, command: ApproveOrder) -> OrderView: ...
12
-
13
- async def queue_approval(self, id: UUID) -> str: ...
@@ -1,10 +0,0 @@
1
- from typing import Protocol
2
-
3
- from modules.{{ cookiecutter.module_name }}.application_contracts.inputs.create_order import CreateOrder
4
- from modules.{{ cookiecutter.module_name }}.application_contracts.views.order_view import OrderView
5
-
6
- from python_ddd_framework import ApplicationServiceContract
7
-
8
-
9
- class {{ cookiecutter.class_prefix }}ManagementApplicationService(ApplicationServiceContract, Protocol):
10
- async def create(self, command: CreateOrder) -> OrderView: ...
@@ -1,12 +0,0 @@
1
- from typing import Protocol
2
- from uuid import UUID
3
-
4
- from modules.{{ cookiecutter.module_name }}.application_contracts.views.order_view import OrderView
5
-
6
- from python_ddd_framework import ApplicationServiceContract
7
-
8
-
9
- class {{ cookiecutter.class_prefix }}QueryApplicationService(ApplicationServiceContract, Protocol):
10
- async def get(self, id: UUID) -> OrderView: ...
11
-
12
- async def get_list(self) -> tuple[OrderView, ...]: ...
@@ -1,9 +0,0 @@
1
- from datetime import datetime
2
-
3
- from pydantic import BaseModel, ConfigDict, Field
4
-
5
-
6
- class OrderStatisticsSnapshot(BaseModel):
7
- model_config = ConfigDict(frozen=True)
8
- count: int = Field(ge=0)
9
- captured_at: datetime
@@ -1,12 +0,0 @@
1
- from uuid import UUID
2
-
3
- from modules.{{ cookiecutter.module_name }}.domain_shared.enums.order_status import OrderStatus
4
- from pydantic import BaseModel, ConfigDict
5
-
6
-
7
- class OrderView(BaseModel):
8
- model_config = ConfigDict(frozen=True, from_attributes=True)
9
- id: UUID
10
- title: str
11
- status: OrderStatus
12
- version: int
@@ -1,48 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from uuid import UUID
4
-
5
- from modules.{{ cookiecutter.module_name }}.domain.events.order_changed import OrderChanged
6
- from modules.{{ cookiecutter.module_name }}.domain.value_objects.order_title import OrderTitle
7
- from modules.{{ cookiecutter.module_name }}.domain_shared.enums.order_status import OrderStatus
8
- from modules.{{ cookiecutter.module_name }}.domain_shared.errors.order_errors import ALREADY_APPROVED
9
-
10
- from python_ddd_framework import AggregateRoot
11
- from python_ddd_framework.errors import BusinessError
12
-
13
-
14
- class Order(AggregateRoot[UUID]):
15
- def __init__(
16
- self,
17
- id: UUID,
18
- title: OrderTitle,
19
- *,
20
- status: OrderStatus = OrderStatus.PENDING,
21
- version: int = 0,
22
- ) -> None:
23
- super().__init__(id, version=version)
24
- self._title, self._status = title, status
25
-
26
- @property
27
- def title(self) -> str:
28
- return self._title.value
29
-
30
- @property
31
- def status(self) -> OrderStatus:
32
- return self._status
33
-
34
- @classmethod
35
- def create(cls, id: UUID, title: OrderTitle) -> Order:
36
- # 业务扩展:在这里校验新聚合的创建条件、初始化实体和值对象;替换订单示例时一起调整参数。
37
- # 领域方法只维护业务状态和事件,持久化由应用服务调用仓储完成。
38
- order = cls(id, title)
39
- order.raise_local_event(OrderChanged(id, order.status))
40
- return order
41
-
42
- def approve(self, *, user_id: UUID | None = None) -> None:
43
- # 业务扩展:在这里实现自己的状态转换条件,例如库存是否足够、单据是否允许提交。
44
- # 校验通过后再修改状态并记录对应事件;HTTP 参数绑定和调用者授权留在外层。
45
- if self.status is OrderStatus.APPROVED:
46
- raise BusinessError(ALREADY_APPROVED)
47
- self._status = OrderStatus.APPROVED
48
- self.raise_local_event(OrderChanged(self.id, self.status, user_id))
@@ -1,13 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from dataclasses import dataclass
4
- from uuid import UUID
5
-
6
- from modules.{{ cookiecutter.module_name }}.domain_shared.enums.order_status import OrderStatus
7
-
8
-
9
- @dataclass(frozen=True)
10
- class OrderChanged:
11
- order_id: UUID
12
- status: OrderStatus
13
- user_id: UUID | None = None
@@ -1,17 +0,0 @@
1
- """领域需要的业务仓储边界;不暴露 Session 或容器。"""
2
-
3
- from typing import Protocol
4
- from uuid import UUID
5
-
6
- from modules.{{ cookiecutter.module_name }}.domain.entities.order import Order
7
-
8
- from python_ddd_framework import RepositoryContract
9
-
10
-
11
- class OrderRepository(RepositoryContract, Protocol):
12
- async def find(self, id: UUID, include_details: bool = True) -> Order | None: ...
13
- async def get(self, id: UUID, include_details: bool = True) -> Order: ...
14
- async def get_count(self) -> int: ...
15
- async def get_paged_list(self, skip_count: int, max_result_count: int, sorting: str | None = None, include_details: bool = False) -> list[Order]: ...
16
- async def get_list(self, include_details: bool = False) -> list[Order]: ...
17
- async def save(self, order: Order) -> None: ...
@@ -1,22 +0,0 @@
1
- """仅 pddd db seed 显式调用;确定性业务 ID 保证重复初始化不产生新订单。"""
2
-
3
- from uuid import NAMESPACE_URL, uuid5
4
-
5
- from modules.{{ cookiecutter.module_name }}.domain.entities.order import Order
6
- from modules.{{ cookiecutter.module_name }}.domain.repositories.order_repository import OrderRepository
7
- from modules.{{ cookiecutter.module_name }}.domain.value_objects.order_title import OrderTitle
8
- from modules.{{ cookiecutter.module_name }}.domain_shared.constants.order_constants import MODULE_NAME
9
-
10
- from python_ddd_framework import DataSeedContributor
11
-
12
-
13
- class {{ cookiecutter.class_prefix }}SeedContributor(DataSeedContributor):
14
- def __init__(self, repository: OrderRepository) -> None:
15
- self._repository = repository
16
-
17
- async def seed(self) -> None:
18
- # 业务扩展:把演示订单换成业务需要的初始数据;使用稳定标识并先查再写,保证重复 seed 安全。
19
- # 真实业务不需要这条示例数据时可移除它;不要在这里执行迁移或启动后台任务。
20
- id = uuid5(NAMESPACE_URL, f"{MODULE_NAME}:sample-order")
21
- if await self._repository.find(id) is None:
22
- await self._repository.save(Order.create(id, OrderTitle(value="Sample order")))
@@ -1,23 +0,0 @@
1
- """审批设置属于跨对象的领域策略;订单自身仍拥有状态转换规则。"""
2
-
3
- from uuid import UUID
4
-
5
- from modules.{{ cookiecutter.module_name }}.domain.entities.order import Order
6
- from modules.{{ cookiecutter.module_name }}.domain.settings.approval_settings import APPROVAL_ENABLED
7
- from modules.{{ cookiecutter.module_name }}.domain_shared.errors.order_errors import APPROVAL_DISABLED
8
-
9
- from python_ddd_framework import DomainService, ValidationEnabled
10
- from python_ddd_framework.errors import BusinessError
11
- from python_ddd_framework.settings import SettingProvider
12
-
13
-
14
- class OrderApprovalService(DomainService, ValidationEnabled):
15
- def __init__(self, settings: SettingProvider) -> None:
16
- self._settings = settings
17
-
18
- async def approve(self, order: Order, *, user_id: UUID | None = None) -> None:
19
- # 业务扩展:依赖设置、仓储或多个聚合的规则写在领域服务;不接收 HTTP DTO。
20
- # 聚合仍保证不能重复审批;保存和提交由调用方完成,失败才能整体回滚。
21
- if not await self._settings.get(APPROVAL_ENABLED):
22
- raise BusinessError(APPROVAL_DISABLED)
23
- order.approve(user_id=user_id)
@@ -1,17 +0,0 @@
1
- """设置默认值属于领域,业务通过 SettingProvider 获取冻结读取视图。"""
2
-
3
- from modules.{{ cookiecutter.module_name }}.domain_shared.constants.order_constants import MODULE_NAME
4
-
5
- from python_ddd_framework.settings import (
6
- SettingDefinition,
7
- SettingDefinitionContext,
8
- SettingDefinitionProvider,
9
- )
10
-
11
- APPROVAL_ENABLED = SettingDefinition(f"{MODULE_NAME}.approval_enabled", bool, default=True)
12
-
13
-
14
- class {{ cookiecutter.class_prefix }}SettingDefinitionProvider(SettingDefinitionProvider):
15
- def define(self, context: SettingDefinitionContext) -> None:
16
- # 定义扩展:在这里发布本模块的动态业务设置;类型/默认值归 SettingDefinition,读取走 SettingProvider。
17
- context.add(APPROVAL_ENABLED)
@@ -1,10 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from modules.{{ cookiecutter.module_name }}.domain_shared.constants.order_constants import TITLE_MAX_LENGTH
4
- from pydantic import Field
5
-
6
- from python_ddd_framework import ValueObject
7
-
8
-
9
- class OrderTitle(ValueObject):
10
- value: str = Field(min_length=1, max_length=TITLE_MAX_LENGTH)
@@ -1,3 +0,0 @@
1
- MODULE_NAME = "{{ cookiecutter.module_name }}"
2
-
3
- TITLE_MAX_LENGTH = 120
@@ -1,6 +0,0 @@
1
- from enum import Enum
2
-
3
-
4
- class OrderStatus(str, Enum):
5
- PENDING = "pending"
6
- APPROVED = "approved"
@@ -1,11 +0,0 @@
1
- from modules.{{ cookiecutter.module_name }}.domain_shared.constants.order_constants import MODULE_NAME
2
-
3
- from python_ddd_framework.errors import BusinessErrorDefinition
4
-
5
- ALREADY_APPROVED = BusinessErrorDefinition(
6
- f"{MODULE_NAME}.already_approved", "Order is already approved"
7
- )
8
-
9
- APPROVAL_DISABLED = BusinessErrorDefinition(
10
- f"{MODULE_NAME}.approval_disabled", "Order approval is disabled"
11
- )