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
|
@@ -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:
|
|
@@ -3,20 +3,22 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
from collections.abc import Mapping
|
|
6
|
-
from dataclasses import dataclass
|
|
7
|
-
from datetime import datetime
|
|
6
|
+
from dataclasses import dataclass, field, replace
|
|
7
|
+
from datetime import datetime, timedelta
|
|
8
8
|
from enum import Enum
|
|
9
9
|
from typing import Annotated, Protocol
|
|
10
10
|
from uuid import UUID
|
|
11
11
|
|
|
12
12
|
from pydantic import BaseModel, ConfigDict, Field, SecretStr, field_validator, model_validator
|
|
13
13
|
|
|
14
|
-
from ..application_services import ApplicationServiceContract
|
|
14
|
+
from ..application_services import ApplicationServiceContract, PagedResult, PagedResultRequestDto
|
|
15
15
|
from ..authorization import (
|
|
16
16
|
CurrentUser,
|
|
17
17
|
PermissionName,
|
|
18
18
|
RolePermissionVersion,
|
|
19
19
|
)
|
|
20
|
+
from ..domain import OptimisticConcurrencyError
|
|
21
|
+
from ..extensions.contracts import ExtensibleModel, ExtensionProperties, ExtensionTarget
|
|
20
22
|
from ..options import BaseOptions
|
|
21
23
|
|
|
22
24
|
IDENTITY_USERS_MANAGE = PermissionName("identity.users.manage")
|
|
@@ -28,6 +30,11 @@ class IdentityOptions(BaseOptions):
|
|
|
28
30
|
audience: str
|
|
29
31
|
active_kid: str
|
|
30
32
|
keys: Mapping[str, SecretStr]
|
|
33
|
+
access_token_lifetime: timedelta = Field(default=timedelta(minutes=15), ge=timedelta(seconds=1))
|
|
34
|
+
refresh_inactivity_lifetime: timedelta = Field(default=timedelta(days=30), gt=timedelta(0))
|
|
35
|
+
refresh_absolute_lifetime: timedelta = Field(default=timedelta(days=90), gt=timedelta(0))
|
|
36
|
+
failed_attempt_limit: int = Field(default=5, ge=1)
|
|
37
|
+
lockout_duration: timedelta = Field(default=timedelta(minutes=15), ge=timedelta(seconds=1))
|
|
31
38
|
seed_admin_username: str | None = None
|
|
32
39
|
seed_admin_password: SecretStr | None = None
|
|
33
40
|
|
|
@@ -80,26 +87,80 @@ class RevokeTokenCommand(BaseModel):
|
|
|
80
87
|
refresh_token: SecretStr
|
|
81
88
|
|
|
82
89
|
|
|
83
|
-
class CreateUserCommand(
|
|
90
|
+
class CreateUserCommand(ExtensibleModel):
|
|
84
91
|
model_config = ConfigDict(frozen=True)
|
|
85
92
|
|
|
86
93
|
username: Annotated[str, Field(min_length=1, max_length=120)]
|
|
87
94
|
password: Annotated[SecretStr, Field(min_length=12, max_length=1024)]
|
|
88
95
|
|
|
96
|
+
@field_validator("username")
|
|
97
|
+
@classmethod
|
|
98
|
+
def _validate_username(cls, value: str) -> str:
|
|
99
|
+
_normalize_name(value, role="username")
|
|
100
|
+
return value.strip()
|
|
101
|
+
|
|
89
102
|
|
|
90
103
|
class CreateRoleCommand(BaseModel):
|
|
91
104
|
model_config = ConfigDict(frozen=True)
|
|
92
105
|
|
|
93
106
|
name: Annotated[str, Field(min_length=1, max_length=120)]
|
|
94
107
|
|
|
108
|
+
@field_validator("name")
|
|
109
|
+
@classmethod
|
|
110
|
+
def _validate_name(cls, value: str) -> str:
|
|
111
|
+
_normalize_name(value, role="role name")
|
|
112
|
+
return value.strip()
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class IdentityConcurrencyCommand(BaseModel):
|
|
116
|
+
model_config = ConfigDict(frozen=True)
|
|
117
|
+
|
|
118
|
+
concurrency_version: Annotated[int, Field(ge=0)]
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class UpdateUserCommand(ExtensibleModel):
|
|
122
|
+
model_config = ConfigDict(frozen=True)
|
|
123
|
+
|
|
124
|
+
username: Annotated[str, Field(min_length=1, max_length=120)]
|
|
125
|
+
concurrency_version: Annotated[int, Field(ge=0)]
|
|
126
|
+
|
|
127
|
+
@field_validator("username")
|
|
128
|
+
@classmethod
|
|
129
|
+
def _validate_username(cls, value: str) -> str:
|
|
130
|
+
_normalize_name(value, role="username")
|
|
131
|
+
return value.strip()
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class SetUserActiveCommand(IdentityConcurrencyCommand):
|
|
135
|
+
is_active: bool
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class UpdateRoleCommand(IdentityConcurrencyCommand):
|
|
139
|
+
name: Annotated[str, Field(min_length=1, max_length=120)]
|
|
140
|
+
|
|
141
|
+
@field_validator("name")
|
|
142
|
+
@classmethod
|
|
143
|
+
def _validate_name(cls, value: str) -> str:
|
|
144
|
+
_normalize_name(value, role="role name")
|
|
145
|
+
return value.strip()
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
class ChangePasswordCommand(IdentityConcurrencyCommand):
|
|
149
|
+
old_password: SecretStr
|
|
150
|
+
new_password: Annotated[SecretStr, Field(min_length=12, max_length=1024)]
|
|
95
151
|
|
|
96
|
-
|
|
152
|
+
|
|
153
|
+
class SetPasswordCommand(IdentityConcurrencyCommand):
|
|
154
|
+
new_password: Annotated[SecretStr, Field(min_length=12, max_length=1024)]
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
class SetUserRolesCommand(IdentityConcurrencyCommand):
|
|
97
158
|
model_config = ConfigDict(frozen=True)
|
|
98
159
|
|
|
99
160
|
role_ids: tuple[UUID, ...]
|
|
100
161
|
|
|
101
162
|
|
|
102
|
-
class SetRolePermissionsCommand(
|
|
163
|
+
class SetRolePermissionsCommand(IdentityConcurrencyCommand):
|
|
103
164
|
model_config = ConfigDict(frozen=True)
|
|
104
165
|
|
|
105
166
|
permissions: tuple[str, ...]
|
|
@@ -113,12 +174,12 @@ class TokenPair:
|
|
|
113
174
|
expires_in: int
|
|
114
175
|
|
|
115
176
|
|
|
116
|
-
|
|
117
|
-
class UserView:
|
|
177
|
+
class UserView(ExtensibleModel):
|
|
118
178
|
user_id: UUID
|
|
119
179
|
username: str
|
|
120
180
|
is_active: bool
|
|
121
181
|
role_ids: tuple[UUID, ...]
|
|
182
|
+
concurrency_version: int
|
|
122
183
|
|
|
123
184
|
|
|
124
185
|
@dataclass(frozen=True, slots=True)
|
|
@@ -126,6 +187,17 @@ class RoleView:
|
|
|
126
187
|
role_id: UUID
|
|
127
188
|
name: str
|
|
128
189
|
permissions: tuple[str, ...]
|
|
190
|
+
concurrency_version: int
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
class UserRolesView(BaseModel):
|
|
194
|
+
role_ids: tuple[UUID, ...]
|
|
195
|
+
concurrency_version: int
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
class RolePermissionsView(BaseModel):
|
|
199
|
+
permissions: tuple[str, ...]
|
|
200
|
+
concurrency_version: int
|
|
129
201
|
|
|
130
202
|
|
|
131
203
|
@dataclass(frozen=True, slots=True)
|
|
@@ -133,6 +205,7 @@ class CurrentUserView:
|
|
|
133
205
|
user_id: UUID
|
|
134
206
|
session_id: UUID
|
|
135
207
|
username: str
|
|
208
|
+
concurrency_version: int
|
|
136
209
|
|
|
137
210
|
|
|
138
211
|
@dataclass(frozen=True, slots=True)
|
|
@@ -145,6 +218,30 @@ class IdentityUser:
|
|
|
145
218
|
failed_access_count: int
|
|
146
219
|
lockout_end: datetime | None
|
|
147
220
|
permission_version: int
|
|
221
|
+
extra_properties: ExtensionProperties = field(default_factory=ExtensionProperties)
|
|
222
|
+
concurrency_version: int = 0
|
|
223
|
+
|
|
224
|
+
def renamed(self, username: str, expected_version: int) -> IdentityUser:
|
|
225
|
+
_check_version(self.concurrency_version, expected_version)
|
|
226
|
+
return replace(
|
|
227
|
+
self,
|
|
228
|
+
username=username.strip(),
|
|
229
|
+
normalized_username=_normalize_name(username, role="username"),
|
|
230
|
+
concurrency_version=self.concurrency_version + 1,
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
def with_active(self, is_active: bool, expected_version: int) -> IdentityUser:
|
|
234
|
+
_check_version(self.concurrency_version, expected_version)
|
|
235
|
+
return replace(
|
|
236
|
+
self,
|
|
237
|
+
is_active=is_active,
|
|
238
|
+
concurrency_version=self.concurrency_version + (self.is_active != is_active),
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
IDENTITY_USER_EXTENSION = ExtensionTarget(
|
|
243
|
+
"identity.user", (CreateUserCommand, UpdateUserCommand, UserView), IdentityUser
|
|
244
|
+
)
|
|
148
245
|
|
|
149
246
|
|
|
150
247
|
@dataclass(frozen=True, slots=True)
|
|
@@ -153,6 +250,16 @@ class IdentityRole:
|
|
|
153
250
|
name: str
|
|
154
251
|
normalized_name: str
|
|
155
252
|
permission_version: int
|
|
253
|
+
concurrency_version: int = 0
|
|
254
|
+
|
|
255
|
+
def renamed(self, name: str, expected_version: int) -> IdentityRole:
|
|
256
|
+
_check_version(self.concurrency_version, expected_version)
|
|
257
|
+
return replace(
|
|
258
|
+
self,
|
|
259
|
+
name=name.strip(),
|
|
260
|
+
normalized_name=_normalize_name(name, role="role name"),
|
|
261
|
+
concurrency_version=self.concurrency_version + 1,
|
|
262
|
+
)
|
|
156
263
|
|
|
157
264
|
|
|
158
265
|
@dataclass(frozen=True, slots=True)
|
|
@@ -188,17 +295,46 @@ class IdentityRepository(Protocol):
|
|
|
188
295
|
|
|
189
296
|
async def create_user(self, user: IdentityUser) -> None: ...
|
|
190
297
|
|
|
191
|
-
async def
|
|
298
|
+
async def get_user(self, user_id: UUID) -> IdentityUser: ...
|
|
299
|
+
|
|
300
|
+
async def get_user_view(self, user_id: UUID) -> UserView: ...
|
|
301
|
+
|
|
302
|
+
async def list_users(self, query: PagedResultRequestDto) -> PagedResult[UserView]: ...
|
|
303
|
+
|
|
304
|
+
async def update_user(self, user_id: UUID, command: UpdateUserCommand) -> UserView: ...
|
|
305
|
+
|
|
306
|
+
async def set_user_active(self, user_id: UUID, command: SetUserActiveCommand) -> UserView: ...
|
|
307
|
+
|
|
308
|
+
async def delete_user(self, user_id: UUID, concurrency_version: int) -> None: ...
|
|
309
|
+
|
|
310
|
+
async def set_password(
|
|
311
|
+
self,
|
|
312
|
+
user_id: UUID,
|
|
313
|
+
password_hash: str,
|
|
314
|
+
concurrency_version: int,
|
|
315
|
+
*,
|
|
316
|
+
expected_password_hash: str | None = None,
|
|
317
|
+
) -> None: ...
|
|
192
318
|
|
|
193
319
|
async def create_role(self, role: IdentityRole) -> None: ...
|
|
194
320
|
|
|
195
321
|
async def find_role_by_normalized_name(self, normalized_name: str) -> IdentityRole | None: ...
|
|
196
322
|
|
|
197
|
-
async def
|
|
323
|
+
async def get_role(self, role_id: UUID) -> RoleView: ...
|
|
324
|
+
|
|
325
|
+
async def list_roles(self, query: PagedResultRequestDto) -> PagedResult[RoleView]: ...
|
|
326
|
+
|
|
327
|
+
async def update_role(self, role_id: UUID, command: UpdateRoleCommand) -> RoleView: ...
|
|
198
328
|
|
|
199
|
-
async def
|
|
329
|
+
async def delete_role(self, role_id: UUID, concurrency_version: int) -> None: ...
|
|
200
330
|
|
|
201
|
-
async def
|
|
331
|
+
async def set_user_roles(
|
|
332
|
+
self, user_id: UUID, role_ids: tuple[UUID, ...], concurrency_version: int
|
|
333
|
+
) -> UserRolesView: ...
|
|
334
|
+
|
|
335
|
+
async def set_role_permissions(
|
|
336
|
+
self, role_id: UUID, permissions: tuple[str, ...], concurrency_version: int
|
|
337
|
+
) -> RolePermissionsView: ...
|
|
202
338
|
|
|
203
339
|
|
|
204
340
|
class IdentityAccessStore(Protocol):
|
|
@@ -259,21 +395,45 @@ class AuthenticationApplicationService(ApplicationServiceContract, Protocol):
|
|
|
259
395
|
|
|
260
396
|
async def me(self) -> CurrentUserView: ...
|
|
261
397
|
|
|
398
|
+
async def change_password(self, command: ChangePasswordCommand) -> None: ...
|
|
399
|
+
|
|
262
400
|
|
|
263
401
|
class IdentityManagementApplicationService(ApplicationServiceContract, Protocol):
|
|
264
402
|
async def create_user(self, command: CreateUserCommand) -> UserView: ...
|
|
265
403
|
|
|
266
|
-
async def
|
|
404
|
+
async def get_user(self, user_id: UUID) -> UserView: ...
|
|
405
|
+
|
|
406
|
+
async def get_users(self, query: PagedResultRequestDto) -> PagedResult[UserView]: ...
|
|
407
|
+
|
|
408
|
+
async def update_user(self, user_id: UUID, command: UpdateUserCommand) -> UserView: ...
|
|
409
|
+
|
|
410
|
+
async def set_user_active(self, user_id: UUID, command: SetUserActiveCommand) -> UserView: ...
|
|
411
|
+
|
|
412
|
+
async def delete_user(self, user_id: UUID, command: IdentityConcurrencyCommand) -> None: ...
|
|
413
|
+
|
|
414
|
+
async def set_password(self, user_id: UUID, command: SetPasswordCommand) -> None: ...
|
|
267
415
|
|
|
268
416
|
async def create_role(self, command: CreateRoleCommand) -> RoleView: ...
|
|
269
417
|
|
|
270
|
-
async def
|
|
418
|
+
async def get_role(self, role_id: UUID) -> RoleView: ...
|
|
271
419
|
|
|
272
|
-
async def
|
|
420
|
+
async def get_roles(self, query: PagedResultRequestDto) -> PagedResult[RoleView]: ...
|
|
421
|
+
|
|
422
|
+
async def update_role(self, role_id: UUID, command: UpdateRoleCommand) -> RoleView: ...
|
|
423
|
+
|
|
424
|
+
async def delete_role(self, role_id: UUID, command: IdentityConcurrencyCommand) -> None: ...
|
|
425
|
+
|
|
426
|
+
async def get_user_roles(self, user_id: UUID) -> UserRolesView: ...
|
|
427
|
+
|
|
428
|
+
async def get_role_permissions(self, role_id: UUID) -> RolePermissionsView: ...
|
|
429
|
+
|
|
430
|
+
async def set_user_roles(
|
|
431
|
+
self, user_id: UUID, command: SetUserRolesCommand
|
|
432
|
+
) -> UserRolesView: ...
|
|
273
433
|
|
|
274
434
|
async def set_role_permissions(
|
|
275
435
|
self, role_id: UUID, command: SetRolePermissionsCommand
|
|
276
|
-
) ->
|
|
436
|
+
) -> RolePermissionsView: ...
|
|
277
437
|
|
|
278
438
|
|
|
279
439
|
def _normalize_name(value: str, *, role: str) -> str:
|
|
@@ -281,3 +441,8 @@ def _normalize_name(value: str, *, role: str) -> str:
|
|
|
281
441
|
if not normalized or len(normalized) > 120:
|
|
282
442
|
raise ValueError(f"{role} must contain between 1 and 120 characters")
|
|
283
443
|
return normalized
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
def _check_version(actual: int, expected: int) -> None:
|
|
447
|
+
if actual != expected:
|
|
448
|
+
raise OptimisticConcurrencyError
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
from ..fastapi import ApplicationServiceRouteOverride, exposes_application_services
|
|
5
|
+
from ..fastapi import ApplicationServiceRouteOverride, FastApiModule, exposes_application_services
|
|
6
6
|
from ..modularity import AppModule
|
|
7
7
|
from .contracts import AuthenticationApplicationService, IdentityManagementApplicationService
|
|
8
8
|
from .module import IdentityModule
|
|
@@ -12,6 +12,83 @@ from .module import IdentityModule
|
|
|
12
12
|
IdentityModule,
|
|
13
13
|
root_path="identity",
|
|
14
14
|
overrides=(
|
|
15
|
+
ApplicationServiceRouteOverride(
|
|
16
|
+
AuthenticationApplicationService,
|
|
17
|
+
method=AuthenticationApplicationService.change_password,
|
|
18
|
+
path="/auth/password",
|
|
19
|
+
verb="PUT",
|
|
20
|
+
operation_id="identity_change_password",
|
|
21
|
+
),
|
|
22
|
+
ApplicationServiceRouteOverride(
|
|
23
|
+
IdentityManagementApplicationService,
|
|
24
|
+
method=IdentityManagementApplicationService.get_user,
|
|
25
|
+
path="/identity/users/{user_id}",
|
|
26
|
+
verb="GET",
|
|
27
|
+
operation_id="identity_get_user",
|
|
28
|
+
),
|
|
29
|
+
ApplicationServiceRouteOverride(
|
|
30
|
+
IdentityManagementApplicationService,
|
|
31
|
+
method=IdentityManagementApplicationService.update_user,
|
|
32
|
+
path="/identity/users/{user_id}",
|
|
33
|
+
verb="PUT",
|
|
34
|
+
operation_id="identity_update_user",
|
|
35
|
+
),
|
|
36
|
+
ApplicationServiceRouteOverride(
|
|
37
|
+
IdentityManagementApplicationService,
|
|
38
|
+
method=IdentityManagementApplicationService.delete_user,
|
|
39
|
+
path="/identity/users/{user_id}",
|
|
40
|
+
verb="DELETE",
|
|
41
|
+
operation_id="identity_delete_user",
|
|
42
|
+
),
|
|
43
|
+
ApplicationServiceRouteOverride(
|
|
44
|
+
IdentityManagementApplicationService,
|
|
45
|
+
method=IdentityManagementApplicationService.set_user_active,
|
|
46
|
+
path="/identity/users/{user_id}/active",
|
|
47
|
+
verb="PUT",
|
|
48
|
+
operation_id="identity_set_user_active",
|
|
49
|
+
),
|
|
50
|
+
ApplicationServiceRouteOverride(
|
|
51
|
+
IdentityManagementApplicationService,
|
|
52
|
+
method=IdentityManagementApplicationService.set_password,
|
|
53
|
+
path="/identity/users/{user_id}/password",
|
|
54
|
+
verb="PUT",
|
|
55
|
+
operation_id="identity_set_password",
|
|
56
|
+
),
|
|
57
|
+
ApplicationServiceRouteOverride(
|
|
58
|
+
IdentityManagementApplicationService,
|
|
59
|
+
method=IdentityManagementApplicationService.get_user_roles,
|
|
60
|
+
path="/identity/users/{user_id}/roles",
|
|
61
|
+
verb="GET",
|
|
62
|
+
operation_id="identity_get_user_roles",
|
|
63
|
+
),
|
|
64
|
+
ApplicationServiceRouteOverride(
|
|
65
|
+
IdentityManagementApplicationService,
|
|
66
|
+
method=IdentityManagementApplicationService.get_role,
|
|
67
|
+
path="/identity/roles/{role_id}",
|
|
68
|
+
verb="GET",
|
|
69
|
+
operation_id="identity_get_role",
|
|
70
|
+
),
|
|
71
|
+
ApplicationServiceRouteOverride(
|
|
72
|
+
IdentityManagementApplicationService,
|
|
73
|
+
method=IdentityManagementApplicationService.update_role,
|
|
74
|
+
path="/identity/roles/{role_id}",
|
|
75
|
+
verb="PUT",
|
|
76
|
+
operation_id="identity_update_role",
|
|
77
|
+
),
|
|
78
|
+
ApplicationServiceRouteOverride(
|
|
79
|
+
IdentityManagementApplicationService,
|
|
80
|
+
method=IdentityManagementApplicationService.delete_role,
|
|
81
|
+
path="/identity/roles/{role_id}",
|
|
82
|
+
verb="DELETE",
|
|
83
|
+
operation_id="identity_delete_role",
|
|
84
|
+
),
|
|
85
|
+
ApplicationServiceRouteOverride(
|
|
86
|
+
IdentityManagementApplicationService,
|
|
87
|
+
method=IdentityManagementApplicationService.get_role_permissions,
|
|
88
|
+
path="/identity/roles/{role_id}/permissions",
|
|
89
|
+
verb="GET",
|
|
90
|
+
operation_id="identity_get_role_permissions",
|
|
91
|
+
),
|
|
15
92
|
ApplicationServiceRouteOverride(
|
|
16
93
|
AuthenticationApplicationService,
|
|
17
94
|
method=AuthenticationApplicationService.login,
|
|
@@ -87,4 +164,4 @@ from .module import IdentityModule
|
|
|
87
164
|
),
|
|
88
165
|
)
|
|
89
166
|
class IdentityHttpApiModule(AppModule):
|
|
90
|
-
dependencies = (
|
|
167
|
+
dependencies = (FastApiModule,)
|