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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) 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/manual_action.py +33 -2
  20. python_ddd_framework/fastapi/module.py +2 -0
  21. python_ddd_framework/fastapi/realtime/authentication.py +21 -10
  22. python_ddd_framework/fastapi/realtime/options.py +0 -1
  23. python_ddd_framework/fastapi/realtime/runtime.py +41 -26
  24. python_ddd_framework/fastapi/request_context.py +10 -2
  25. python_ddd_framework/fastapi/routing.py +12 -5
  26. python_ddd_framework/fastapi/server.py +9 -0
  27. python_ddd_framework/fastapi/service_endpoints.py +5 -0
  28. python_ddd_framework/fastapi/streaming.py +95 -0
  29. python_ddd_framework/fastapi/transfer.py +138 -3
  30. python_ddd_framework/invocation/dispatcher.py +2 -24
  31. python_ddd_framework/invocation/scopes.py +5 -3
  32. python_ddd_framework/observability/formatting.py +1 -0
  33. python_ddd_framework/unit_of_work/manager.py +11 -2
  34. {python_ddd_framework-0.6.0.dist-info → python_ddd_framework-0.7.0.dist-info}/METADATA +29 -8
  35. {python_ddd_framework-0.6.0.dist-info → python_ddd_framework-0.7.0.dist-info}/RECORD +39 -86
  36. {python_ddd_framework-0.6.0.dist-info → python_ddd_framework-0.7.0.dist-info}/WHEEL +1 -1
  37. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_jobs/order_approval/__init__.py.jinja +0 -1
  38. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_jobs/order_approval/handler.py.jinja +0 -45
  39. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_jobs/order_approval/payload.py.jinja +0 -8
  40. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_jobs/order_statistics/__init__.py.jinja +0 -1
  41. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_jobs/order_statistics/handler.py.jinja +0 -35
  42. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_jobs/order_statistics/payload.py.jinja +0 -5
  43. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_jobs/order_statistics/schedule.py.jinja +0 -15
  44. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_workers/order_maintenance_worker.py.jinja +0 -28
  45. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_workers/order_statistics_worker.py.jinja +0 -26
  46. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/caching/order_cache.py.jinja +0 -9
  47. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/caching/statistics_cache.py.jinja +0 -9
  48. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/event_handlers/order_changed_handler.py.jinja +0 -23
  49. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/hosted_services/order_integration_service.py.jinja +0 -71
  50. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/hosted_services/order_observation_handler.py.jinja +0 -16
  51. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/integration_services/order_reporting_service.py.jinja +0 -16
  52. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/interceptors/order_timing_interceptor.py.jinja +0 -17
  53. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/options/order_options.py.jinja +0 -7
  54. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/services/order_approval_service.py.jinja +0 -65
  55. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/services/order_management_service.py.jinja +0 -35
  56. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/services/order_query_service.py.jinja +0 -41
  57. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/setting_handlers/approval_setting_observer.py.jinja +0 -17
  58. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/inputs/approve_order.py.jinja +0 -6
  59. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/inputs/create_order.py.jinja +0 -7
  60. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/integration_services/order_reporting_service.py.jinja +0 -7
  61. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/services/order_approval_service.py.jinja +0 -13
  62. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/services/order_management_service.py.jinja +0 -10
  63. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/services/order_query_service.py.jinja +0 -12
  64. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/views/order_statistics_snapshot.py.jinja +0 -9
  65. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/views/order_view.py.jinja +0 -12
  66. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/entities/order.py.jinja +0 -48
  67. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/events/order_changed.py.jinja +0 -13
  68. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/repositories/order_repository.py.jinja +0 -17
  69. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/seeding/order_seed_contributor.py.jinja +0 -22
  70. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/services/order_approval_service.py.jinja +0 -23
  71. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/settings/approval_settings.py.jinja +0 -17
  72. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/value_objects/order_title.py.jinja +0 -10
  73. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/constants/order_constants.py.jinja +0 -3
  74. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/enums/order_status.py.jinja +0 -6
  75. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/errors/order_errors.py.jinja +0 -11
  76. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/messages/order_messages.py.jinja +0 -26
  77. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/messages/order_observation.py.jinja +0 -13
  78. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/permissions/order_permission_provider.py.jinja +0 -19
  79. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/permissions/order_permissions.py.jinja +0 -7
  80. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/value_objects/money.py.jinja +0 -18
  81. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/filters/export_filter.py.jinja +0 -15
  82. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/models/refresh_orders.py.jinja +0 -5
  83. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/routers/order_files.py.jinja +0 -76
  84. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/websockets/order_socket.py.jinja +0 -44
  85. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/models/order_model.py.jinja +0 -29
  86. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/repositories/order_repository.py.jinja +0 -81
  87. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/tests/test_domain.py.jinja +0 -19
  88. {python_ddd_framework-0.6.0.dist-info → python_ddd_framework-0.7.0.dist-info}/entry_points.txt +0 -0
  89. {python_ddd_framework-0.6.0.dist-info → python_ddd_framework-0.7.0.dist-info}/licenses/LICENSE +0 -0
  90. {python_ddd_framework-0.6.0.dist-info → python_ddd_framework-0.7.0.dist-info}/licenses/src/python_ddd_framework/background_jobs/pgqueuer/UPSTREAM_LICENSE.txt +0 -0
@@ -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()