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
|
@@ -10,7 +10,8 @@ from typing import cast
|
|
|
10
10
|
from dishka import AsyncContainer
|
|
11
11
|
|
|
12
12
|
from ..application_services import ApplicationServiceCatalog
|
|
13
|
-
from ..
|
|
13
|
+
from ..application_services.invocation import _InvocationRuntime
|
|
14
|
+
from ..lifecycle import ApplicationState, RuntimeParticipant, ShutdownReason
|
|
14
15
|
from .bridge import HostedServiceContext, _HostedServiceBridge
|
|
15
16
|
from .catalog import HostedServiceCatalog, HostedServiceRegistration
|
|
16
17
|
from .contracts import HostedService, HostedServiceStatus
|
|
@@ -25,6 +26,8 @@ class _HostedServiceEntry:
|
|
|
25
26
|
service: HostedService
|
|
26
27
|
context: HostedServiceContext
|
|
27
28
|
bridge: _HostedServiceBridge
|
|
29
|
+
started: bool = False
|
|
30
|
+
failed: bool = False
|
|
28
31
|
|
|
29
32
|
|
|
30
33
|
class _HostedServiceCoordinator:
|
|
@@ -72,6 +75,38 @@ class _HostedServiceCoordinator:
|
|
|
72
75
|
def readiness(self) -> ApplicationReadinessSnapshot:
|
|
73
76
|
return self._state_owner.snapshot(self._state_getter())
|
|
74
77
|
|
|
78
|
+
def runtime_participant(self) -> RuntimeParticipant:
|
|
79
|
+
enabled = True
|
|
80
|
+
|
|
81
|
+
def prepare(run_services: bool) -> None:
|
|
82
|
+
nonlocal enabled
|
|
83
|
+
enabled = run_services
|
|
84
|
+
|
|
85
|
+
async def start() -> None:
|
|
86
|
+
if not enabled:
|
|
87
|
+
return
|
|
88
|
+
try:
|
|
89
|
+
await self.start()
|
|
90
|
+
except HostedServiceLifecycleError:
|
|
91
|
+
raise
|
|
92
|
+
except BaseException as error:
|
|
93
|
+
raise HostedServiceLifecycleError(
|
|
94
|
+
service=type(self).__qualname__,
|
|
95
|
+
operation="start",
|
|
96
|
+
error_type=type(error).__name__,
|
|
97
|
+
) from error
|
|
98
|
+
|
|
99
|
+
return RuntimeParticipant(
|
|
100
|
+
prepare=prepare,
|
|
101
|
+
start=start,
|
|
102
|
+
stop=self.stop,
|
|
103
|
+
quiesce=self.close_admission,
|
|
104
|
+
stopping=self.stopping,
|
|
105
|
+
close_admission=self.close_admission,
|
|
106
|
+
drain=self.drain,
|
|
107
|
+
ready=lambda: self.readiness().ready,
|
|
108
|
+
)
|
|
109
|
+
|
|
75
110
|
def select_child(self) -> None:
|
|
76
111
|
if self._started:
|
|
77
112
|
raise RuntimeError("Hosted Service process selection must precede start")
|
|
@@ -97,6 +132,7 @@ class _HostedServiceCoordinator:
|
|
|
97
132
|
if container is None or application_service_catalog is None:
|
|
98
133
|
raise RuntimeError("Hosted Service coordinator container is unavailable")
|
|
99
134
|
self._started = True
|
|
135
|
+
runtime = await container.get(_InvocationRuntime)
|
|
100
136
|
for registration in self._registrations:
|
|
101
137
|
self._state_owner.set_status(registration.name, HostedServiceStatus.STARTING)
|
|
102
138
|
try:
|
|
@@ -121,17 +157,55 @@ class _HostedServiceCoordinator:
|
|
|
121
157
|
container=container,
|
|
122
158
|
catalog=application_service_catalog,
|
|
123
159
|
limit=self._options.max_pending_invocations,
|
|
160
|
+
runtime=runtime,
|
|
161
|
+
stopping_owners=(registration.service_type,),
|
|
124
162
|
)
|
|
125
163
|
context = HostedServiceContext(registration.name, bridge)
|
|
126
164
|
entry = _HostedServiceEntry(registration, resolved, context, bridge)
|
|
127
165
|
self._entries.append(entry)
|
|
128
166
|
bridge.open()
|
|
129
167
|
await self._start_entry(entry)
|
|
168
|
+
entry.started = True
|
|
130
169
|
|
|
131
170
|
def close_admission(self) -> None:
|
|
132
171
|
for entry in self._entries:
|
|
133
172
|
entry.bridge.close()
|
|
134
173
|
|
|
174
|
+
async def stopping(self, reason: ShutdownReason) -> tuple[BaseException, ...]:
|
|
175
|
+
failures: list[BaseException] = []
|
|
176
|
+
for entry in reversed(self._entries):
|
|
177
|
+
self._state_owner.set_status(entry.registration.name, HostedServiceStatus.STOPPING)
|
|
178
|
+
if not entry.started:
|
|
179
|
+
continue
|
|
180
|
+
assert self._container is not None
|
|
181
|
+
runtime = await self._container.get(_InvocationRuntime)
|
|
182
|
+
permission = runtime.grant_stopping(entry.registration.service_type)
|
|
183
|
+
context = HostedServiceContext(entry.registration.name, entry.bridge, permission)
|
|
184
|
+
try:
|
|
185
|
+
failure = await self._stop_entry(entry, reason, context=context)
|
|
186
|
+
except BaseException as error:
|
|
187
|
+
failures.append(error)
|
|
188
|
+
entry.failed = True
|
|
189
|
+
else:
|
|
190
|
+
if failure is not None:
|
|
191
|
+
failures.append(failure)
|
|
192
|
+
entry.failed = True
|
|
193
|
+
finally:
|
|
194
|
+
runtime.revoke_stopping(permission)
|
|
195
|
+
return tuple(failures)
|
|
196
|
+
|
|
197
|
+
async def drain(self) -> None:
|
|
198
|
+
outcomes = await asyncio.gather(
|
|
199
|
+
*(
|
|
200
|
+
entry.bridge.drain(self._options.shutdown_timeout.total_seconds())
|
|
201
|
+
for entry in self._entries
|
|
202
|
+
),
|
|
203
|
+
return_exceptions=True,
|
|
204
|
+
)
|
|
205
|
+
failures = tuple(item for item in outcomes if isinstance(item, BaseException))
|
|
206
|
+
if failures:
|
|
207
|
+
raise BaseExceptionGroup("Hosted Service invocation drain failed", failures)
|
|
208
|
+
|
|
135
209
|
async def stop(
|
|
136
210
|
self,
|
|
137
211
|
reason: ShutdownReason,
|
|
@@ -147,19 +221,14 @@ class _HostedServiceCoordinator:
|
|
|
147
221
|
if failure is not None:
|
|
148
222
|
failures.append(failure)
|
|
149
223
|
|
|
150
|
-
if self._entries:
|
|
151
|
-
await asyncio.gather(
|
|
152
|
-
*(
|
|
153
|
-
entry.bridge.drain(self._options.shutdown_timeout.total_seconds())
|
|
154
|
-
for entry in self._entries
|
|
155
|
-
)
|
|
156
|
-
)
|
|
157
224
|
failed_names = {failure.service for failure in failures}
|
|
158
225
|
for entry in self._entries:
|
|
159
226
|
name = entry.registration.name
|
|
160
227
|
self._state_owner.set_status(
|
|
161
228
|
name,
|
|
162
|
-
HostedServiceStatus.FAILED
|
|
229
|
+
HostedServiceStatus.FAILED
|
|
230
|
+
if name in failed_names or entry.failed
|
|
231
|
+
else HostedServiceStatus.STOPPED,
|
|
163
232
|
)
|
|
164
233
|
self._started = False
|
|
165
234
|
return tuple(failures)
|
|
@@ -224,18 +293,29 @@ class _HostedServiceCoordinator:
|
|
|
224
293
|
self,
|
|
225
294
|
entry: _HostedServiceEntry,
|
|
226
295
|
reason: ShutdownReason,
|
|
296
|
+
*,
|
|
297
|
+
context: HostedServiceContext | None = None,
|
|
227
298
|
) -> HostedServiceLifecycleError | None:
|
|
228
299
|
name = entry.registration.name
|
|
229
|
-
|
|
230
|
-
Callable[[ShutdownReason], Awaitable[object]],
|
|
231
|
-
entry.service.stop,
|
|
232
|
-
)
|
|
300
|
+
operation = "stopping" if context is not None else "stop"
|
|
233
301
|
|
|
234
302
|
async def invoke_stop() -> object:
|
|
235
|
-
|
|
303
|
+
if context is None:
|
|
304
|
+
callback = cast(Callable[[ShutdownReason], Awaitable[object]], entry.service.stop)
|
|
305
|
+
return await callback(reason)
|
|
306
|
+
stopping_callback = cast(
|
|
307
|
+
Callable[[HostedServiceContext, ShutdownReason], Awaitable[object]],
|
|
308
|
+
entry.service.stopping,
|
|
309
|
+
)
|
|
310
|
+
try:
|
|
311
|
+
return await stopping_callback(context, reason)
|
|
312
|
+
finally:
|
|
313
|
+
# 返回和失败都在用户钩子的 task 内撤权,不留下一轮调度的准入窗口。
|
|
314
|
+
assert context._permission is not None
|
|
315
|
+
context._permission.runtime.revoke_stopping(context._permission)
|
|
236
316
|
|
|
237
317
|
stop_task = asyncio.create_task(
|
|
238
|
-
invoke_stop(), name=f"python-ddd-framework-hosted-service-
|
|
318
|
+
invoke_stop(), name=f"python-ddd-framework-hosted-service-{operation}:{name}"
|
|
239
319
|
)
|
|
240
320
|
done, _ = await asyncio.wait(
|
|
241
321
|
(stop_task,), timeout=self._options.shutdown_timeout.total_seconds()
|
|
@@ -243,25 +323,28 @@ class _HostedServiceCoordinator:
|
|
|
243
323
|
timed_out = not done
|
|
244
324
|
if timed_out:
|
|
245
325
|
# 取消只是一项请求。实现可能在 finally 中异步释放 SDK 资源;完成前不能释放容器。
|
|
326
|
+
if context is not None:
|
|
327
|
+
assert context._permission is not None
|
|
328
|
+
context._permission.runtime.revoke_stopping(context._permission)
|
|
246
329
|
stop_task.cancel()
|
|
247
330
|
try:
|
|
248
331
|
result = await stop_task
|
|
249
332
|
except BaseException as original_error:
|
|
250
333
|
return HostedServiceLifecycleError(
|
|
251
334
|
service=name,
|
|
252
|
-
operation=
|
|
335
|
+
operation=operation,
|
|
253
336
|
error_type="TimeoutError"
|
|
254
337
|
if timed_out and isinstance(original_error, asyncio.CancelledError)
|
|
255
338
|
else type(original_error).__name__,
|
|
256
339
|
)
|
|
257
340
|
if timed_out:
|
|
258
341
|
return HostedServiceLifecycleError(
|
|
259
|
-
service=name, operation=
|
|
342
|
+
service=name, operation=operation, error_type="TimeoutError"
|
|
260
343
|
)
|
|
261
344
|
if result is not None:
|
|
262
345
|
return HostedServiceLifecycleError(
|
|
263
346
|
service=name,
|
|
264
|
-
operation=
|
|
347
|
+
operation=operation,
|
|
265
348
|
error_type="InvalidReturnValue",
|
|
266
349
|
)
|
|
267
350
|
return None
|
|
@@ -1,11 +1,17 @@
|
|
|
1
|
+
from .application import (
|
|
2
|
+
DefaultAuthenticationApplicationService,
|
|
3
|
+
)
|
|
1
4
|
from .contracts import (
|
|
2
5
|
IDENTITY_ROLES_MANAGE,
|
|
6
|
+
IDENTITY_USER_EXTENSION,
|
|
3
7
|
IDENTITY_USERS_MANAGE,
|
|
4
8
|
AuthenticationApplicationService,
|
|
9
|
+
ChangePasswordCommand,
|
|
5
10
|
CreateRoleCommand,
|
|
6
11
|
CreateUserCommand,
|
|
7
12
|
CurrentUserView,
|
|
8
13
|
IdentityAccessStore,
|
|
14
|
+
IdentityConcurrencyCommand,
|
|
9
15
|
IdentityLoginStore,
|
|
10
16
|
IdentityManagementApplicationService,
|
|
11
17
|
IdentityOptions,
|
|
@@ -19,10 +25,16 @@ from .contracts import (
|
|
|
19
25
|
RefreshSessionStore,
|
|
20
26
|
RefreshTokenCommand,
|
|
21
27
|
RevokeTokenCommand,
|
|
28
|
+
RolePermissionsView,
|
|
22
29
|
RoleView,
|
|
30
|
+
SetPasswordCommand,
|
|
23
31
|
SetRolePermissionsCommand,
|
|
32
|
+
SetUserActiveCommand,
|
|
24
33
|
SetUserRolesCommand,
|
|
25
34
|
TokenPair,
|
|
35
|
+
UpdateRoleCommand,
|
|
36
|
+
UpdateUserCommand,
|
|
37
|
+
UserRolesView,
|
|
26
38
|
UserView,
|
|
27
39
|
)
|
|
28
40
|
from .errors import (
|
|
@@ -31,6 +43,7 @@ from .errors import (
|
|
|
31
43
|
IdentityResourceNotFoundError,
|
|
32
44
|
InvalidCredentialsError,
|
|
33
45
|
)
|
|
46
|
+
from .management import DefaultIdentityManagementApplicationService
|
|
34
47
|
from .module import IdentityModule
|
|
35
48
|
from .passwords import PasswordHasher
|
|
36
49
|
from .services import IdentityAuthenticator
|
|
@@ -39,12 +52,17 @@ from .tokens import JwtTokenService
|
|
|
39
52
|
__all__ = (
|
|
40
53
|
"IDENTITY_ROLES_MANAGE",
|
|
41
54
|
"IDENTITY_USERS_MANAGE",
|
|
55
|
+
"IDENTITY_USER_EXTENSION",
|
|
42
56
|
"AuthenticationApplicationService",
|
|
57
|
+
"ChangePasswordCommand",
|
|
43
58
|
"CreateRoleCommand",
|
|
44
59
|
"CreateUserCommand",
|
|
45
60
|
"CurrentUserView",
|
|
61
|
+
"DefaultAuthenticationApplicationService",
|
|
62
|
+
"DefaultIdentityManagementApplicationService",
|
|
46
63
|
"IdentityAccessStore",
|
|
47
64
|
"IdentityAuthenticator",
|
|
65
|
+
"IdentityConcurrencyCommand",
|
|
48
66
|
"IdentityConflictError",
|
|
49
67
|
"IdentityDefinitionError",
|
|
50
68
|
"IdentityLoginStore",
|
|
@@ -65,9 +83,15 @@ __all__ = (
|
|
|
65
83
|
"RefreshSessionStore",
|
|
66
84
|
"RefreshTokenCommand",
|
|
67
85
|
"RevokeTokenCommand",
|
|
86
|
+
"RolePermissionsView",
|
|
68
87
|
"RoleView",
|
|
88
|
+
"SetPasswordCommand",
|
|
69
89
|
"SetRolePermissionsCommand",
|
|
90
|
+
"SetUserActiveCommand",
|
|
70
91
|
"SetUserRolesCommand",
|
|
71
92
|
"TokenPair",
|
|
93
|
+
"UpdateRoleCommand",
|
|
94
|
+
"UpdateUserCommand",
|
|
95
|
+
"UserRolesView",
|
|
72
96
|
"UserView",
|
|
73
97
|
)
|
|
@@ -2,27 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
from datetime import UTC, datetime
|
|
5
|
+
from datetime import UTC, datetime
|
|
6
6
|
from uuid import UUID, uuid4
|
|
7
7
|
|
|
8
|
-
from ..application_services import ApplicationService,
|
|
8
|
+
from ..application_services import ApplicationService, application_service
|
|
9
9
|
from ..authorization import (
|
|
10
10
|
CurrentUser,
|
|
11
11
|
PermissionCatalog,
|
|
12
|
-
PermissionName,
|
|
13
12
|
allow_anonymous,
|
|
14
13
|
authorize,
|
|
15
14
|
)
|
|
16
15
|
from ..options import Options
|
|
16
|
+
from ..seeding.contracts import DataSeedContributor
|
|
17
17
|
from .contracts import (
|
|
18
|
-
IDENTITY_ROLES_MANAGE,
|
|
19
|
-
IDENTITY_USERS_MANAGE,
|
|
20
18
|
AuthenticationApplicationService,
|
|
21
|
-
|
|
22
|
-
CreateUserCommand,
|
|
19
|
+
ChangePasswordCommand,
|
|
23
20
|
CurrentUserView,
|
|
24
21
|
IdentityLoginStore,
|
|
25
|
-
IdentityManagementApplicationService,
|
|
26
22
|
IdentityOptions,
|
|
27
23
|
IdentityRepository,
|
|
28
24
|
IdentityRole,
|
|
@@ -33,34 +29,24 @@ from .contracts import (
|
|
|
33
29
|
RefreshSessionStore,
|
|
34
30
|
RefreshTokenCommand,
|
|
35
31
|
RevokeTokenCommand,
|
|
36
|
-
RoleView,
|
|
37
|
-
SetRolePermissionsCommand,
|
|
38
|
-
SetUserRolesCommand,
|
|
39
32
|
TokenPair,
|
|
40
|
-
UserView,
|
|
41
33
|
_normalize_name,
|
|
42
34
|
)
|
|
43
35
|
from .errors import (
|
|
44
|
-
IdentityConflictError,
|
|
45
|
-
IdentityResourceNotFoundError,
|
|
46
36
|
InvalidCredentialsError,
|
|
47
37
|
)
|
|
48
38
|
from .passwords import PasswordHasher
|
|
49
39
|
from .tokens import (
|
|
50
|
-
ACCESS_TOKEN_LIFETIME,
|
|
51
40
|
BEARER_TOKEN_TYPE,
|
|
52
41
|
JwtTokenService,
|
|
53
42
|
hash_refresh_token,
|
|
54
43
|
new_refresh_token,
|
|
55
44
|
)
|
|
56
45
|
|
|
57
|
-
FAILED_ATTEMPT_LIMIT = 5
|
|
58
|
-
LOCKOUT_DURATION = timedelta(minutes=15)
|
|
59
|
-
|
|
60
46
|
|
|
61
47
|
@authorize()
|
|
62
48
|
@application_service(provides=AuthenticationApplicationService)
|
|
63
|
-
class
|
|
49
|
+
class DefaultAuthenticationApplicationService(ApplicationService):
|
|
64
50
|
def __init__(
|
|
65
51
|
self,
|
|
66
52
|
login_store: IdentityLoginStore,
|
|
@@ -68,12 +54,16 @@ class _AuthenticationApplicationService(ApplicationService):
|
|
|
68
54
|
passwords: PasswordHasher,
|
|
69
55
|
tokens: JwtTokenService,
|
|
70
56
|
current_user: CurrentUser,
|
|
57
|
+
repository: IdentityRepository,
|
|
58
|
+
options: Options[IdentityOptions],
|
|
71
59
|
) -> None:
|
|
72
60
|
self._login_store = login_store
|
|
73
61
|
self._sessions = sessions
|
|
74
62
|
self._passwords = passwords
|
|
75
63
|
self._tokens = tokens
|
|
76
64
|
self._current_user = current_user
|
|
65
|
+
self._repository = repository
|
|
66
|
+
self._options = options.value
|
|
77
67
|
|
|
78
68
|
@allow_anonymous
|
|
79
69
|
async def login(self, command: LoginCommand) -> TokenPair:
|
|
@@ -95,8 +85,8 @@ class _AuthenticationApplicationService(ApplicationService):
|
|
|
95
85
|
await self._login_store.record_failure(
|
|
96
86
|
user.user_id,
|
|
97
87
|
now=now,
|
|
98
|
-
attempt_limit=
|
|
99
|
-
lockout_duration_seconds=int(
|
|
88
|
+
attempt_limit=self._options.failed_attempt_limit,
|
|
89
|
+
lockout_duration_seconds=int(self._options.lockout_duration.total_seconds()),
|
|
100
90
|
)
|
|
101
91
|
raise InvalidCredentialsError
|
|
102
92
|
|
|
@@ -146,6 +136,28 @@ class _AuthenticationApplicationService(ApplicationService):
|
|
|
146
136
|
user_id=self._current_user.user_id,
|
|
147
137
|
session_id=self._current_user.session_id,
|
|
148
138
|
username=self._current_user.username,
|
|
139
|
+
concurrency_version=(
|
|
140
|
+
await self._repository.get_user(self._current_user.user_id)
|
|
141
|
+
).concurrency_version,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
async def change_password(self, command: ChangePasswordCommand) -> None:
|
|
145
|
+
user_id = self._current_user.user_id
|
|
146
|
+
if user_id is None:
|
|
147
|
+
raise InvalidCredentialsError
|
|
148
|
+
user = await self._repository.get_user(user_id)
|
|
149
|
+
verified, _ = await self._passwords.verify(
|
|
150
|
+
command.old_password.get_secret_value(), user.password_hash
|
|
151
|
+
)
|
|
152
|
+
if not verified:
|
|
153
|
+
raise InvalidCredentialsError
|
|
154
|
+
password_hash = await self._passwords.hash(command.new_password.get_secret_value())
|
|
155
|
+
# Hash 在线程计算;写入时重新核对版本及旧 hash,拒绝计算期间的凭据竞争。
|
|
156
|
+
await self._repository.set_password(
|
|
157
|
+
user_id,
|
|
158
|
+
password_hash,
|
|
159
|
+
command.concurrency_version,
|
|
160
|
+
expected_password_hash=user.password_hash,
|
|
149
161
|
)
|
|
150
162
|
|
|
151
163
|
def _token_pair(
|
|
@@ -159,77 +171,10 @@ class _AuthenticationApplicationService(ApplicationService):
|
|
|
159
171
|
access_token=self._tokens.issue(session.user_id, session.session_id, now=now),
|
|
160
172
|
refresh_token=refresh_token,
|
|
161
173
|
token_type=BEARER_TOKEN_TYPE,
|
|
162
|
-
expires_in=int(
|
|
163
|
-
)
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
@application_service(provides=IdentityManagementApplicationService)
|
|
167
|
-
class _IdentityManagementApplicationService(ApplicationService):
|
|
168
|
-
def __init__(
|
|
169
|
-
self,
|
|
170
|
-
repository: IdentityRepository,
|
|
171
|
-
passwords: PasswordHasher,
|
|
172
|
-
permissions: PermissionCatalog,
|
|
173
|
-
) -> None:
|
|
174
|
-
self._repository = repository
|
|
175
|
-
self._passwords = passwords
|
|
176
|
-
self._permissions = permissions
|
|
177
|
-
|
|
178
|
-
@authorize(IDENTITY_USERS_MANAGE)
|
|
179
|
-
async def create_user(self, command: CreateUserCommand) -> UserView:
|
|
180
|
-
normalized = _normalize_name(command.username, role="username")
|
|
181
|
-
if await self._repository.find_user_by_normalized_username(normalized) is not None:
|
|
182
|
-
raise IdentityConflictError(resource="user")
|
|
183
|
-
user = IdentityUser(
|
|
184
|
-
user_id=uuid4(),
|
|
185
|
-
username=command.username.strip(),
|
|
186
|
-
normalized_username=normalized,
|
|
187
|
-
password_hash=await self._passwords.hash(command.password.get_secret_value()),
|
|
188
|
-
is_active=True,
|
|
189
|
-
failed_access_count=0,
|
|
190
|
-
lockout_end=None,
|
|
191
|
-
permission_version=0,
|
|
174
|
+
expires_in=int(self._options.access_token_lifetime.total_seconds()),
|
|
192
175
|
)
|
|
193
|
-
await self._repository.create_user(user)
|
|
194
|
-
return UserView(user.user_id, user.username, user.is_active, ())
|
|
195
176
|
|
|
196
|
-
@authorize(IDENTITY_USERS_MANAGE)
|
|
197
|
-
async def get_users(self) -> tuple[UserView, ...]:
|
|
198
|
-
return await self._repository.list_users()
|
|
199
177
|
|
|
200
|
-
@authorize(IDENTITY_ROLES_MANAGE)
|
|
201
|
-
async def create_role(self, command: CreateRoleCommand) -> RoleView:
|
|
202
|
-
normalized = _normalize_name(command.name, role="role name")
|
|
203
|
-
if await self._repository.find_role_by_normalized_name(normalized) is not None:
|
|
204
|
-
raise IdentityConflictError(resource="role")
|
|
205
|
-
role = IdentityRole(uuid4(), command.name.strip(), normalized, 0)
|
|
206
|
-
await self._repository.create_role(role)
|
|
207
|
-
return RoleView(role.role_id, role.name, ())
|
|
208
|
-
|
|
209
|
-
@authorize(IDENTITY_ROLES_MANAGE)
|
|
210
|
-
async def get_roles(self) -> tuple[RoleView, ...]:
|
|
211
|
-
return await self._repository.list_roles()
|
|
212
|
-
|
|
213
|
-
@authorize(IDENTITY_USERS_MANAGE, IDENTITY_ROLES_MANAGE)
|
|
214
|
-
async def set_user_roles(self, user_id: UUID, command: SetUserRolesCommand) -> None:
|
|
215
|
-
role_ids = tuple(dict.fromkeys(command.role_ids))
|
|
216
|
-
await self._repository.set_user_roles(user_id, role_ids)
|
|
217
|
-
|
|
218
|
-
@authorize(IDENTITY_ROLES_MANAGE)
|
|
219
|
-
async def set_role_permissions(self, role_id: UUID, command: SetRolePermissionsCommand) -> None:
|
|
220
|
-
names = tuple(PermissionName(item) for item in command.permissions)
|
|
221
|
-
if len(set(names)) != len(names):
|
|
222
|
-
raise IdentityConflictError(resource="role permission")
|
|
223
|
-
unknown = tuple(item for item in names if item not in self._permissions)
|
|
224
|
-
if unknown:
|
|
225
|
-
raise IdentityResourceNotFoundError(resource="permission")
|
|
226
|
-
await self._repository.set_role_permissions(
|
|
227
|
-
role_id,
|
|
228
|
-
tuple(item.value for item in names),
|
|
229
|
-
)
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
@application_service()
|
|
233
178
|
class IdentityAdminSeedContributor(DataSeedContributor):
|
|
234
179
|
def __init__(
|
|
235
180
|
self,
|
|
@@ -270,8 +215,11 @@ class IdentityAdminSeedContributor(DataSeedContributor):
|
|
|
270
215
|
await self._repository.set_role_permissions(
|
|
271
216
|
role.role_id,
|
|
272
217
|
tuple(item.name.value for item in self._permissions.definitions),
|
|
218
|
+
role.concurrency_version,
|
|
219
|
+
)
|
|
220
|
+
await self._repository.set_user_roles(
|
|
221
|
+
user.user_id, (role.role_id,), user.concurrency_version
|
|
273
222
|
)
|
|
274
|
-
await self._repository.set_user_roles(user.user_id, (role.role_id,))
|
|
275
223
|
|
|
276
224
|
|
|
277
225
|
def _new_session(user_id: UUID, refresh_token: str, *, now: datetime) -> RefreshSession:
|