python-ddd-framework 0.3.2__py3-none-any.whl → 0.5.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/__init__.py +139 -21
- python_ddd_framework/application/builder.py +23 -78
- python_ddd_framework/application/composition.py +4 -22
- python_ddd_framework/application/runtime.py +120 -288
- python_ddd_framework/application/state.py +7 -75
- python_ddd_framework/application_services/__init__.py +5 -21
- python_ddd_framework/{services/application_bindings.py → application_services/bindings.py} +52 -9
- python_ddd_framework/application_services/catalog.py +36 -288
- python_ddd_framework/application_services/contracts.py +14 -13
- python_ddd_framework/application_services/dispatcher.py +19 -269
- python_ddd_framework/application_services/invocation.py +197 -15
- python_ddd_framework/application_services/module.py +168 -0
- python_ddd_framework/application_services/pagination.py +21 -0
- python_ddd_framework/auditing/control.py +1 -1
- python_ddd_framework/auditing/module.py +15 -0
- python_ddd_framework/authorization/__init__.py +2 -0
- python_ddd_framework/authorization/contracts.py +7 -2
- python_ddd_framework/authorization/definitions.py +5 -0
- python_ddd_framework/authorization/module.py +40 -0
- python_ddd_framework/authorization/options.py +7 -0
- python_ddd_framework/background_execution/child.py +13 -3
- python_ddd_framework/background_execution/declarations.py +45 -0
- python_ddd_framework/background_execution/lifecycle.py +12 -0
- python_ddd_framework/background_execution/local.py +20 -1
- python_ddd_framework/background_execution/module.py +107 -1
- python_ddd_framework/background_execution/processes.py +11 -1
- python_ddd_framework/background_jobs/catalog.py +59 -42
- python_ddd_framework/background_jobs/contracts.py +16 -19
- python_ddd_framework/background_jobs/pgqueuer/enqueue.py +4 -0
- python_ddd_framework/background_jobs/pgqueuer/module.py +2 -1
- python_ddd_framework/background_jobs/pgqueuer/runtime.py +7 -7
- python_ddd_framework/background_workers/catalog.py +23 -36
- python_ddd_framework/background_workers/contracts.py +3 -0
- python_ddd_framework/background_workers/execution.py +1 -1
- python_ddd_framework/background_workers/runtime.py +47 -1
- python_ddd_framework/caching/__init__.py +7 -6
- python_ddd_framework/caching/contracts.py +65 -67
- python_ddd_framework/caching/errors.py +0 -7
- python_ddd_framework/caching/module.py +18 -0
- python_ddd_framework/caching/naming.py +29 -0
- python_ddd_framework/caching/options.py +38 -0
- python_ddd_framework/caching/unit_of_work.py +46 -0
- python_ddd_framework/cli/__init__.py +31 -6
- python_ddd_framework/cli/project.py +68 -6
- python_ddd_framework/cli/runtime.py +1 -1
- python_ddd_framework/developer_kit/generation.py +75 -22
- python_ddd_framework/developer_kit/project_metadata.py +2 -4
- python_ddd_framework/developer_kit/templates/basic/cookiecutter.json +4 -0
- python_ddd_framework/developer_kit/templates/basic/{{cookiecutter.module_name}}/README.md +45 -0
- python_ddd_framework/developer_kit/templates/basic/{{cookiecutter.module_name}}/__init__.py.jinja +0 -0
- python_ddd_framework/developer_kit/templates/basic/{{cookiecutter.module_name}}/contracts/__init__.py.jinja +0 -0
- python_ddd_framework/developer_kit/templates/basic/{{cookiecutter.module_name}}/contracts/conversion_service.py.jinja +7 -0
- python_ddd_framework/developer_kit/templates/basic/{{cookiecutter.module_name}}/module.py.jinja +38 -0
- python_ddd_framework/developer_kit/templates/basic/{{cookiecutter.module_name}}/services/__init__.py.jinja +0 -0
- python_ddd_framework/developer_kit/templates/basic/{{cookiecutter.module_name}}/services/default_conversion_service.py.jinja +10 -0
- python_ddd_framework/developer_kit/templates/module/cookiecutter.json +1 -1
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/README.md +42 -49
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_jobs/__init__.py.jinja +1 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_jobs/order_approval/__init__.py.jinja +1 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_jobs/order_approval/handler.py.jinja +45 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_jobs/order_approval/payload.py.jinja +8 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_jobs/order_statistics/__init__.py.jinja +1 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_jobs/order_statistics/handler.py.jinja +35 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_jobs/order_statistics/payload.py.jinja +5 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_jobs/order_statistics/schedule.py.jinja +15 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_workers/__init__.py.jinja +1 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_workers/order_maintenance_worker.py.jinja +28 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_workers/order_statistics_worker.py.jinja +26 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/caching/__init__.py.jinja +1 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/caching/order_cache.py.jinja +9 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/caching/statistics_cache.py.jinja +9 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/event_handlers/__init__.py.jinja +1 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/event_handlers/order_changed_handler.py.jinja +23 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/hosted_services/__init__.py.jinja +1 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/hosted_services/order_integration_service.py.jinja +71 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/hosted_services/order_observation_handler.py.jinja +16 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/integration_services/__init__.py.jinja +1 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/integration_services/order_reporting_service.py.jinja +16 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/interceptors/__init__.py.jinja +1 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/interceptors/order_timing_interceptor.py.jinja +17 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/module.py.jinja +105 -10
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/options/__init__.py.jinja +1 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/{options.py.jinja → options/order_options.py.jinja} +1 -1
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/services/__init__.py.jinja +1 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/services/order_approval_service.py.jinja +65 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/services/order_management_service.py.jinja +35 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/services/order_query_service.py.jinja +41 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/setting_handlers/__init__.py.jinja +1 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/setting_handlers/approval_setting_observer.py.jinja +17 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/inputs/__init__.py.jinja +1 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/inputs/approve_order.py.jinja +6 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/inputs/create_order.py.jinja +7 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/integration_services/__init__.py.jinja +1 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/integration_services/order_reporting_service.py.jinja +7 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/module.py.jinja +39 -2
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/services/__init__.py.jinja +1 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/services/order_approval_service.py.jinja +13 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/services/order_management_service.py.jinja +10 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/services/order_query_service.py.jinja +12 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/views/__init__.py.jinja +1 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/views/order_statistics_snapshot.py.jinja +9 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/views/order_view.py.jinja +12 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/entities/__init__.py.jinja +1 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/{orders.py.jinja → entities/order.py.jinja} +10 -18
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/events/__init__.py.jinja +1 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/events/order_changed.py.jinja +13 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/module.py.jinja +42 -3
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/repositories/__init__.py.jinja +1 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/repositories/order_repository.py.jinja +17 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/seeding/__init__.py.jinja +1 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/seeding/order_seed_contributor.py.jinja +22 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/services/__init__.py.jinja +1 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/services/order_approval_service.py.jinja +23 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/settings/__init__.py.jinja +1 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/{settings.py.jinja → settings/approval_settings.py.jinja} +3 -2
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/value_objects/__init__.py.jinja +1 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/value_objects/order_title.py.jinja +10 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/constants/__init__.py.jinja +1 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/constants/order_constants.py.jinja +3 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/enums/__init__.py.jinja +1 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/enums/order_status.py.jinja +6 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/errors/__init__.py.jinja +1 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/errors/order_errors.py.jinja +11 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/messages/__init__.py.jinja +1 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/messages/order_messages.py.jinja +26 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/messages/order_observation.py.jinja +13 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/module.py.jinja +40 -1
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/permissions/__init__.py.jinja +1 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/{permissions.py.jinja → permissions/order_permission_provider.py.jinja} +6 -2
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/permissions/order_permissions.py.jinja +7 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/value_objects/__init__.py.jinja +1 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/value_objects/money.py.jinja +18 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/filters/__init__.py.jinja +1 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/filters/export_filter.py.jinja +15 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/models/__init__.py.jinja +1 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/models/refresh_orders.py.jinja +5 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/module.py.jinja +85 -7
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/routers/__init__.py.jinja +1 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/{files.py.jinja → routers/order_files.py.jinja} +23 -20
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/websockets/__init__.py.jinja +1 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/websockets/order_socket.py.jinja +44 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/migrations/__init__.py.jinja +1 -1
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/models/__init__.py.jinja +1 -7
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/models/base.py.jinja +7 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/models/{orders.py.jinja → order_model.py.jinja} +7 -3
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/module.py.jinja +42 -5
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/repositories/order_repository.py.jinja +79 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/tests/__init__.py.jinja +1 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/tests/test_domain.py.jinja +4 -3
- python_ddd_framework/developer_kit/templates/project/cookiecutter.json +1 -1
- python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/AGENTS.md +23 -3
- python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/README.md +29 -22
- python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/{app.development.yaml.jinja → backend/app.development.yaml.jinja} +2 -0
- python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/{pyproject.toml.jinja → backend/pyproject.toml.jinja} +1 -1
- python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/{src → backend/src}/host/main.py.jinja +2 -0
- python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/{src → backend/src}/host/module.py.jinja +40 -3
- python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/docs/architecture.md +42 -11
- python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/docs/development.md +144 -23
- python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/scripts/.gitkeep +0 -0
- python_ddd_framework/distributed_lock/contracts.py +4 -1
- python_ddd_framework/domain/__init__.py +2 -0
- python_ddd_framework/domain/services.py +11 -0
- python_ddd_framework/errors/business.py +10 -1
- python_ddd_framework/errors/lifecycle.py +3 -31
- python_ddd_framework/events/__init__.py +5 -2
- python_ddd_framework/events/aggregate.py +3 -3
- python_ddd_framework/events/catalog.py +12 -11
- python_ddd_framework/events/contracts.py +6 -1
- python_ddd_framework/events/contribution.py +1 -52
- python_ddd_framework/events/entities.py +23 -0
- python_ddd_framework/events/module.py +97 -0
- python_ddd_framework/events/runtime.py +80 -29
- python_ddd_framework/events/subscription.py +36 -0
- python_ddd_framework/events/unit_of_work.py +25 -0
- python_ddd_framework/extensions/__init__.py +22 -0
- python_ddd_framework/extensions/catalog.py +198 -0
- python_ddd_framework/extensions/contracts.py +85 -0
- python_ddd_framework/extensions/models.py +142 -0
- python_ddd_framework/extensions/module.py +21 -0
- python_ddd_framework/fastapi/__init__.py +2 -0
- python_ddd_framework/fastapi/action.py +1 -2
- python_ddd_framework/fastapi/adapter.py +16 -0
- python_ddd_framework/fastapi/application_services.py +43 -25
- python_ddd_framework/fastapi/background.py +2 -1
- python_ddd_framework/fastapi/contracts.py +26 -8
- python_ddd_framework/fastapi/health.py +1 -1
- python_ddd_framework/fastapi/module.py +14 -0
- python_ddd_framework/fastapi/parameters.py +3 -18
- python_ddd_framework/fastapi/realtime/connection.py +10 -7
- python_ddd_framework/fastapi/realtime/module.py +3 -0
- python_ddd_framework/fastapi/realtime/runtime.py +10 -24
- python_ddd_framework/fastapi/request_context.py +2 -1
- python_ddd_framework/fastapi/routing.py +2 -1
- python_ddd_framework/fastapi/server.py +8 -0
- python_ddd_framework/fastapi/settings.py +2 -1
- python_ddd_framework/hosted_services/bridge.py +95 -17
- python_ddd_framework/hosted_services/catalog.py +6 -16
- python_ddd_framework/hosted_services/contracts.py +9 -1
- python_ddd_framework/hosted_services/declarations.py +15 -0
- python_ddd_framework/hosted_services/module.py +69 -0
- python_ddd_framework/hosted_services/runtime.py +101 -18
- python_ddd_framework/identity/__init__.py +24 -0
- python_ddd_framework/identity/application.py +38 -90
- python_ddd_framework/identity/contracts.py +181 -16
- python_ddd_framework/identity/http_api.py +79 -2
- python_ddd_framework/identity/management.py +166 -0
- python_ddd_framework/identity/module.py +12 -4
- python_ddd_framework/identity/services.py +29 -16
- python_ddd_framework/identity/sqlalchemy/migrations/0003_extra_properties.py +26 -0
- python_ddd_framework/identity/sqlalchemy/migrations/0004_concurrency_version.py +29 -0
- python_ddd_framework/identity/sqlalchemy/models.py +21 -2
- python_ddd_framework/identity/sqlalchemy/module.py +11 -2
- python_ddd_framework/identity/sqlalchemy/repository.py +458 -0
- python_ddd_framework/identity/sqlalchemy/session_security.py +88 -0
- python_ddd_framework/identity/sqlalchemy/stores.py +69 -290
- python_ddd_framework/identity/tokens.py +2 -5
- python_ddd_framework/{application_services/validation.py → invocation/arguments.py} +8 -6
- python_ddd_framework/invocation/contribution.py +61 -0
- python_ddd_framework/invocation/dispatcher.py +291 -0
- python_ddd_framework/invocation/entries.py +2 -25
- python_ddd_framework/invocation/entrypoints.py +126 -0
- python_ddd_framework/{application_services → invocation}/execution.py +8 -5
- python_ddd_framework/invocation/function_runtime.py +3 -1
- python_ddd_framework/invocation/interception.py +113 -42
- python_ddd_framework/{application_services → invocation}/interceptors.py +2 -2
- python_ddd_framework/invocation/managed_proxy.py +116 -0
- python_ddd_framework/invocation/managed_services.py +250 -0
- python_ddd_framework/invocation/methods.py +345 -0
- python_ddd_framework/invocation/module.py +113 -0
- python_ddd_framework/{application_services → invocation}/policies.py +8 -1
- python_ddd_framework/invocation/validation.py +25 -0
- python_ddd_framework/lifecycle/__init__.py +2 -0
- python_ddd_framework/lifecycle/composition.py +5 -2
- python_ddd_framework/lifecycle/participants.py +37 -0
- python_ddd_framework/messaging/__init__.py +25 -0
- python_ddd_framework/messaging/channel.py +232 -0
- python_ddd_framework/messaging/contracts.py +78 -0
- python_ddd_framework/messaging/module.py +114 -0
- python_ddd_framework/messaging/options.py +13 -0
- python_ddd_framework/messaging/receipt.py +27 -0
- python_ddd_framework/messaging/runtime.py +139 -0
- python_ddd_framework/modularity/__init__.py +11 -1
- python_ddd_framework/modularity/contracts.py +47 -16
- python_ddd_framework/modularity/discovery.py +21 -2
- python_ddd_framework/modularity/graph.py +45 -1
- python_ddd_framework/modularity/registry.py +83 -3
- python_ddd_framework/notifications/__init__.py +2 -0
- python_ddd_framework/notifications/catalog.py +3 -2
- python_ddd_framework/notifications/declarations.py +18 -0
- python_ddd_framework/notifications/module.py +21 -0
- python_ddd_framework/observability/logging.py +3 -0
- python_ddd_framework/observability/tracing.py +3 -0
- python_ddd_framework/options/contribution.py +12 -0
- python_ddd_framework/options/registry.py +44 -11
- python_ddd_framework/realtime/__init__.py +2 -1
- python_ddd_framework/realtime/contracts.py +3 -7
- python_ddd_framework/realtime/messages.py +26 -1
- python_ddd_framework/redis/cache.py +243 -0
- python_ddd_framework/redis/cache_scripts.py +40 -0
- python_ddd_framework/redis/distributed_lock.py +10 -2
- python_ddd_framework/redis/module.py +13 -4
- python_ddd_framework/redis/notifications.py +5 -2
- python_ddd_framework/redis/runtime.py +1 -79
- python_ddd_framework/seeding/__init__.py +13 -0
- python_ddd_framework/seeding/contracts.py +45 -0
- python_ddd_framework/seeding/module.py +73 -0
- python_ddd_framework/seeding/runtime.py +63 -0
- python_ddd_framework/services/arbitration.py +23 -0
- python_ddd_framework/services/binding.py +2 -3
- python_ddd_framework/services/composition.py +108 -0
- python_ddd_framework/services/contribution.py +44 -54
- python_ddd_framework/services/convention.py +53 -181
- python_ddd_framework/services/fixed_lifetime.py +12 -15
- python_ddd_framework/services/framework_provider.py +3 -158
- python_ddd_framework/services/native_graph.py +35 -60
- python_ddd_framework/services/provider.py +59 -4
- python_ddd_framework/services/repository.py +6 -0
- python_ddd_framework/services/runtime.py +129 -215
- python_ddd_framework/settings/catalog.py +5 -4
- python_ddd_framework/settings/declarations.py +18 -0
- python_ddd_framework/settings/definitions.py +5 -0
- python_ddd_framework/settings/manager.py +1 -1
- python_ddd_framework/settings/module.py +50 -1
- python_ddd_framework/settings/notifications.py +4 -2
- python_ddd_framework/settings/refresh_module.py +9 -2
- python_ddd_framework/settings/sqlalchemy/module.py +2 -1
- python_ddd_framework/sqlalchemy/__init__.py +12 -0
- python_ddd_framework/sqlalchemy/alembic_runtime/env.py +1 -0
- python_ddd_framework/sqlalchemy/auditing.py +7 -1
- python_ddd_framework/sqlalchemy/extensions.py +186 -0
- python_ddd_framework/sqlalchemy/metadata.py +110 -6
- python_ddd_framework/sqlalchemy/migration.py +61 -8
- python_ddd_framework/sqlalchemy/migration_operations.py +86 -0
- python_ddd_framework/sqlalchemy/module.py +14 -11
- python_ddd_framework/sqlalchemy/module_migration.py +71 -3
- python_ddd_framework/sqlalchemy/repository.py +217 -13
- python_ddd_framework/sqlalchemy/session_provider.py +14 -1
- python_ddd_framework/testing/runtime.py +5 -4
- python_ddd_framework/unit_of_work/__init__.py +2 -1
- python_ddd_framework/unit_of_work/contracts.py +6 -7
- python_ddd_framework/unit_of_work/errors.py +11 -0
- python_ddd_framework/unit_of_work/lifecycle.py +19 -0
- python_ddd_framework/unit_of_work/manager.py +86 -32
- python_ddd_framework/unit_of_work/module.py +61 -0
- python_ddd_framework/unit_of_work/options.py +7 -1
- python_ddd_framework/validation/__init__.py +1 -0
- python_ddd_framework/validation/models.py +37 -0
- {python_ddd_framework-0.3.2.dist-info → python_ddd_framework-0.5.1.dist-info}/METADATA +147 -44
- python_ddd_framework-0.5.1.dist-info/RECORD +488 -0
- {python_ddd_framework-0.3.2.dist-info → python_ddd_framework-0.5.1.dist-info}/WHEEL +1 -1
- python_ddd_framework/application_services/seeding.py +0 -72
- python_ddd_framework/caching/catalog.py +0 -73
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/cache.py.jinja +0 -10
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/events.py.jinja +0 -24
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/integration.py.jinja +0 -41
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/orders.py.jinja +0 -83
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/tasks.py.jinja +0 -84
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/orders.py.jinja +0 -35
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/repository.py.jinja +0 -14
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/seeding.py.jinja +0 -19
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/definitions.py.jinja +0 -24
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/realtime.py.jinja +0 -36
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/repositories/orders.py.jinja +0 -49
- python_ddd_framework-0.3.2.dist-info/RECORD +0 -355
- /python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/{.dockerignore → backend/.dockerignore} +0 -0
- /python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/{.gitignore → backend/.gitignore} +0 -0
- /python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/{.python-version → backend/.python-version} +0 -0
- /python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/{Dockerfile → backend/Dockerfile} +0 -0
- /python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/{compose.dev.yaml.jinja → backend/compose.dev.yaml.jinja} +0 -0
- /python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/{compose.production.yaml.jinja → backend/compose.production.yaml.jinja} +0 -0
- /python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/{src → backend/src}/host/__init__.py.jinja +0 -0
- /python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/{tests → backend/tests}/conftest.py.jinja +0 -0
- /python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/{tests → backend/tests}/host/test_http.py.jinja +0 -0
- /python_ddd_framework/{application_services → invocation}/signature.py +0 -0
- {python_ddd_framework-0.3.2.dist-info → python_ddd_framework-0.5.1.dist-info}/entry_points.txt +0 -0
- {python_ddd_framework-0.3.2.dist-info → python_ddd_framework-0.5.1.dist-info}/licenses/LICENSE +0 -0
- {python_ddd_framework-0.3.2.dist-info → python_ddd_framework-0.5.1.dist-info}/licenses/src/python_ddd_framework/background_jobs/pgqueuer/UPSTREAM_LICENSE.txt +0 -0
|
@@ -1,73 +1,71 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""注入时确定值类型的缓存契约。"""
|
|
2
2
|
|
|
3
|
-
from
|
|
3
|
+
from collections.abc import Awaitable, Callable, Iterable
|
|
4
|
+
from typing import Protocol, TypeVar
|
|
4
5
|
|
|
5
|
-
from
|
|
6
|
-
from typing import Any, Generic, Protocol, TypeVar
|
|
7
|
-
|
|
8
|
-
from pydantic import TypeAdapter
|
|
9
|
-
|
|
10
|
-
from .errors import CacheDefinitionError
|
|
6
|
+
from .options import DistributedCacheEntryOptions
|
|
11
7
|
|
|
12
8
|
_T = TypeVar("_T")
|
|
13
9
|
|
|
14
10
|
|
|
15
|
-
class
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
async def
|
|
72
|
-
|
|
73
|
-
async def
|
|
11
|
+
class DistributedCache(Protocol[_T]):
|
|
12
|
+
async def get(
|
|
13
|
+
self, key: str, *, hide_errors: bool | None = None, consider_uow: bool = False
|
|
14
|
+
) -> _T | None: ...
|
|
15
|
+
|
|
16
|
+
async def get_many(
|
|
17
|
+
self, keys: Iterable[str], *, hide_errors: bool | None = None, consider_uow: bool = False
|
|
18
|
+
) -> list[tuple[str, _T | None]]: ...
|
|
19
|
+
|
|
20
|
+
async def get_or_add(
|
|
21
|
+
self,
|
|
22
|
+
key: str,
|
|
23
|
+
factory: Callable[[], Awaitable[_T | None]],
|
|
24
|
+
*,
|
|
25
|
+
options_factory: Callable[[], DistributedCacheEntryOptions] | None = None,
|
|
26
|
+
hide_errors: bool | None = None,
|
|
27
|
+
consider_uow: bool = False,
|
|
28
|
+
) -> _T | None: ...
|
|
29
|
+
|
|
30
|
+
async def get_or_add_many(
|
|
31
|
+
self,
|
|
32
|
+
keys: Iterable[str],
|
|
33
|
+
factory: Callable[[list[str]], Awaitable[Iterable[tuple[str, _T]]]],
|
|
34
|
+
*,
|
|
35
|
+
options_factory: Callable[[], DistributedCacheEntryOptions] | None = None,
|
|
36
|
+
hide_errors: bool | None = None,
|
|
37
|
+
consider_uow: bool = False,
|
|
38
|
+
) -> list[tuple[str, _T | None]]: ...
|
|
39
|
+
|
|
40
|
+
async def set(
|
|
41
|
+
self,
|
|
42
|
+
key: str,
|
|
43
|
+
value: _T,
|
|
44
|
+
*,
|
|
45
|
+
options: DistributedCacheEntryOptions | None = None,
|
|
46
|
+
hide_errors: bool | None = None,
|
|
47
|
+
consider_uow: bool = False,
|
|
48
|
+
) -> None: ...
|
|
49
|
+
|
|
50
|
+
async def set_many(
|
|
51
|
+
self,
|
|
52
|
+
items: Iterable[tuple[str, _T]],
|
|
53
|
+
*,
|
|
54
|
+
options: DistributedCacheEntryOptions | None = None,
|
|
55
|
+
hide_errors: bool | None = None,
|
|
56
|
+
consider_uow: bool = False,
|
|
57
|
+
) -> None: ...
|
|
58
|
+
|
|
59
|
+
async def remove(
|
|
60
|
+
self, key: str, *, hide_errors: bool | None = None, consider_uow: bool = False
|
|
61
|
+
) -> None: ...
|
|
62
|
+
|
|
63
|
+
async def remove_many(
|
|
64
|
+
self, keys: Iterable[str], *, hide_errors: bool | None = None, consider_uow: bool = False
|
|
65
|
+
) -> None: ...
|
|
66
|
+
|
|
67
|
+
async def refresh(self, key: str, *, hide_errors: bool | None = None) -> None: ...
|
|
68
|
+
|
|
69
|
+
async def refresh_many(
|
|
70
|
+
self, keys: Iterable[str], *, hide_errors: bool | None = None
|
|
71
|
+
) -> None: ...
|
|
@@ -5,13 +5,6 @@ from __future__ import annotations
|
|
|
5
5
|
from ..errors.base import FrameworkError
|
|
6
6
|
|
|
7
7
|
|
|
8
|
-
class CacheDefinitionError(FrameworkError):
|
|
9
|
-
def __init__(self, *, cache: str, reason: str) -> None:
|
|
10
|
-
self.cache = cache
|
|
11
|
-
self.reason = reason
|
|
12
|
-
super().__init__(f"Invalid cache {cache!r}: {reason}")
|
|
13
|
-
|
|
14
|
-
|
|
15
8
|
class DistributedCacheError(FrameworkError):
|
|
16
9
|
def __init__(self, *, operation: str, cache: str, error_type: str) -> None:
|
|
17
10
|
self.operation = operation
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""缓存默认选项与具体 UoW 延迟写生命周期的组装 owner。"""
|
|
2
|
+
|
|
3
|
+
from ..lifecycle import ConfigureContext
|
|
4
|
+
from ..modularity import AppModule
|
|
5
|
+
from ..unit_of_work.lifecycle import UnitOfWorkLifecycleFactory
|
|
6
|
+
from ..unit_of_work.module import UnitOfWorkModule
|
|
7
|
+
from .options import DistributedCacheOptions
|
|
8
|
+
from .unit_of_work import _CacheLifecycleFactory
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class CachingModule(AppModule):
|
|
12
|
+
dependencies = (UnitOfWorkModule,)
|
|
13
|
+
|
|
14
|
+
def configure(self, context: ConfigureContext) -> None:
|
|
15
|
+
context.configure.default(DistributedCacheOptions, section="caching")
|
|
16
|
+
context.services.add_singleton(
|
|
17
|
+
UnitOfWorkLifecycleFactory, _CacheLifecycleFactory, multi=True
|
|
18
|
+
)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""值类型拥有缓存名称;Provider 复用此元数据,不逐类型注册。"""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
from typing import TypeVar, get_origin
|
|
5
|
+
|
|
6
|
+
_T = TypeVar("_T")
|
|
7
|
+
_CACHE_NAME = "__distributed_cache_name__"
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def cache_name(name: str) -> Callable[[type[_T]], type[_T]]:
|
|
11
|
+
if not name or any(not (character.isalnum() or character in "._-") for character in name):
|
|
12
|
+
raise ValueError("cache name must be a stable identifier")
|
|
13
|
+
|
|
14
|
+
def decorate(value_type: type[_T]) -> type[_T]:
|
|
15
|
+
setattr(value_type, _CACHE_NAME, name)
|
|
16
|
+
return value_type
|
|
17
|
+
|
|
18
|
+
return decorate
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _cache_name(value_type: type[object]) -> str:
|
|
22
|
+
if get_origin(value_type) is not None:
|
|
23
|
+
return str(value_type)
|
|
24
|
+
name = value_type.__dict__.get(_CACHE_NAME)
|
|
25
|
+
if name is None:
|
|
26
|
+
return f"{value_type.__module__}.{value_type.__qualname__}"
|
|
27
|
+
if not isinstance(name, str):
|
|
28
|
+
raise TypeError("cache name metadata must be a string")
|
|
29
|
+
return name
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""全局默认与调用级完整对象覆盖,不建立按缓存类型的配置表。"""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime, timedelta
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
|
6
|
+
|
|
7
|
+
from ..options import BaseOptions
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class DistributedCacheEntryOptions(BaseModel):
|
|
11
|
+
model_config = ConfigDict(frozen=True, extra="forbid", validate_default=True)
|
|
12
|
+
|
|
13
|
+
absolute_expiration: datetime | None = None
|
|
14
|
+
absolute_expiration_relative_to_now: timedelta | None = None
|
|
15
|
+
sliding_expiration: timedelta | None = None
|
|
16
|
+
|
|
17
|
+
@field_validator("absolute_expiration")
|
|
18
|
+
@classmethod
|
|
19
|
+
def _aware(cls, value: datetime | None) -> datetime | None:
|
|
20
|
+
if value is not None and value.utcoffset() is None:
|
|
21
|
+
raise ValueError("absolute_expiration must include a timezone")
|
|
22
|
+
return value
|
|
23
|
+
|
|
24
|
+
@field_validator("absolute_expiration_relative_to_now", "sliding_expiration")
|
|
25
|
+
@classmethod
|
|
26
|
+
def _positive(cls, value: timedelta | None) -> timedelta | None:
|
|
27
|
+
if value is not None and not 0 < value.total_seconds() <= 2_147_483_647:
|
|
28
|
+
raise ValueError("expiration must be positive and within the Redis range")
|
|
29
|
+
return value
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class DistributedCacheOptions(BaseOptions):
|
|
33
|
+
hide_errors: bool = True
|
|
34
|
+
default_entry_options: DistributedCacheEntryOptions = Field(
|
|
35
|
+
default_factory=lambda: DistributedCacheEntryOptions(
|
|
36
|
+
sliding_expiration=timedelta(minutes=20)
|
|
37
|
+
)
|
|
38
|
+
)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""延迟写快照归具体 UoW;未完成 owner 的生命周期状态随资源退出丢弃。"""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Awaitable, Callable
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
|
|
6
|
+
from dishka import AsyncContainer
|
|
7
|
+
|
|
8
|
+
from ..invocation import ApplicationInvocationContext
|
|
9
|
+
from ..unit_of_work import UnitOfWork
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class _CachePending:
|
|
14
|
+
writes: dict[str, tuple[bytes | None, Callable[[], Awaitable[None]]]] = field(
|
|
15
|
+
default_factory=dict
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
async def before_commit(self) -> None:
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
async def after_commit(self) -> None:
|
|
22
|
+
failures: list[Exception] = []
|
|
23
|
+
try:
|
|
24
|
+
for _, apply in self.writes.values():
|
|
25
|
+
try:
|
|
26
|
+
await apply()
|
|
27
|
+
except Exception as error:
|
|
28
|
+
failures.append(error)
|
|
29
|
+
finally:
|
|
30
|
+
self.writes.clear()
|
|
31
|
+
if failures:
|
|
32
|
+
raise ExceptionGroup("Committed cache writes failed", failures)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class _CacheLifecycleFactory:
|
|
36
|
+
def create(
|
|
37
|
+
self, container: AsyncContainer, context: ApplicationInvocationContext
|
|
38
|
+
) -> _CachePending:
|
|
39
|
+
return _CachePending()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _pending(work: UnitOfWork) -> _CachePending:
|
|
43
|
+
for lifecycle in work._lifecycles:
|
|
44
|
+
if isinstance(lifecycle, _CachePending):
|
|
45
|
+
return lifecycle
|
|
46
|
+
raise RuntimeError("Caching lifecycle is not configured for this UnitOfWork")
|
|
@@ -6,6 +6,7 @@ import argparse
|
|
|
6
6
|
import asyncio
|
|
7
7
|
import os
|
|
8
8
|
import subprocess
|
|
9
|
+
import sys
|
|
9
10
|
from collections.abc import Sequence
|
|
10
11
|
from enum import Enum
|
|
11
12
|
from pathlib import Path
|
|
@@ -30,12 +31,15 @@ def _parser() -> argparse.ArgumentParser:
|
|
|
30
31
|
new.add_argument("name")
|
|
31
32
|
new.add_argument("--framework-wheel", type=Path, help="Validate with a local framework wheel")
|
|
32
33
|
new.add_argument("--dry-run", action="store_true")
|
|
34
|
+
new.add_argument("--python", help="Portable target version, e.g. 3.12 or 3.12.13")
|
|
35
|
+
new.add_argument("--no-sync", action="store_true", help="Generate files without an environment")
|
|
33
36
|
add = commands.add_parser(
|
|
34
37
|
_Command.ADD.value, help="Add a business module to the current project"
|
|
35
38
|
).add_subparsers(dest="kind", required=True)
|
|
36
39
|
module = add.add_parser("module", help="Generate and register a business module")
|
|
37
40
|
module.add_argument("name")
|
|
38
41
|
module.add_argument("--dry-run", action="store_true")
|
|
42
|
+
module.add_argument("--template", choices=("basic", "ddd"), default="ddd")
|
|
39
43
|
dev = commands.add_parser(_Command.DEV.value, help="Run the development web server")
|
|
40
44
|
dev.add_argument("--host", default="127.0.0.1")
|
|
41
45
|
dev.add_argument("--port", type=int, default=8000)
|
|
@@ -58,11 +62,24 @@ def _parser() -> argparse.ArgumentParser:
|
|
|
58
62
|
|
|
59
63
|
def main(argv: Sequence[str] | None = None) -> int:
|
|
60
64
|
parser = _parser()
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
+
arguments = list(sys.argv[1:] if argv is None else argv)
|
|
66
|
+
previous_bytecode = sys.dont_write_bytecode
|
|
67
|
+
if "--dry-run" in arguments:
|
|
68
|
+
# 已处于后端环境时也不让 Host 导入在消费者源码目录留下 pycache。
|
|
69
|
+
sys.dont_write_bytecode = True
|
|
65
70
|
try:
|
|
71
|
+
if arguments and arguments[0] in {
|
|
72
|
+
item.value for item in _Command if item is not _Command.NEW
|
|
73
|
+
}:
|
|
74
|
+
from .project import Project
|
|
75
|
+
|
|
76
|
+
result = Project.current().dispatch(arguments)
|
|
77
|
+
if result is not None:
|
|
78
|
+
return result
|
|
79
|
+
args = parser.parse_args(arguments)
|
|
80
|
+
if args.command is None:
|
|
81
|
+
parser.print_help()
|
|
82
|
+
return 0
|
|
66
83
|
return _execute(args)
|
|
67
84
|
except CommandError as error:
|
|
68
85
|
parser.exit(2, f"pddd: {error}\n")
|
|
@@ -70,6 +87,8 @@ def main(argv: Sequence[str] | None = None) -> int:
|
|
|
70
87
|
parser.exit(
|
|
71
88
|
error.returncode, "pddd: external command failed; diagnostic files were retained\n"
|
|
72
89
|
)
|
|
90
|
+
finally:
|
|
91
|
+
sys.dont_write_bytecode = previous_bytecode
|
|
73
92
|
|
|
74
93
|
|
|
75
94
|
def _execute(args: argparse.Namespace) -> int:
|
|
@@ -84,9 +103,15 @@ def _execute(args: argparse.Namespace) -> int:
|
|
|
84
103
|
"Install python-ddd-framework[developer-kit] to use generation commands"
|
|
85
104
|
) from None
|
|
86
105
|
if command is _Command.NEW:
|
|
87
|
-
new_project(
|
|
106
|
+
new_project(
|
|
107
|
+
args.name,
|
|
108
|
+
args.framework_wheel,
|
|
109
|
+
args.dry_run,
|
|
110
|
+
python=args.python,
|
|
111
|
+
no_sync=args.no_sync,
|
|
112
|
+
)
|
|
88
113
|
else:
|
|
89
|
-
add_module(args.name, args.dry_run)
|
|
114
|
+
add_module(args.name, args.dry_run, template=args.template)
|
|
90
115
|
else:
|
|
91
116
|
from .runtime import database, development_server
|
|
92
117
|
|
|
@@ -3,7 +3,11 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import importlib.metadata as metadata
|
|
6
|
+
import os
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
6
9
|
import tomllib
|
|
10
|
+
from collections.abc import Sequence
|
|
7
11
|
from dataclasses import dataclass
|
|
8
12
|
from pathlib import Path
|
|
9
13
|
from typing import Any
|
|
@@ -33,12 +37,53 @@ class Project:
|
|
|
33
37
|
|
|
34
38
|
@classmethod
|
|
35
39
|
def current(cls) -> Project:
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
current = Path.cwd().resolve()
|
|
41
|
+
# 标准 Host entry point 标识所属后端;无 Host 的嵌套 Python 项目不拥有 pddd 命令。
|
|
42
|
+
for parent in (current, *current.parents):
|
|
43
|
+
for root in (parent / "backend", parent):
|
|
44
|
+
path = root / "pyproject.toml"
|
|
45
|
+
if not path.is_file():
|
|
46
|
+
continue
|
|
47
|
+
project = read_project(path).get("project", {})
|
|
48
|
+
if not isinstance(project, dict):
|
|
49
|
+
raise CommandError("Project metadata must declare project.name")
|
|
50
|
+
hosts = project.get("entry-points", {}).get(HOST_ENTRY_POINT_GROUP)
|
|
51
|
+
if hosts is None:
|
|
52
|
+
continue
|
|
53
|
+
name = project.get("name")
|
|
54
|
+
if not isinstance(name, str) or not name:
|
|
55
|
+
raise CommandError("Project metadata must declare project.name")
|
|
56
|
+
return cls(root, name)
|
|
57
|
+
raise CommandError("Cannot find backend project metadata with framework Host entry points")
|
|
58
|
+
|
|
59
|
+
def dispatch(self, argv: Sequence[str]) -> int | None:
|
|
60
|
+
"""先交给所属环境中的 CLI,再解释命令,避免全局工具版本劫持项目。"""
|
|
61
|
+
environment = self.root / ".venv"
|
|
62
|
+
if Path(sys.prefix).resolve() == environment.resolve():
|
|
63
|
+
os.chdir(self.root)
|
|
64
|
+
return None
|
|
65
|
+
if not (environment / "pyvenv.cfg").is_file():
|
|
66
|
+
raise CommandError(f'Run uv sync --project "{self.root}" before project commands')
|
|
67
|
+
return subprocess.run(
|
|
68
|
+
[
|
|
69
|
+
"uv",
|
|
70
|
+
"run",
|
|
71
|
+
"--project",
|
|
72
|
+
str(self.root),
|
|
73
|
+
"--directory",
|
|
74
|
+
str(self.root),
|
|
75
|
+
"--no-sync",
|
|
76
|
+
"python",
|
|
77
|
+
"-I",
|
|
78
|
+
"-B",
|
|
79
|
+
"-c",
|
|
80
|
+
"from python_ddd_framework.cli import main; raise SystemExit(main())",
|
|
81
|
+
*argv,
|
|
82
|
+
],
|
|
83
|
+
cwd=self.root,
|
|
84
|
+
env=project_environment(),
|
|
85
|
+
check=False,
|
|
86
|
+
).returncode
|
|
42
87
|
|
|
43
88
|
def host(self, name: str) -> metadata.EntryPoint:
|
|
44
89
|
try:
|
|
@@ -67,3 +112,20 @@ class Project:
|
|
|
67
112
|
if len(matches) != 1:
|
|
68
113
|
raise CommandError(f"Module alias {name!r} must select exactly one live Module")
|
|
69
114
|
return live[matches.pop()]
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def project_environment() -> dict[str, str]:
|
|
118
|
+
# 父进程可能来自 ROS、uv tool 或另一个项目;后端环境由本项目声明和 uv 选择。
|
|
119
|
+
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
|
+
environment.pop(key, None)
|
|
131
|
+
return environment
|
|
@@ -11,7 +11,7 @@ from .project import Project
|
|
|
11
11
|
|
|
12
12
|
async def database(operation: str, environment: str, module: str) -> None:
|
|
13
13
|
from ..application import Application
|
|
14
|
-
from ..
|
|
14
|
+
from ..seeding import DataSeeder
|
|
15
15
|
from ..sqlalchemy import SqlAlchemyMigrator
|
|
16
16
|
|
|
17
17
|
project = Project.current()
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import ast
|
|
6
|
+
import asyncio
|
|
6
7
|
import importlib.metadata as metadata
|
|
7
8
|
import importlib.resources as resources
|
|
8
9
|
import inspect
|
|
@@ -15,7 +16,7 @@ import shutil
|
|
|
15
16
|
import subprocess
|
|
16
17
|
import tempfile
|
|
17
18
|
from pathlib import Path
|
|
18
|
-
from typing import Any
|
|
19
|
+
from typing import Any, Literal
|
|
19
20
|
|
|
20
21
|
# 官方库未发布 typing marker;仅在第三方导入边界忽略缺失的类型信息。
|
|
21
22
|
from cookiecutter.main import cookiecutter # type: ignore[import-untyped]
|
|
@@ -23,7 +24,7 @@ from cookiecutter.main import cookiecutter # type: ignore[import-untyped]
|
|
|
23
24
|
from ..application import Application
|
|
24
25
|
from ..cli import ENVIRONMENT_VARIABLE
|
|
25
26
|
from ..cli.errors import CommandError
|
|
26
|
-
from ..cli.project import Project, read_project
|
|
27
|
+
from ..cli.project import Project, project_environment, read_project
|
|
27
28
|
from .project_metadata import add_module_metadata
|
|
28
29
|
from .source import framework_source
|
|
29
30
|
from .wiring import add_dependency
|
|
@@ -79,8 +80,30 @@ def _validate(root: Path) -> None:
|
|
|
79
80
|
read_project(path)
|
|
80
81
|
|
|
81
82
|
|
|
82
|
-
def
|
|
83
|
+
def _python_version(value: str | None) -> tuple[str, str]:
|
|
84
|
+
version = value or platform.python_version()
|
|
85
|
+
match = re.fullmatch(r"(\d+\.\d+)(?:\.\d+)?", version)
|
|
86
|
+
distribution = metadata.distribution("python-ddd-framework")
|
|
87
|
+
supported = {
|
|
88
|
+
classifier.removeprefix("Programming Language :: Python :: ")
|
|
89
|
+
for classifier in distribution.metadata.get_all("Classifier", [])
|
|
90
|
+
if classifier.startswith("Programming Language :: Python :: ")
|
|
91
|
+
}
|
|
92
|
+
if match is None or match[1] not in supported:
|
|
93
|
+
raise CommandError("--python must be a supported Python major.minor[.patch] version")
|
|
94
|
+
return version, "==" + version + (".*" if version.count(".") == 1 else "")
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def new_project(
|
|
98
|
+
name: str,
|
|
99
|
+
wheel: Path | None,
|
|
100
|
+
dry_run: bool,
|
|
101
|
+
*,
|
|
102
|
+
python: str | None = None,
|
|
103
|
+
no_sync: bool = False,
|
|
104
|
+
) -> None:
|
|
83
105
|
package, class_prefix = _name(name)
|
|
106
|
+
python_version, requires_python = _python_version(python)
|
|
84
107
|
destination = Path.cwd() / name
|
|
85
108
|
if destination.exists():
|
|
86
109
|
raise CommandError(f"Destination already exists: {destination}")
|
|
@@ -100,7 +123,8 @@ def new_project(name: str, wheel: Path | None, dry_run: bool) -> None:
|
|
|
100
123
|
"class_prefix": class_prefix,
|
|
101
124
|
"framework_version": source.version,
|
|
102
125
|
"framework_source": source.uv_source,
|
|
103
|
-
"python_version":
|
|
126
|
+
"python_version": python_version,
|
|
127
|
+
"requires_python": requires_python,
|
|
104
128
|
"build_version": generator,
|
|
105
129
|
"host_environment_variable": ENVIRONMENT_VARIABLE,
|
|
106
130
|
"database_password": secrets.token_urlsafe(24),
|
|
@@ -109,22 +133,32 @@ def new_project(name: str, wheel: Path | None, dry_run: bool) -> None:
|
|
|
109
133
|
},
|
|
110
134
|
)
|
|
111
135
|
if source.wheel is not None:
|
|
112
|
-
vendor = staged / "vendor"
|
|
136
|
+
vendor = staged / "backend" / "vendor"
|
|
113
137
|
vendor.mkdir()
|
|
114
138
|
shutil.copy2(source.wheel, vendor / source.wheel.name)
|
|
115
139
|
# copytree 原生拒绝已存在目录;staging 跨 Windows 盘符,不能依赖目录 rename。
|
|
116
140
|
shutil.copytree(staged, destination)
|
|
117
141
|
print(f"Generated {destination}; staging retained at {staged}")
|
|
118
|
-
|
|
142
|
+
if not no_sync:
|
|
143
|
+
backend = destination / "backend"
|
|
144
|
+
subprocess.run(
|
|
145
|
+
["uv", "sync", "--directory", str(backend)],
|
|
146
|
+
cwd=backend,
|
|
147
|
+
env=project_environment(),
|
|
148
|
+
check=True,
|
|
149
|
+
)
|
|
119
150
|
|
|
120
151
|
|
|
121
|
-
def add_module(name: str, dry_run: bool) -> None:
|
|
152
|
+
def add_module(name: str, dry_run: bool, *, template: Literal["basic", "ddd"] = "ddd") -> None:
|
|
122
153
|
package, class_prefix = _name(name)
|
|
123
154
|
project = Project.current()
|
|
124
155
|
application = project.host("application").load()(environment="development")
|
|
125
156
|
if not isinstance(application, Application):
|
|
126
157
|
raise CommandError("The application entry point must return Application")
|
|
127
|
-
|
|
158
|
+
try:
|
|
159
|
+
startup = application.modules.startup.module_type
|
|
160
|
+
finally:
|
|
161
|
+
asyncio.run(application.stop())
|
|
128
162
|
if startup.__qualname__ != startup.__name__:
|
|
129
163
|
raise CommandError("Startup Module must be a top-level class for module generation")
|
|
130
164
|
source_path = Path(inspect.getfile(startup)).resolve()
|
|
@@ -138,29 +172,43 @@ def add_module(name: str, dry_run: bool) -> None:
|
|
|
138
172
|
raise CommandError(f"Module destination already exists: {destination}")
|
|
139
173
|
source = source_path.read_text(encoding="utf-8")
|
|
140
174
|
updated = source
|
|
141
|
-
|
|
142
|
-
("
|
|
143
|
-
|
|
144
|
-
(
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
175
|
+
entries = (
|
|
176
|
+
((f"modules.{package}.module", class_prefix + "Module"),)
|
|
177
|
+
if template == "basic"
|
|
178
|
+
else tuple(
|
|
179
|
+
(f"modules.{package}.{layer}.module", class_prefix + suffix)
|
|
180
|
+
for layer, suffix in (
|
|
181
|
+
("application", "ApplicationModule"),
|
|
182
|
+
("sqlalchemy", "SqlAlchemyModule"),
|
|
183
|
+
("http_api", "HttpApiModule"),
|
|
184
|
+
)
|
|
148
185
|
)
|
|
186
|
+
)
|
|
187
|
+
for import_path, class_name in entries:
|
|
188
|
+
updated = add_dependency(updated, startup.__name__, import_path, class_name)
|
|
149
189
|
metadata_path = project.root / "pyproject.toml"
|
|
150
190
|
metadata_source = metadata_path.read_text(encoding="utf-8")
|
|
151
|
-
|
|
191
|
+
alias_path, alias_class = entries[0 if template == "basic" else 1]
|
|
192
|
+
updated_metadata = add_module_metadata(metadata_source, package, f"{alias_path}:{alias_class}")
|
|
152
193
|
ast.parse(updated)
|
|
153
194
|
if dry_run:
|
|
154
195
|
print(
|
|
155
196
|
f"Would add {destination}, wire modules into {source_path} and update {metadata_path}"
|
|
156
197
|
)
|
|
157
198
|
return
|
|
199
|
+
context = {"module_name": package, "class_prefix": class_prefix}
|
|
200
|
+
if template == "ddd":
|
|
201
|
+
from ..fastapi.application_services import _operation_id, _service_path
|
|
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, ""),
|
|
208
|
+
)
|
|
158
209
|
staged = _render(
|
|
159
|
-
"module",
|
|
160
|
-
|
|
161
|
-
"module_name": package,
|
|
162
|
-
"class_prefix": class_prefix,
|
|
163
|
-
},
|
|
210
|
+
"basic" if template == "basic" else "module",
|
|
211
|
+
context,
|
|
164
212
|
)
|
|
165
213
|
# 先验证全部新文件和接线;已有 startup 文件仅在内容仍等于读取快照时替换。
|
|
166
214
|
if (
|
|
@@ -178,5 +226,10 @@ def add_module(name: str, dry_run: bool) -> None:
|
|
|
178
226
|
pending.write(updated)
|
|
179
227
|
Path(pending.name).replace(source_path)
|
|
180
228
|
metadata_path.write_text(updated_metadata, encoding="utf-8")
|
|
181
|
-
subprocess.run(
|
|
229
|
+
subprocess.run(
|
|
230
|
+
["uv", "sync", "--directory", str(project.root)],
|
|
231
|
+
cwd=project.root,
|
|
232
|
+
env=project_environment(),
|
|
233
|
+
check=True,
|
|
234
|
+
)
|
|
182
235
|
print(f"Added {package}; staging retained at {staged}")
|
|
@@ -8,7 +8,7 @@ from ..cli.errors import CommandError
|
|
|
8
8
|
from ..cli.project import MODULE_ENTRY_POINT_GROUP
|
|
9
9
|
|
|
10
10
|
|
|
11
|
-
def add_module_metadata(source: str, package: str,
|
|
11
|
+
def add_module_metadata(source: str, package: str, entry_point: str) -> str:
|
|
12
12
|
project = tomllib.loads(source)
|
|
13
13
|
backend = project.get("tool", {}).get("uv", {}).get("build-backend", {})
|
|
14
14
|
names = backend.get("module-name")
|
|
@@ -32,9 +32,7 @@ def add_module_metadata(source: str, package: str, class_prefix: str) -> str:
|
|
|
32
32
|
if package in entries:
|
|
33
33
|
raise CommandError(f"Module alias already exists: {package}")
|
|
34
34
|
header = f'[project.entry-points."{MODULE_ENTRY_POINT_GROUP}"]'
|
|
35
|
-
declaration =
|
|
36
|
-
f'{package} = "modules.{package}.sqlalchemy.module:{class_prefix}SqlAlchemyModule"\n'
|
|
37
|
-
)
|
|
35
|
+
declaration = f'{package} = "{entry_point}"\n'
|
|
38
36
|
if MODULE_ENTRY_POINT_GROUP in project["project"].get("entry-points", {}):
|
|
39
37
|
if header not in updated:
|
|
40
38
|
raise CommandError("Module entry point table must use the standard quoted group name")
|