python-ddd-framework 0.3.2__py3-none-any.whl → 0.4.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +96 -291
- 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 +126 -7
- 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/module.py +107 -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 +38 -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 +41 -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/{integration.py.jinja → hosted_services/order_integration_service.py.jinja} +8 -2
- 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 +16 -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 +40 -10
- python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/docs/development.md +134 -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 +20 -1
- python_ddd_framework/hosted_services/catalog.py +6 -16
- python_ddd_framework/hosted_services/contracts.py +4 -0
- 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 +30 -1
- 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 +33 -0
- python_ddd_framework/messaging/__init__.py +25 -0
- python_ddd_framework/messaging/channel.py +205 -0
- python_ddd_framework/messaging/contracts.py +69 -0
- python_ddd_framework/messaging/module.py +101 -0
- python_ddd_framework/messaging/options.py +13 -0
- python_ddd_framework/messaging/receipt.py +27 -0
- python_ddd_framework/messaging/runtime.py +124 -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.4.0.dist-info}/METADATA +139 -44
- python_ddd_framework-0.4.0.dist-info/RECORD +488 -0
- {python_ddd_framework-0.3.2.dist-info → python_ddd_framework-0.4.0.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/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.4.0.dist-info}/entry_points.txt +0 -0
- {python_ddd_framework-0.3.2.dist-info → python_ddd_framework-0.4.0.dist-info}/licenses/LICENSE +0 -0
- {python_ddd_framework-0.3.2.dist-info → python_ddd_framework-0.4.0.dist-info}/licenses/src/python_ddd_framework/background_jobs/pgqueuer/UPSTREAM_LICENSE.txt +0 -0
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
"""管理仓储加入调用方 UoW;拥有并发写入与实体/有效 ORM 映射。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from datetime import UTC, datetime
|
|
6
|
+
from uuid import UUID
|
|
7
|
+
|
|
8
|
+
from sqlalchemy import Select, func, select
|
|
9
|
+
from sqlalchemy.exc import IntegrityError
|
|
10
|
+
|
|
11
|
+
from ...application_services import PagedResult, PagedResultRequestDto
|
|
12
|
+
from ...domain import OptimisticConcurrencyError
|
|
13
|
+
from ...extensions import ExtensionProperties
|
|
14
|
+
from ...sqlalchemy import SqlAlchemyModelCatalog, SqlAlchemySessionProvider
|
|
15
|
+
from ..contracts import (
|
|
16
|
+
IDENTITY_USER_EXTENSION,
|
|
17
|
+
IdentityRole,
|
|
18
|
+
IdentityUser,
|
|
19
|
+
RolePermissionsView,
|
|
20
|
+
RoleView,
|
|
21
|
+
SetUserActiveCommand,
|
|
22
|
+
UpdateRoleCommand,
|
|
23
|
+
UpdateUserCommand,
|
|
24
|
+
UserRolesView,
|
|
25
|
+
UserView,
|
|
26
|
+
_check_version,
|
|
27
|
+
)
|
|
28
|
+
from ..errors import IdentityConflictError, IdentityResourceNotFoundError
|
|
29
|
+
from .models import (
|
|
30
|
+
RolePermissionRow,
|
|
31
|
+
RoleRow,
|
|
32
|
+
UserRoleRow,
|
|
33
|
+
UserRowValues,
|
|
34
|
+
)
|
|
35
|
+
from .session_security import _lock_identity_user, _revoke_user_sessions
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class SqlAlchemyIdentityRepository:
|
|
39
|
+
__slots__ = ("_provider", "_users")
|
|
40
|
+
|
|
41
|
+
def __init__(self, provider: SqlAlchemySessionProvider, models: SqlAlchemyModelCatalog) -> None:
|
|
42
|
+
self._provider = provider
|
|
43
|
+
self._users = models.extension_model(IDENTITY_USER_EXTENSION, UserRowValues)
|
|
44
|
+
|
|
45
|
+
async def find_user_by_normalized_username(
|
|
46
|
+
self,
|
|
47
|
+
normalized_username: str,
|
|
48
|
+
) -> IdentityUser | None:
|
|
49
|
+
async with self._provider.operation("find_user_by_normalized_username"):
|
|
50
|
+
session = await self._provider.get_session()
|
|
51
|
+
statement = select(self._users.model).where(
|
|
52
|
+
self._users.model.normalized_username == normalized_username
|
|
53
|
+
)
|
|
54
|
+
row = await session.scalar(statement)
|
|
55
|
+
return None if row is None else _user_value(row, self._users.read(row))
|
|
56
|
+
|
|
57
|
+
async def create_user(self, user: IdentityUser) -> None:
|
|
58
|
+
async with self._provider.operation("create_user", write=True):
|
|
59
|
+
session = await self._provider.get_session()
|
|
60
|
+
row = self._users.model(
|
|
61
|
+
user_id=user.user_id,
|
|
62
|
+
username=user.username,
|
|
63
|
+
normalized_username=user.normalized_username,
|
|
64
|
+
password_hash=user.password_hash,
|
|
65
|
+
is_active=user.is_active,
|
|
66
|
+
failed_access_count=user.failed_access_count,
|
|
67
|
+
lockout_end=user.lockout_end,
|
|
68
|
+
permission_version=user.permission_version,
|
|
69
|
+
concurrency_version=user.concurrency_version,
|
|
70
|
+
)
|
|
71
|
+
self._users.write(row, user.extra_properties)
|
|
72
|
+
session.add(row)
|
|
73
|
+
await self._flush_unique("user")
|
|
74
|
+
|
|
75
|
+
async def get_user(self, user_id: UUID) -> IdentityUser:
|
|
76
|
+
async with self._provider.operation("get_user"):
|
|
77
|
+
row = await self._require_user(user_id, for_update=False)
|
|
78
|
+
return _user_value(row, self._users.read(row))
|
|
79
|
+
|
|
80
|
+
async def get_user_view(self, user_id: UUID) -> UserView:
|
|
81
|
+
async with self._provider.operation("get_user_view"):
|
|
82
|
+
items = await self._read_user_views(
|
|
83
|
+
select(self._users.model.user_id).where(self._users.model.user_id == user_id)
|
|
84
|
+
)
|
|
85
|
+
if not items:
|
|
86
|
+
raise IdentityResourceNotFoundError(resource="user")
|
|
87
|
+
return items[0]
|
|
88
|
+
|
|
89
|
+
async def _user_view(self, row: UserRowValues) -> UserView:
|
|
90
|
+
session = await self._provider.get_session()
|
|
91
|
+
roles = tuple(
|
|
92
|
+
(
|
|
93
|
+
await session.scalars(
|
|
94
|
+
select(UserRoleRow.role_id)
|
|
95
|
+
.where(UserRoleRow.user_id == row.user_id)
|
|
96
|
+
.order_by(UserRoleRow.role_id)
|
|
97
|
+
)
|
|
98
|
+
).all()
|
|
99
|
+
)
|
|
100
|
+
return UserView(
|
|
101
|
+
user_id=row.user_id,
|
|
102
|
+
username=row.username,
|
|
103
|
+
is_active=row.is_active,
|
|
104
|
+
role_ids=roles,
|
|
105
|
+
concurrency_version=row.concurrency_version,
|
|
106
|
+
extra_properties=self._users.read(row),
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
async def list_users(self, query: PagedResultRequestDto) -> PagedResult[UserView]:
|
|
110
|
+
async with self._provider.operation("list_users"):
|
|
111
|
+
session = await self._provider.get_session()
|
|
112
|
+
total = await session.scalar(select(func.count()).select_from(self._users.model))
|
|
113
|
+
identifiers = (
|
|
114
|
+
select(self._users.model.user_id)
|
|
115
|
+
.order_by(self._users.model.normalized_username, self._users.model.user_id)
|
|
116
|
+
.offset(query.skip_count)
|
|
117
|
+
.limit(query.max_result_count)
|
|
118
|
+
)
|
|
119
|
+
items = await self._read_user_views(identifiers)
|
|
120
|
+
return PagedResult[UserView](items=items, total_count=total or 0)
|
|
121
|
+
|
|
122
|
+
async def _read_user_views(self, identifiers: Select[tuple[UUID]]) -> list[UserView]:
|
|
123
|
+
session = await self._provider.get_session()
|
|
124
|
+
role_ids = (
|
|
125
|
+
select(func.array_agg(UserRoleRow.role_id))
|
|
126
|
+
.where(UserRoleRow.user_id == self._users.model.user_id)
|
|
127
|
+
.correlate(self._users.model)
|
|
128
|
+
.scalar_subquery()
|
|
129
|
+
)
|
|
130
|
+
# 一条 SQL 快照同时读取编辑版本和关系,避免读锁升级与 user gate 形成反向等待。
|
|
131
|
+
rows = (
|
|
132
|
+
await session.execute(
|
|
133
|
+
select(self._users.model, role_ids)
|
|
134
|
+
.where(self._users.model.user_id.in_(identifiers))
|
|
135
|
+
.order_by(self._users.model.normalized_username, self._users.model.user_id)
|
|
136
|
+
.execution_options(populate_existing=True)
|
|
137
|
+
)
|
|
138
|
+
).all()
|
|
139
|
+
return [
|
|
140
|
+
UserView(
|
|
141
|
+
user_id=row.user_id,
|
|
142
|
+
username=row.username,
|
|
143
|
+
is_active=row.is_active,
|
|
144
|
+
role_ids=tuple(sorted(roles or ())),
|
|
145
|
+
concurrency_version=row.concurrency_version,
|
|
146
|
+
extra_properties=self._users.read(row),
|
|
147
|
+
)
|
|
148
|
+
for row, roles in rows
|
|
149
|
+
]
|
|
150
|
+
|
|
151
|
+
async def update_user(self, user_id: UUID, command: UpdateUserCommand) -> UserView:
|
|
152
|
+
async with self._provider.operation("update_user", write=True):
|
|
153
|
+
row = await self._require_user(user_id, for_update=True)
|
|
154
|
+
user = _user_value(row, self._users.read(row)).renamed(
|
|
155
|
+
command.username, command.concurrency_version
|
|
156
|
+
)
|
|
157
|
+
row.normalized_username = user.normalized_username
|
|
158
|
+
row.username = user.username
|
|
159
|
+
self._users.write(row, command.extra_properties)
|
|
160
|
+
row.concurrency_version = user.concurrency_version
|
|
161
|
+
await self._flush_unique("user")
|
|
162
|
+
return await self._user_view(row)
|
|
163
|
+
|
|
164
|
+
async def set_user_active(self, user_id: UUID, command: SetUserActiveCommand) -> UserView:
|
|
165
|
+
async with self._provider.operation("set_user_active", write=True):
|
|
166
|
+
session = await self._provider.get_session()
|
|
167
|
+
row = await self._require_user(user_id, for_update=True)
|
|
168
|
+
user = _user_value(row, self._users.read(row)).with_active(
|
|
169
|
+
command.is_active, command.concurrency_version
|
|
170
|
+
)
|
|
171
|
+
row.is_active = user.is_active
|
|
172
|
+
row.concurrency_version = user.concurrency_version
|
|
173
|
+
if not command.is_active:
|
|
174
|
+
await _revoke_user_sessions(session, user_id, now=datetime.now(UTC))
|
|
175
|
+
await session.flush()
|
|
176
|
+
return await self._user_view(row)
|
|
177
|
+
|
|
178
|
+
async def delete_user(self, user_id: UUID, concurrency_version: int) -> None:
|
|
179
|
+
async with self._provider.operation("delete_user", write=True):
|
|
180
|
+
session = await self._provider.get_session()
|
|
181
|
+
row = await self._require_user(user_id, for_update=True)
|
|
182
|
+
_check_version(row.concurrency_version, concurrency_version)
|
|
183
|
+
await _revoke_user_sessions(session, user_id, now=datetime.now(UTC))
|
|
184
|
+
# FK CASCADE 在同一 UoW 内删除该用户的关系和所有 family 历史。
|
|
185
|
+
await session.delete(row)
|
|
186
|
+
await session.flush()
|
|
187
|
+
|
|
188
|
+
async def set_password(
|
|
189
|
+
self,
|
|
190
|
+
user_id: UUID,
|
|
191
|
+
password_hash: str,
|
|
192
|
+
concurrency_version: int,
|
|
193
|
+
*,
|
|
194
|
+
expected_password_hash: str | None = None,
|
|
195
|
+
) -> None:
|
|
196
|
+
async with self._provider.operation("set_password", write=True):
|
|
197
|
+
session = await self._provider.get_session()
|
|
198
|
+
row = await self._require_user(user_id, for_update=True)
|
|
199
|
+
_check_version(row.concurrency_version, concurrency_version)
|
|
200
|
+
if expected_password_hash is not None and row.password_hash != expected_password_hash:
|
|
201
|
+
raise OptimisticConcurrencyError
|
|
202
|
+
row.password_hash = password_hash
|
|
203
|
+
row.failed_access_count = 0
|
|
204
|
+
row.lockout_end = None
|
|
205
|
+
row.concurrency_version += 1
|
|
206
|
+
await _revoke_user_sessions(session, user_id, now=datetime.now(UTC))
|
|
207
|
+
await session.flush()
|
|
208
|
+
|
|
209
|
+
async def create_role(self, role: IdentityRole) -> None:
|
|
210
|
+
async with self._provider.operation("create_role", write=True):
|
|
211
|
+
session = await self._provider.get_session()
|
|
212
|
+
session.add(
|
|
213
|
+
RoleRow(
|
|
214
|
+
role_id=role.role_id,
|
|
215
|
+
name=role.name,
|
|
216
|
+
normalized_name=role.normalized_name,
|
|
217
|
+
permission_version=role.permission_version,
|
|
218
|
+
concurrency_version=role.concurrency_version,
|
|
219
|
+
)
|
|
220
|
+
)
|
|
221
|
+
await self._flush_unique("role")
|
|
222
|
+
|
|
223
|
+
async def find_role_by_normalized_name(self, normalized_name: str) -> IdentityRole | None:
|
|
224
|
+
async with self._provider.operation("find_role_by_normalized_name"):
|
|
225
|
+
session = await self._provider.get_session()
|
|
226
|
+
row = await session.scalar(
|
|
227
|
+
select(RoleRow).where(RoleRow.normalized_name == normalized_name)
|
|
228
|
+
)
|
|
229
|
+
return None if row is None else _role_value(row)
|
|
230
|
+
|
|
231
|
+
async def get_role(self, role_id: UUID) -> RoleView:
|
|
232
|
+
async with self._provider.operation("get_role"):
|
|
233
|
+
items = await self._read_role_views(
|
|
234
|
+
select(RoleRow.role_id).where(RoleRow.role_id == role_id)
|
|
235
|
+
)
|
|
236
|
+
if not items:
|
|
237
|
+
raise IdentityResourceNotFoundError(resource="role")
|
|
238
|
+
return items[0]
|
|
239
|
+
|
|
240
|
+
async def _role_view(self, row: RoleRow) -> RoleView:
|
|
241
|
+
session = await self._provider.get_session()
|
|
242
|
+
permissions = tuple(
|
|
243
|
+
(
|
|
244
|
+
await session.scalars(
|
|
245
|
+
select(RolePermissionRow.permission_name)
|
|
246
|
+
.where(RolePermissionRow.role_id == row.role_id)
|
|
247
|
+
.order_by(RolePermissionRow.permission_name)
|
|
248
|
+
)
|
|
249
|
+
).all()
|
|
250
|
+
)
|
|
251
|
+
return RoleView(row.role_id, row.name, permissions, row.concurrency_version)
|
|
252
|
+
|
|
253
|
+
async def list_roles(self, query: PagedResultRequestDto) -> PagedResult[RoleView]:
|
|
254
|
+
async with self._provider.operation("list_roles"):
|
|
255
|
+
session = await self._provider.get_session()
|
|
256
|
+
total = await session.scalar(select(func.count()).select_from(RoleRow))
|
|
257
|
+
identifiers = (
|
|
258
|
+
select(RoleRow.role_id)
|
|
259
|
+
.order_by(RoleRow.normalized_name, RoleRow.role_id)
|
|
260
|
+
.offset(query.skip_count)
|
|
261
|
+
.limit(query.max_result_count)
|
|
262
|
+
)
|
|
263
|
+
items = await self._read_role_views(identifiers)
|
|
264
|
+
return PagedResult[RoleView](items=items, total_count=total or 0)
|
|
265
|
+
|
|
266
|
+
async def _read_role_views(self, identifiers: Select[tuple[UUID]]) -> list[RoleView]:
|
|
267
|
+
session = await self._provider.get_session()
|
|
268
|
+
permissions = (
|
|
269
|
+
select(func.array_agg(RolePermissionRow.permission_name))
|
|
270
|
+
.where(RolePermissionRow.role_id == RoleRow.role_id)
|
|
271
|
+
.correlate(RoleRow)
|
|
272
|
+
.scalar_subquery()
|
|
273
|
+
)
|
|
274
|
+
rows = (
|
|
275
|
+
await session.execute(
|
|
276
|
+
select(RoleRow, permissions)
|
|
277
|
+
.where(RoleRow.role_id.in_(identifiers))
|
|
278
|
+
.order_by(RoleRow.normalized_name, RoleRow.role_id)
|
|
279
|
+
.execution_options(populate_existing=True)
|
|
280
|
+
)
|
|
281
|
+
).all()
|
|
282
|
+
return [
|
|
283
|
+
RoleView(row.role_id, row.name, tuple(sorted(names or ())), row.concurrency_version)
|
|
284
|
+
for row, names in rows
|
|
285
|
+
]
|
|
286
|
+
|
|
287
|
+
async def update_role(self, role_id: UUID, command: UpdateRoleCommand) -> RoleView:
|
|
288
|
+
async with self._provider.operation("update_role", write=True):
|
|
289
|
+
row = await self._require_role(role_id, for_update=True)
|
|
290
|
+
role = _role_value(row).renamed(command.name, command.concurrency_version)
|
|
291
|
+
row.normalized_name = role.normalized_name
|
|
292
|
+
row.name = role.name
|
|
293
|
+
row.concurrency_version = role.concurrency_version
|
|
294
|
+
await self._flush_unique("role")
|
|
295
|
+
return await self._role_view(row)
|
|
296
|
+
|
|
297
|
+
async def delete_role(self, role_id: UUID, concurrency_version: int) -> None:
|
|
298
|
+
async with self._provider.operation("delete_role", write=True):
|
|
299
|
+
session = await self._provider.get_session()
|
|
300
|
+
role = await self._require_role(role_id, for_update=True)
|
|
301
|
+
_check_version(role.concurrency_version, concurrency_version)
|
|
302
|
+
user_ids = tuple(
|
|
303
|
+
(
|
|
304
|
+
await session.scalars(
|
|
305
|
+
select(UserRoleRow.user_id)
|
|
306
|
+
.where(UserRoleRow.role_id == role_id)
|
|
307
|
+
.order_by(UserRoleRow.user_id)
|
|
308
|
+
)
|
|
309
|
+
).all()
|
|
310
|
+
)
|
|
311
|
+
# 和分配保持 role -> user 顺序;级联删除也是用户关系的修改,必须推进其版本。
|
|
312
|
+
for user_id in user_ids:
|
|
313
|
+
await _lock_identity_user(session, user_id)
|
|
314
|
+
user = await session.scalar(
|
|
315
|
+
select(self._users.model)
|
|
316
|
+
.where(self._users.model.user_id == user_id)
|
|
317
|
+
.with_for_update()
|
|
318
|
+
.execution_options(populate_existing=True)
|
|
319
|
+
)
|
|
320
|
+
if user is not None:
|
|
321
|
+
user.concurrency_version += 1
|
|
322
|
+
user.permission_version += 1
|
|
323
|
+
await session.delete(role)
|
|
324
|
+
await session.flush()
|
|
325
|
+
|
|
326
|
+
async def set_user_roles(
|
|
327
|
+
self, user_id: UUID, role_ids: tuple[UUID, ...], concurrency_version: int
|
|
328
|
+
) -> UserRolesView:
|
|
329
|
+
async with self._provider.operation("set_user_roles", write=True):
|
|
330
|
+
session = await self._provider.get_session()
|
|
331
|
+
requested = frozenset(role_ids)
|
|
332
|
+
roles = tuple(
|
|
333
|
+
(
|
|
334
|
+
await session.scalars(
|
|
335
|
+
select(RoleRow)
|
|
336
|
+
.where(RoleRow.role_id.in_(requested))
|
|
337
|
+
.order_by(RoleRow.role_id)
|
|
338
|
+
.with_for_update()
|
|
339
|
+
)
|
|
340
|
+
).all()
|
|
341
|
+
)
|
|
342
|
+
if len(roles) != len(requested):
|
|
343
|
+
raise IdentityResourceNotFoundError(resource="role")
|
|
344
|
+
user = await self._require_user(user_id, for_update=True)
|
|
345
|
+
_check_version(user.concurrency_version, concurrency_version)
|
|
346
|
+
existing = tuple(
|
|
347
|
+
(
|
|
348
|
+
await session.scalars(
|
|
349
|
+
select(UserRoleRow).where(UserRoleRow.user_id == user_id).with_for_update()
|
|
350
|
+
)
|
|
351
|
+
).all()
|
|
352
|
+
)
|
|
353
|
+
current = frozenset(item.role_id for item in existing)
|
|
354
|
+
if current == requested:
|
|
355
|
+
return UserRolesView(
|
|
356
|
+
role_ids=tuple(sorted(current)), concurrency_version=user.concurrency_version
|
|
357
|
+
)
|
|
358
|
+
for item in existing:
|
|
359
|
+
if item.role_id not in requested:
|
|
360
|
+
await session.delete(item)
|
|
361
|
+
for role_id in sorted(requested - current):
|
|
362
|
+
session.add(UserRoleRow(user_id=user_id, role_id=role_id))
|
|
363
|
+
user.permission_version += 1
|
|
364
|
+
user.concurrency_version += 1
|
|
365
|
+
await session.flush()
|
|
366
|
+
return UserRolesView(
|
|
367
|
+
role_ids=tuple(sorted(requested)), concurrency_version=user.concurrency_version
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
async def set_role_permissions(
|
|
371
|
+
self, role_id: UUID, permissions: tuple[str, ...], concurrency_version: int
|
|
372
|
+
) -> RolePermissionsView:
|
|
373
|
+
async with self._provider.operation("set_role_permissions", write=True):
|
|
374
|
+
session = await self._provider.get_session()
|
|
375
|
+
role = await self._require_role(role_id, for_update=True)
|
|
376
|
+
_check_version(role.concurrency_version, concurrency_version)
|
|
377
|
+
requested = frozenset(permissions)
|
|
378
|
+
existing = tuple(
|
|
379
|
+
(
|
|
380
|
+
await session.scalars(
|
|
381
|
+
select(RolePermissionRow)
|
|
382
|
+
.where(RolePermissionRow.role_id == role_id)
|
|
383
|
+
.with_for_update()
|
|
384
|
+
)
|
|
385
|
+
).all()
|
|
386
|
+
)
|
|
387
|
+
current = frozenset(item.permission_name for item in existing)
|
|
388
|
+
if current == requested:
|
|
389
|
+
return RolePermissionsView(
|
|
390
|
+
permissions=tuple(sorted(current)), concurrency_version=role.concurrency_version
|
|
391
|
+
)
|
|
392
|
+
for item in existing:
|
|
393
|
+
if item.permission_name not in requested:
|
|
394
|
+
await session.delete(item)
|
|
395
|
+
for permission in sorted(requested - current):
|
|
396
|
+
session.add(RolePermissionRow(role_id=role_id, permission_name=permission))
|
|
397
|
+
role.permission_version += 1
|
|
398
|
+
role.concurrency_version += 1
|
|
399
|
+
await session.flush()
|
|
400
|
+
return RolePermissionsView(
|
|
401
|
+
permissions=tuple(sorted(requested)), concurrency_version=role.concurrency_version
|
|
402
|
+
)
|
|
403
|
+
|
|
404
|
+
async def _require_user(self, user_id: UUID, *, for_update: bool) -> UserRowValues:
|
|
405
|
+
session = await self._provider.get_session()
|
|
406
|
+
statement = select(self._users.model).where(self._users.model.user_id == user_id)
|
|
407
|
+
if for_update:
|
|
408
|
+
await _lock_identity_user(session, user_id)
|
|
409
|
+
statement = statement.with_for_update()
|
|
410
|
+
# 写锁后强制刷新 identity map,不拿事务内缓存的旧版本执行并发比较。
|
|
411
|
+
statement = statement.execution_options(populate_existing=True)
|
|
412
|
+
row = await session.scalar(statement)
|
|
413
|
+
if row is None:
|
|
414
|
+
raise IdentityResourceNotFoundError(resource="user")
|
|
415
|
+
return row
|
|
416
|
+
|
|
417
|
+
async def _require_role(self, role_id: UUID, *, for_update: bool) -> RoleRow:
|
|
418
|
+
session = await self._provider.get_session()
|
|
419
|
+
statement = select(RoleRow).where(RoleRow.role_id == role_id)
|
|
420
|
+
if for_update:
|
|
421
|
+
statement = statement.with_for_update()
|
|
422
|
+
statement = statement.execution_options(populate_existing=True)
|
|
423
|
+
row = await session.scalar(statement)
|
|
424
|
+
if row is None:
|
|
425
|
+
raise IdentityResourceNotFoundError(resource="role")
|
|
426
|
+
return row
|
|
427
|
+
|
|
428
|
+
async def _flush_unique(self, resource: str) -> None:
|
|
429
|
+
session = await self._provider.get_session()
|
|
430
|
+
try:
|
|
431
|
+
await session.flush()
|
|
432
|
+
except IntegrityError:
|
|
433
|
+
raise IdentityConflictError(resource=resource) from None
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
def _user_value(row: UserRowValues, extra_properties: ExtensionProperties) -> IdentityUser:
|
|
437
|
+
return IdentityUser(
|
|
438
|
+
user_id=row.user_id,
|
|
439
|
+
username=row.username,
|
|
440
|
+
normalized_username=row.normalized_username,
|
|
441
|
+
password_hash=row.password_hash,
|
|
442
|
+
is_active=row.is_active,
|
|
443
|
+
failed_access_count=row.failed_access_count,
|
|
444
|
+
lockout_end=row.lockout_end,
|
|
445
|
+
permission_version=row.permission_version,
|
|
446
|
+
concurrency_version=row.concurrency_version,
|
|
447
|
+
extra_properties=extra_properties,
|
|
448
|
+
)
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
def _role_value(row: RoleRow) -> IdentityRole:
|
|
452
|
+
return IdentityRole(
|
|
453
|
+
role_id=row.role_id,
|
|
454
|
+
name=row.name,
|
|
455
|
+
normalized_name=row.normalized_name,
|
|
456
|
+
permission_version=row.permission_version,
|
|
457
|
+
concurrency_version=row.concurrency_version,
|
|
458
|
+
)
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""凭据会话的统一锁序、撤销与期限判断;调用方拥有事务。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from hashlib import blake2b
|
|
7
|
+
from uuid import UUID
|
|
8
|
+
|
|
9
|
+
from sqlalchemy import func, select
|
|
10
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
11
|
+
|
|
12
|
+
from ..contracts import (
|
|
13
|
+
IdentityOptions,
|
|
14
|
+
)
|
|
15
|
+
from .models import (
|
|
16
|
+
RefreshSessionRow,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
_REFRESH_FAMILY_LOCK_PERSON = b"pyplat.rfam.v1"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
async def _revoke_family(
|
|
23
|
+
session: AsyncSession,
|
|
24
|
+
family_id: UUID,
|
|
25
|
+
*,
|
|
26
|
+
now: datetime,
|
|
27
|
+
) -> None:
|
|
28
|
+
rows = tuple(
|
|
29
|
+
(
|
|
30
|
+
await session.scalars(
|
|
31
|
+
select(RefreshSessionRow)
|
|
32
|
+
.where(RefreshSessionRow.family_id == family_id)
|
|
33
|
+
.with_for_update()
|
|
34
|
+
)
|
|
35
|
+
).all()
|
|
36
|
+
)
|
|
37
|
+
for row in rows:
|
|
38
|
+
if row.revoked_at is None:
|
|
39
|
+
row.revoked_at = now
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
async def _lock_refresh_family(session: AsyncSession, family_id: UUID) -> None:
|
|
43
|
+
# family 没有可锁的稳定 parent row;transaction advisory lock 在同一短事务内提供
|
|
44
|
+
# family -> token/family rows 的统一顺序,并随 commit/rollback 自动释放。
|
|
45
|
+
await session.scalar(select(func.pg_advisory_xact_lock(_refresh_family_lock_key(family_id))))
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
async def _lock_identity_user(session: AsyncSession, user_id: UUID) -> None:
|
|
49
|
+
# 登录创建新 family 与管理撤销共同持有稳定 user gate,阻止枚举 family 后漏掉新会话。
|
|
50
|
+
# 所有凭据/family 写入先取得此 gate;family 内仍是 advisory -> refresh rows。
|
|
51
|
+
digest = blake2b(user_id.bytes, digest_size=8, person=b"pyplat.user.v1").digest()
|
|
52
|
+
key = int.from_bytes(digest, byteorder="big", signed=True)
|
|
53
|
+
await session.scalar(select(func.pg_advisory_xact_lock(key)))
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
async def _revoke_user_sessions(session: AsyncSession, user_id: UUID, *, now: datetime) -> None:
|
|
57
|
+
# 调用方已持有 user gate;family 集合不会在此事务期间增长。
|
|
58
|
+
families = tuple(
|
|
59
|
+
(
|
|
60
|
+
await session.scalars(
|
|
61
|
+
select(RefreshSessionRow.family_id)
|
|
62
|
+
.where(RefreshSessionRow.user_id == user_id)
|
|
63
|
+
.distinct()
|
|
64
|
+
.order_by(RefreshSessionRow.family_id)
|
|
65
|
+
)
|
|
66
|
+
).all()
|
|
67
|
+
)
|
|
68
|
+
for family_id in families:
|
|
69
|
+
await _lock_refresh_family(session, family_id)
|
|
70
|
+
await _revoke_family(session, family_id, now=now)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _refresh_family_lock_key(family_id: UUID) -> int:
|
|
74
|
+
digest = blake2b(
|
|
75
|
+
family_id.bytes,
|
|
76
|
+
digest_size=8,
|
|
77
|
+
person=_REFRESH_FAMILY_LOCK_PERSON,
|
|
78
|
+
).digest()
|
|
79
|
+
return int.from_bytes(digest, byteorder="big", signed=True)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _session_is_active(row: RefreshSessionRow, now: datetime, options: IdentityOptions) -> bool:
|
|
83
|
+
return (
|
|
84
|
+
row.revoked_at is None
|
|
85
|
+
and row.replaced_by_session_id is None
|
|
86
|
+
and now - row.last_used_at < options.refresh_inactivity_lifetime
|
|
87
|
+
and now - row.family_created_at < options.refresh_absolute_lifetime
|
|
88
|
+
)
|