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.
- python_ddd_framework/authorization/checking.py +35 -0
- python_ddd_framework/developer_kit/generation.py +0 -9
- python_ddd_framework/developer_kit/templates/module/cookiecutter.json +1 -1
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/README.md +29 -77
- 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 +12 -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 +40 -6
- python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/docs/development.md +177 -30
- 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 +6 -0
- python_ddd_framework/fastapi/adapter.py +25 -10
- python_ddd_framework/fastapi/health.py +20 -11
- 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 +41 -26
- python_ddd_framework/fastapi/request_context.py +10 -2
- python_ddd_framework/fastapi/routing.py +13 -6
- python_ddd_framework/fastapi/server.py +9 -0
- python_ddd_framework/fastapi/service_endpoints.py +5 -0
- python_ddd_framework/fastapi/streaming.py +95 -0
- python_ddd_framework/fastapi/transfer.py +138 -3
- python_ddd_framework/invocation/dispatcher.py +2 -24
- python_ddd_framework/invocation/scopes.py +5 -3
- python_ddd_framework/observability/formatting.py +1 -0
- python_ddd_framework/sqlalchemy/migration.py +17 -1
- python_ddd_framework/unit_of_work/manager.py +11 -2
- {python_ddd_framework-0.6.0.dist-info → python_ddd_framework-0.7.1.dist-info}/METADATA +38 -9
- {python_ddd_framework-0.6.0.dist-info → python_ddd_framework-0.7.1.dist-info}/RECORD +42 -89
- {python_ddd_framework-0.6.0.dist-info → python_ddd_framework-0.7.1.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 -81
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/tests/test_domain.py.jinja +0 -19
- {python_ddd_framework-0.6.0.dist-info → python_ddd_framework-0.7.1.dist-info}/entry_points.txt +0 -0
- {python_ddd_framework-0.6.0.dist-info → python_ddd_framework-0.7.1.dist-info}/licenses/LICENSE +0 -0
- {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
|
-
from uuid import UUID
|
|
2
|
-
|
|
3
|
-
from modules.{{ cookiecutter.module_name }}.domain_shared.constants.order_constants import MODULE_NAME
|
|
4
|
-
from modules.{{ cookiecutter.module_name }}.domain_shared.enums.order_status import OrderStatus
|
|
5
|
-
from pydantic import BaseModel, ConfigDict
|
|
6
|
-
|
|
7
|
-
from python_ddd_framework.realtime import RealtimeMessage
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
class OrderChangedMessage(RealtimeMessage):
|
|
11
|
-
message_type = f"{MODULE_NAME}.changed.v1"
|
|
12
|
-
id: UUID
|
|
13
|
-
status: OrderStatus
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
class OrderSnapshotItem(BaseModel):
|
|
17
|
-
model_config = ConfigDict(frozen=True)
|
|
18
|
-
id: UUID
|
|
19
|
-
title: str
|
|
20
|
-
status: OrderStatus
|
|
21
|
-
version: int
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
class OrderSnapshotMessage(RealtimeMessage):
|
|
25
|
-
message_type = f"{MODULE_NAME}.snapshot.v1"
|
|
26
|
-
orders: tuple[OrderSnapshotItem, ...]
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
"""权限名称由共享定义拥有;Provider 仅发布定义。"""
|
|
2
|
-
|
|
3
|
-
from modules.{{ cookiecutter.module_name }}.domain_shared.permissions.order_permissions import (
|
|
4
|
-
ORDERS_READ,
|
|
5
|
-
ORDERS_WRITE,
|
|
6
|
-
)
|
|
7
|
-
|
|
8
|
-
from python_ddd_framework import (
|
|
9
|
-
PermissionDefinition,
|
|
10
|
-
PermissionDefinitionContext,
|
|
11
|
-
PermissionDefinitionProvider,
|
|
12
|
-
)
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
class {{ cookiecutter.class_prefix }}PermissionDefinitionProvider(PermissionDefinitionProvider):
|
|
16
|
-
def define(self, context: PermissionDefinitionContext) -> None:
|
|
17
|
-
# 定义扩展:在这里发布自己的业务权限,名称统一放在共享定义;实际授权检查声明在服务/端点上。
|
|
18
|
-
context.add(PermissionDefinition(ORDERS_READ, "Read orders"))
|
|
19
|
-
context.add(PermissionDefinition(ORDERS_WRITE, "Create and approve orders"))
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
from modules.{{ cookiecutter.module_name }}.domain_shared.constants.order_constants import MODULE_NAME
|
|
2
|
-
|
|
3
|
-
from python_ddd_framework import PermissionName
|
|
4
|
-
|
|
5
|
-
ORDERS_READ = PermissionName(f"{MODULE_NAME}.read")
|
|
6
|
-
|
|
7
|
-
ORDERS_WRITE = PermissionName(f"{MODULE_NAME}.write")
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
"""Shared immutable monetary value for domain objects and public DTOs."""
|
|
2
|
-
|
|
3
|
-
from decimal import Decimal
|
|
4
|
-
|
|
5
|
-
from pydantic import Field, field_validator
|
|
6
|
-
|
|
7
|
-
from python_ddd_framework import ValueObject
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
class Money(ValueObject):
|
|
11
|
-
amount: Decimal = Field(ge=0, max_digits=12, decimal_places=2)
|
|
12
|
-
currency: str = Field(default="CNY", pattern=r"^[A-Z]{3}$")
|
|
13
|
-
|
|
14
|
-
@field_validator("amount")
|
|
15
|
-
@classmethod
|
|
16
|
-
def normalize_amount(cls, value: Decimal) -> Decimal:
|
|
17
|
-
# 与两位小数的持久价格一致,避免写入前后公开 DTO 表达不同。
|
|
18
|
-
return value.quantize(Decimal("0.01"))
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import logging
|
|
2
|
-
from collections.abc import Awaitable, Callable
|
|
3
|
-
|
|
4
|
-
from python_ddd_framework.fastapi import ActionFilterContext
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
class ExportFilter:
|
|
8
|
-
async def invoke(
|
|
9
|
-
self, context: ActionFilterContext, next_action: Callable[[], Awaitable[object]]
|
|
10
|
-
) -> object:
|
|
11
|
-
# 管线扩展:在这里添加端点执行前/后的 HTTP 处理;领域规则仍由调用的应用服务/聚合负责。
|
|
12
|
-
# Filter 的作用域只覆盖已绑定参数到响应准备;不假定流传输已经结束。
|
|
13
|
-
result = await next_action()
|
|
14
|
-
logging.getLogger(__name__).info("File response prepared")
|
|
15
|
-
return result
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
import asyncio
|
|
2
|
-
import hashlib
|
|
3
|
-
import logging
|
|
4
|
-
from collections.abc import AsyncIterator
|
|
5
|
-
from typing import Annotated
|
|
6
|
-
from uuid import UUID
|
|
7
|
-
|
|
8
|
-
from fastapi import File, Form, UploadFile
|
|
9
|
-
from modules.{{ cookiecutter.module_name }}.application_contracts.inputs.create_order import CreateOrder
|
|
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 }}.domain_shared.constants.order_constants import MODULE_NAME
|
|
17
|
-
from modules.{{ cookiecutter.module_name }}.domain_shared.permissions.order_permissions import (
|
|
18
|
-
ORDERS_READ,
|
|
19
|
-
ORDERS_WRITE,
|
|
20
|
-
)
|
|
21
|
-
from modules.{{ cookiecutter.module_name }}.http_api.filters.export_filter import ExportFilter
|
|
22
|
-
from starlette.responses import Response, StreamingResponse
|
|
23
|
-
|
|
24
|
-
from python_ddd_framework.authorization import authorize
|
|
25
|
-
from python_ddd_framework.fastapi import HttpRouter, service_filter
|
|
26
|
-
|
|
27
|
-
router = HttpRouter(use_api_prefix=True, prefix=f"/{MODULE_NAME}/files", permissions=(ORDERS_READ,))
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
@router.post("/upload", status_code=201)
|
|
31
|
-
@authorize(ORDERS_WRITE)
|
|
32
|
-
async def upload(
|
|
33
|
-
service: {{ cookiecutter.class_prefix }}ManagementApplicationService,
|
|
34
|
-
title: Annotated[str, Form()],
|
|
35
|
-
file: Annotated[UploadFile, File()],
|
|
36
|
-
) -> dict[str, object]:
|
|
37
|
-
# 接口扩展:在这里解析上传内容并组装业务命令,交给应用服务处理;当前只演示创建订单及计算摘要。
|
|
38
|
-
# File/Form 使用框架读取期限额及原生 parser;UploadFile 资源由 REQUEST 关闭。
|
|
39
|
-
content = await file.read()
|
|
40
|
-
order = await service.create(CreateOrder(title=title))
|
|
41
|
-
return {
|
|
42
|
-
"order": order.model_dump(mode="json"),
|
|
43
|
-
"bytes": len(content),
|
|
44
|
-
"sha256": hashlib.sha256(content).hexdigest(),
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
@router.get("/{id}/download")
|
|
49
|
-
@service_filter(ExportFilter)
|
|
50
|
-
async def download(id: UUID, service: {{ cookiecutter.class_prefix }}QueryApplicationService) -> Response:
|
|
51
|
-
# 接口扩展:通过服务查询已授权的数据后,在这里转换为实际下载格式、文件名和媒体类型。
|
|
52
|
-
order = await service.get(id)
|
|
53
|
-
return Response(
|
|
54
|
-
order.model_dump_json(),
|
|
55
|
-
media_type="application/json",
|
|
56
|
-
headers={"Content-Disposition": f'attachment; filename="{id}.json"'},
|
|
57
|
-
)
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
@router.get("/export/stream")
|
|
61
|
-
@service_filter(ExportFilter)
|
|
62
|
-
async def export(service: {{ cookiecutter.class_prefix }}QueryApplicationService) -> StreamingResponse:
|
|
63
|
-
# 接口扩展:这里准备导出数据,下面的 producer 负责编码输出;不要让业务事务跨越网络发送。
|
|
64
|
-
orders = await service.get_list()
|
|
65
|
-
|
|
66
|
-
async def produce() -> AsyncIterator[bytes]:
|
|
67
|
-
# 输出扩展:将 JSON 行替换为实际流格式;大数据分批读取须各自完成短 UoW 后再 yield。
|
|
68
|
-
try:
|
|
69
|
-
for order in orders:
|
|
70
|
-
yield (order.model_dump_json() + "\n").encode()
|
|
71
|
-
await asyncio.sleep(0)
|
|
72
|
-
finally:
|
|
73
|
-
# 此示例只持有不可变 DTO,无悬挂 Session/文件;扩展外部资源时在此 shield 异步关闭。
|
|
74
|
-
logging.getLogger(__name__).info("Order export producer closed")
|
|
75
|
-
|
|
76
|
-
return StreamingResponse(produce(), media_type="application/x-ndjson")
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
from typing import Annotated
|
|
2
|
-
|
|
3
|
-
from fastapi import APIRouter, Depends
|
|
4
|
-
from modules.{{ cookiecutter.module_name }}.application_contracts.services.order_query_service import (
|
|
5
|
-
{{ cookiecutter.class_prefix }}QueryApplicationService,
|
|
6
|
-
)
|
|
7
|
-
from modules.{{ cookiecutter.module_name }}.domain_shared.constants.order_constants import MODULE_NAME
|
|
8
|
-
from modules.{{ cookiecutter.module_name }}.domain_shared.messages.order_messages import (
|
|
9
|
-
OrderSnapshotItem,
|
|
10
|
-
OrderSnapshotMessage,
|
|
11
|
-
)
|
|
12
|
-
from modules.{{ cookiecutter.module_name }}.http_api.models.refresh_orders import RefreshOrders
|
|
13
|
-
from starlette.websockets import WebSocketDisconnect
|
|
14
|
-
|
|
15
|
-
from python_ddd_framework import FrameworkError
|
|
16
|
-
from python_ddd_framework.fastapi import WebSocketConnection, websocket_connection
|
|
17
|
-
|
|
18
|
-
router = APIRouter()
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
@router.websocket(f"/ws/{MODULE_NAME}")
|
|
22
|
-
async def orders_socket(
|
|
23
|
-
connection: Annotated[WebSocketConnection, Depends(websocket_connection)],
|
|
24
|
-
) -> None:
|
|
25
|
-
# 接口扩展:这里定义“查询快照 → 发送消息 → 接收下一条命令”的交互,替换消息 DTO 和业务调用。
|
|
26
|
-
# 每次业务操作仍经 connection.invoke 进入授权/事务管线,不在连接期间保留 Session 或聚合实例。
|
|
27
|
-
try:
|
|
28
|
-
while True:
|
|
29
|
-
orders = await connection.invoke({{ cookiecutter.class_prefix }}QueryApplicationService.get_list)
|
|
30
|
-
await connection.send(
|
|
31
|
-
OrderSnapshotMessage(
|
|
32
|
-
orders=tuple(
|
|
33
|
-
OrderSnapshotItem(
|
|
34
|
-
id=item.id, title=item.title, status=item.status, version=item.version
|
|
35
|
-
)
|
|
36
|
-
for item in orders
|
|
37
|
-
)
|
|
38
|
-
),
|
|
39
|
-
)
|
|
40
|
-
await connection.receive(RefreshOrders)
|
|
41
|
-
except WebSocketDisconnect:
|
|
42
|
-
return
|
|
43
|
-
except FrameworkError as error:
|
|
44
|
-
await connection.send_problem(error)
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
"""修改模型后显式生成并审查迁移,不在 Host 启动时建表。"""
|
|
2
|
-
|
|
3
|
-
from uuid import UUID
|
|
4
|
-
|
|
5
|
-
from modules.{{ cookiecutter.module_name }}.domain_shared.constants.order_constants import (
|
|
6
|
-
MODULE_NAME,
|
|
7
|
-
TITLE_MAX_LENGTH,
|
|
8
|
-
)
|
|
9
|
-
from modules.{{ cookiecutter.module_name }}.domain_shared.enums.order_status import OrderStatus
|
|
10
|
-
from modules.{{ cookiecutter.module_name }}.sqlalchemy.models.base import {{ cookiecutter.class_prefix }}Base
|
|
11
|
-
from sqlalchemy import Integer, String, Uuid
|
|
12
|
-
from sqlalchemy.orm import Mapped, mapped_column
|
|
13
|
-
|
|
14
|
-
from python_ddd_framework.sqlalchemy import AuditedEntityMixin
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
class OrderRow(AuditedEntityMixin, {{ cookiecutter.class_prefix }}Base):
|
|
18
|
-
__tablename__ = MODULE_NAME
|
|
19
|
-
id: Mapped[UUID] = mapped_column(Uuid, primary_key=True)
|
|
20
|
-
title: Mapped[str] = mapped_column(String(TITLE_MAX_LENGTH), nullable=False)
|
|
21
|
-
status: Mapped[str] = mapped_column(
|
|
22
|
-
String(max(len(item.value) for item in OrderStatus)), nullable=False
|
|
23
|
-
)
|
|
24
|
-
version: Mapped[int] = mapped_column(Integer, nullable=False)
|
|
25
|
-
# SQLAlchemy 以行版本检查并发;聚合暂存版本与数据库提交版本保持一致。
|
|
26
|
-
__mapper_args__ = { # noqa: RUF012 -- SQLAlchemy 原生声明属性,基类不允许 ClassVar。
|
|
27
|
-
"version_id_col": version,
|
|
28
|
-
"version_id_generator": False,
|
|
29
|
-
}
|
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
"""仓储参与当前 UoW,成功暂存后由 operation 收集聚合事件。"""
|
|
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.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.enums.order_status import OrderStatus
|
|
9
|
-
from modules.{{ cookiecutter.module_name }}.sqlalchemy.models.order_model import OrderRow
|
|
10
|
-
|
|
11
|
-
from python_ddd_framework import (
|
|
12
|
-
OptimisticConcurrencyError, ResourceNotFoundError, EntityCreatedEvent, EntityUpdatedEvent,
|
|
13
|
-
)
|
|
14
|
-
from python_ddd_framework.sqlalchemy import SqlAlchemyRepository, SqlAlchemySessionProvider
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
class _OrderRows(SqlAlchemyRepository[OrderRow, UUID]):
|
|
18
|
-
def _default_sorting(self) -> str:
|
|
19
|
-
return "created_at"
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
class SqlAlchemyOrderRepository(OrderRepository):
|
|
23
|
-
def __init__(self, provider: SqlAlchemySessionProvider) -> None:
|
|
24
|
-
self._provider = provider
|
|
25
|
-
self._rows = _OrderRows(provider, OrderRow)
|
|
26
|
-
|
|
27
|
-
async def find(self, id: UUID, include_details: bool = True) -> Order | None:
|
|
28
|
-
# 持久化扩展:这里查询本模块的表,再通过显式映射重建聚合;业务判断留给聚合/应用服务。
|
|
29
|
-
async with self._provider.operation("get"):
|
|
30
|
-
row = await self._rows.find(id, include_details)
|
|
31
|
-
return None if row is None else _order(row)
|
|
32
|
-
|
|
33
|
-
async def get(self, id: UUID, include_details: bool = True) -> Order:
|
|
34
|
-
order = await self.find(id, include_details)
|
|
35
|
-
if order is None:
|
|
36
|
-
raise ResourceNotFoundError("Order not found")
|
|
37
|
-
return order
|
|
38
|
-
|
|
39
|
-
async def get_list(self, include_details: bool = False) -> list[Order]:
|
|
40
|
-
async with self._provider.operation("get_list"):
|
|
41
|
-
return [_order(row) for row in await self._rows.get_list(include_details)]
|
|
42
|
-
|
|
43
|
-
async def get_count(self) -> int:
|
|
44
|
-
return await self._rows.get_count()
|
|
45
|
-
|
|
46
|
-
async def get_paged_list(self, skip_count: int, max_result_count: int,
|
|
47
|
-
sorting: str | None = None, include_details: bool = False) -> list[Order]:
|
|
48
|
-
async with self._provider.operation("get_paged_list"):
|
|
49
|
-
rows = await self._rows.get_paged_list(
|
|
50
|
-
skip_count, max_result_count, sorting, include_details
|
|
51
|
-
)
|
|
52
|
-
return [_order(row) for row in rows]
|
|
53
|
-
|
|
54
|
-
async def save(self, order: Order) -> None:
|
|
55
|
-
# 持久化扩展:把自己的聚合字段/子实体映射到 ORM;下面的版本核对和事件收集边界要保留。
|
|
56
|
-
# 这里只暂存改动,不自行 commit,避免绕开调用者的工作单元。
|
|
57
|
-
async with self._provider.operation(
|
|
58
|
-
"save", aggregate=order,
|
|
59
|
-
entity_event=EntityCreatedEvent(order) if order.version == 0 else EntityUpdatedEvent(order),
|
|
60
|
-
):
|
|
61
|
-
session = await self._provider.get_session()
|
|
62
|
-
row = await session.get(OrderRow, order.id)
|
|
63
|
-
if row is None:
|
|
64
|
-
if order.version:
|
|
65
|
-
raise OptimisticConcurrencyError
|
|
66
|
-
row = OrderRow(id=order.id)
|
|
67
|
-
session.add(row)
|
|
68
|
-
elif row.version != order.version:
|
|
69
|
-
raise OptimisticConcurrencyError
|
|
70
|
-
order.stage_next_version(order.version)
|
|
71
|
-
# 新增持久字段的赋值写在这里,同时修改模型、下方读取映射并生成迁移。
|
|
72
|
-
row.title, row.status, row.version = order.title, order.status.value, order.version
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
def _order(row: OrderRow) -> Order:
|
|
76
|
-
# 映射扩展:在这里把数据库字段恢复成领域实体/值对象,不调用会再次产生创建事件的工厂。
|
|
77
|
-
# 驱动的 UUID 子类在边界恢复为原生 UUID,聚合事件只持有稳定领域值。
|
|
78
|
-
return Order(
|
|
79
|
-
UUID(bytes=row.id.bytes), OrderTitle(value=row.title),
|
|
80
|
-
status=OrderStatus(row.status), version=row.version
|
|
81
|
-
)
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
"""模块内领域验收,不随生产 wheel 打包或参与服务扫描。"""
|
|
2
|
-
|
|
3
|
-
from uuid import uuid4
|
|
4
|
-
|
|
5
|
-
import pytest
|
|
6
|
-
from modules.{{ cookiecutter.module_name }}.domain.entities.order import Order
|
|
7
|
-
from modules.{{ cookiecutter.module_name }}.domain.value_objects.order_title import OrderTitle
|
|
8
|
-
from modules.{{ cookiecutter.module_name }}.domain_shared.enums.order_status import OrderStatus
|
|
9
|
-
|
|
10
|
-
from python_ddd_framework.errors import BusinessError
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
def test_order_approval_rejects_repeat():
|
|
14
|
-
order = Order.create(uuid4(), OrderTitle(value="Example order"))
|
|
15
|
-
order.approve()
|
|
16
|
-
assert order.status is OrderStatus.APPROVED
|
|
17
|
-
assert len(order.release_local_events()) == 2
|
|
18
|
-
with pytest.raises(BusinessError):
|
|
19
|
-
order.approve()
|
{python_ddd_framework-0.6.0.dist-info → python_ddd_framework-0.7.1.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
{python_ddd_framework-0.6.0.dist-info → python_ddd_framework-0.7.1.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|