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
|
@@ -3,231 +3,50 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
from datetime import datetime, timedelta
|
|
6
|
-
from hashlib import blake2b
|
|
7
6
|
from uuid import UUID
|
|
8
7
|
|
|
9
|
-
from sqlalchemy import
|
|
10
|
-
from sqlalchemy.exc import IntegrityError
|
|
11
|
-
from sqlalchemy.ext.asyncio import AsyncSession
|
|
8
|
+
from sqlalchemy import select
|
|
12
9
|
|
|
13
10
|
from ...authorization import CurrentUser, RolePermissionVersion
|
|
14
|
-
from ...
|
|
11
|
+
from ...options import Options
|
|
12
|
+
from ...sqlalchemy import FrameworkAsyncSession, SqlAlchemyModelCatalog
|
|
15
13
|
from ...sqlalchemy.runtime import _SqlAlchemyEngineRegistry
|
|
16
14
|
from ..contracts import (
|
|
17
|
-
|
|
15
|
+
IDENTITY_USER_EXTENSION,
|
|
16
|
+
IdentityOptions,
|
|
18
17
|
IdentityUser,
|
|
19
18
|
RefreshRotationResult,
|
|
20
19
|
RefreshRotationStatus,
|
|
21
20
|
RefreshSession,
|
|
22
|
-
RoleView,
|
|
23
|
-
UserView,
|
|
24
21
|
)
|
|
25
|
-
from ..errors import IdentityConflictError, IdentityResourceNotFoundError
|
|
26
|
-
from ..tokens import REFRESH_ABSOLUTE_LIFETIME, REFRESH_INACTIVITY_LIFETIME
|
|
27
22
|
from .models import (
|
|
28
23
|
RefreshSessionRow,
|
|
29
24
|
RolePermissionRow,
|
|
30
25
|
RoleRow,
|
|
31
26
|
UserRoleRow,
|
|
32
|
-
|
|
27
|
+
UserRowValues,
|
|
28
|
+
)
|
|
29
|
+
from .repository import _user_value
|
|
30
|
+
from .session_security import (
|
|
31
|
+
_lock_identity_user,
|
|
32
|
+
_lock_refresh_family,
|
|
33
|
+
_revoke_family,
|
|
34
|
+
_session_is_active,
|
|
33
35
|
)
|
|
34
|
-
|
|
35
|
-
_REFRESH_FAMILY_LOCK_PERSON = b"pyplat.rfam.v1"
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
class SqlAlchemyIdentityRepository:
|
|
39
|
-
__slots__ = ("_provider",)
|
|
40
|
-
|
|
41
|
-
def __init__(self, provider: SqlAlchemySessionProvider) -> None:
|
|
42
|
-
self._provider = provider
|
|
43
|
-
|
|
44
|
-
async def find_user_by_normalized_username(
|
|
45
|
-
self,
|
|
46
|
-
normalized_username: str,
|
|
47
|
-
) -> IdentityUser | None:
|
|
48
|
-
async with self._provider.operation("find_user_by_normalized_username"):
|
|
49
|
-
session = await self._provider.get_session()
|
|
50
|
-
statement = select(UserRow).where(UserRow.normalized_username == normalized_username)
|
|
51
|
-
row = await session.scalar(statement)
|
|
52
|
-
return None if row is None else _user_value(row)
|
|
53
|
-
|
|
54
|
-
async def create_user(self, user: IdentityUser) -> None:
|
|
55
|
-
async with self._provider.operation("create_user"):
|
|
56
|
-
session = await self._provider.get_session()
|
|
57
|
-
session.add(
|
|
58
|
-
UserRow(
|
|
59
|
-
user_id=user.user_id,
|
|
60
|
-
username=user.username,
|
|
61
|
-
normalized_username=user.normalized_username,
|
|
62
|
-
password_hash=user.password_hash,
|
|
63
|
-
is_active=user.is_active,
|
|
64
|
-
failed_access_count=user.failed_access_count,
|
|
65
|
-
lockout_end=user.lockout_end,
|
|
66
|
-
permission_version=user.permission_version,
|
|
67
|
-
)
|
|
68
|
-
)
|
|
69
|
-
await self._flush_unique("user")
|
|
70
|
-
|
|
71
|
-
async def list_users(self) -> tuple[UserView, ...]:
|
|
72
|
-
async with self._provider.operation("list_users"):
|
|
73
|
-
session = await self._provider.get_session()
|
|
74
|
-
rows = tuple(
|
|
75
|
-
(await session.scalars(select(UserRow).order_by(UserRow.normalized_username))).all()
|
|
76
|
-
)
|
|
77
|
-
role_rows = (
|
|
78
|
-
await session.execute(
|
|
79
|
-
select(UserRoleRow.user_id, UserRoleRow.role_id).order_by(
|
|
80
|
-
UserRoleRow.user_id, UserRoleRow.role_id
|
|
81
|
-
)
|
|
82
|
-
)
|
|
83
|
-
).all()
|
|
84
|
-
roles_by_user: dict[UUID, list[UUID]] = {}
|
|
85
|
-
for user_id, role_id in role_rows:
|
|
86
|
-
roles_by_user.setdefault(user_id, []).append(role_id)
|
|
87
|
-
return tuple(
|
|
88
|
-
UserView(
|
|
89
|
-
user_id=row.user_id,
|
|
90
|
-
username=row.username,
|
|
91
|
-
is_active=row.is_active,
|
|
92
|
-
role_ids=tuple(roles_by_user.get(row.user_id, ())),
|
|
93
|
-
)
|
|
94
|
-
for row in rows
|
|
95
|
-
)
|
|
96
|
-
|
|
97
|
-
async def create_role(self, role: IdentityRole) -> None:
|
|
98
|
-
async with self._provider.operation("create_role"):
|
|
99
|
-
session = await self._provider.get_session()
|
|
100
|
-
session.add(
|
|
101
|
-
RoleRow(
|
|
102
|
-
role_id=role.role_id,
|
|
103
|
-
name=role.name,
|
|
104
|
-
normalized_name=role.normalized_name,
|
|
105
|
-
permission_version=role.permission_version,
|
|
106
|
-
)
|
|
107
|
-
)
|
|
108
|
-
await self._flush_unique("role")
|
|
109
|
-
|
|
110
|
-
async def find_role_by_normalized_name(self, normalized_name: str) -> IdentityRole | None:
|
|
111
|
-
async with self._provider.operation("find_role_by_normalized_name"):
|
|
112
|
-
session = await self._provider.get_session()
|
|
113
|
-
row = await session.scalar(
|
|
114
|
-
select(RoleRow).where(RoleRow.normalized_name == normalized_name)
|
|
115
|
-
)
|
|
116
|
-
return None if row is None else _role_value(row)
|
|
117
|
-
|
|
118
|
-
async def list_roles(self) -> tuple[RoleView, ...]:
|
|
119
|
-
async with self._provider.operation("list_roles"):
|
|
120
|
-
session = await self._provider.get_session()
|
|
121
|
-
roles = tuple(
|
|
122
|
-
(await session.scalars(select(RoleRow).order_by(RoleRow.normalized_name))).all()
|
|
123
|
-
)
|
|
124
|
-
permission_rows = (
|
|
125
|
-
await session.execute(
|
|
126
|
-
select(RolePermissionRow.role_id, RolePermissionRow.permission_name).order_by(
|
|
127
|
-
RolePermissionRow.role_id, RolePermissionRow.permission_name
|
|
128
|
-
)
|
|
129
|
-
)
|
|
130
|
-
).all()
|
|
131
|
-
permissions_by_role: dict[UUID, list[str]] = {}
|
|
132
|
-
for role_id, permission_name in permission_rows:
|
|
133
|
-
permissions_by_role.setdefault(role_id, []).append(permission_name)
|
|
134
|
-
return tuple(
|
|
135
|
-
RoleView(
|
|
136
|
-
role_id=row.role_id,
|
|
137
|
-
name=row.name,
|
|
138
|
-
permissions=tuple(permissions_by_role.get(row.role_id, ())),
|
|
139
|
-
)
|
|
140
|
-
for row in roles
|
|
141
|
-
)
|
|
142
|
-
|
|
143
|
-
async def set_user_roles(self, user_id: UUID, role_ids: tuple[UUID, ...]) -> None:
|
|
144
|
-
async with self._provider.operation("set_user_roles"):
|
|
145
|
-
session = await self._provider.get_session()
|
|
146
|
-
user = await self._require_user(user_id, for_update=True)
|
|
147
|
-
requested = frozenset(role_ids)
|
|
148
|
-
roles = tuple(
|
|
149
|
-
(
|
|
150
|
-
await session.scalars(
|
|
151
|
-
select(RoleRow).where(RoleRow.role_id.in_(requested)).with_for_update()
|
|
152
|
-
)
|
|
153
|
-
).all()
|
|
154
|
-
)
|
|
155
|
-
if len(roles) != len(requested):
|
|
156
|
-
raise IdentityResourceNotFoundError(resource="role")
|
|
157
|
-
existing = tuple(
|
|
158
|
-
(
|
|
159
|
-
await session.scalars(
|
|
160
|
-
select(UserRoleRow).where(UserRoleRow.user_id == user_id).with_for_update()
|
|
161
|
-
)
|
|
162
|
-
).all()
|
|
163
|
-
)
|
|
164
|
-
current = frozenset(item.role_id for item in existing)
|
|
165
|
-
if current == requested:
|
|
166
|
-
return
|
|
167
|
-
for item in existing:
|
|
168
|
-
if item.role_id not in requested:
|
|
169
|
-
await session.delete(item)
|
|
170
|
-
for role_id in sorted(requested - current):
|
|
171
|
-
session.add(UserRoleRow(user_id=user_id, role_id=role_id))
|
|
172
|
-
user.permission_version += 1
|
|
173
|
-
|
|
174
|
-
async def set_role_permissions(self, role_id: UUID, permissions: tuple[str, ...]) -> None:
|
|
175
|
-
async with self._provider.operation("set_role_permissions"):
|
|
176
|
-
session = await self._provider.get_session()
|
|
177
|
-
role = await self._require_role(role_id, for_update=True)
|
|
178
|
-
requested = frozenset(permissions)
|
|
179
|
-
existing = tuple(
|
|
180
|
-
(
|
|
181
|
-
await session.scalars(
|
|
182
|
-
select(RolePermissionRow)
|
|
183
|
-
.where(RolePermissionRow.role_id == role_id)
|
|
184
|
-
.with_for_update()
|
|
185
|
-
)
|
|
186
|
-
).all()
|
|
187
|
-
)
|
|
188
|
-
current = frozenset(item.permission_name for item in existing)
|
|
189
|
-
if current == requested:
|
|
190
|
-
return
|
|
191
|
-
for item in existing:
|
|
192
|
-
if item.permission_name not in requested:
|
|
193
|
-
await session.delete(item)
|
|
194
|
-
for permission in sorted(requested - current):
|
|
195
|
-
session.add(RolePermissionRow(role_id=role_id, permission_name=permission))
|
|
196
|
-
role.permission_version += 1
|
|
197
|
-
|
|
198
|
-
async def _require_user(self, user_id: UUID, *, for_update: bool) -> UserRow:
|
|
199
|
-
session = await self._provider.get_session()
|
|
200
|
-
statement = select(UserRow).where(UserRow.user_id == user_id)
|
|
201
|
-
if for_update:
|
|
202
|
-
statement = statement.with_for_update()
|
|
203
|
-
row = await session.scalar(statement)
|
|
204
|
-
if row is None:
|
|
205
|
-
raise IdentityResourceNotFoundError(resource="user")
|
|
206
|
-
return row
|
|
207
|
-
|
|
208
|
-
async def _require_role(self, role_id: UUID, *, for_update: bool) -> RoleRow:
|
|
209
|
-
session = await self._provider.get_session()
|
|
210
|
-
statement = select(RoleRow).where(RoleRow.role_id == role_id)
|
|
211
|
-
if for_update:
|
|
212
|
-
statement = statement.with_for_update()
|
|
213
|
-
row = await session.scalar(statement)
|
|
214
|
-
if row is None:
|
|
215
|
-
raise IdentityResourceNotFoundError(resource="role")
|
|
216
|
-
return row
|
|
217
|
-
|
|
218
|
-
async def _flush_unique(self, resource: str) -> None:
|
|
219
|
-
session = await self._provider.get_session()
|
|
220
|
-
try:
|
|
221
|
-
await session.flush()
|
|
222
|
-
except IntegrityError:
|
|
223
|
-
raise IdentityConflictError(resource=resource) from None
|
|
224
36
|
|
|
225
37
|
|
|
226
38
|
class SqlAlchemyIdentityAccessStore:
|
|
227
|
-
__slots__ = ("_engines",)
|
|
39
|
+
__slots__ = ("_engines", "_options", "_users")
|
|
228
40
|
|
|
229
|
-
def __init__(
|
|
41
|
+
def __init__(
|
|
42
|
+
self,
|
|
43
|
+
engines: _SqlAlchemyEngineRegistry,
|
|
44
|
+
models: SqlAlchemyModelCatalog,
|
|
45
|
+
options: Options[IdentityOptions],
|
|
46
|
+
) -> None:
|
|
230
47
|
self._engines = engines
|
|
48
|
+
self._users = models.extension_model(IDENTITY_USER_EXTENSION, UserRowValues)
|
|
49
|
+
self._options = options.value
|
|
231
50
|
|
|
232
51
|
async def load_current_user(
|
|
233
52
|
self,
|
|
@@ -237,7 +56,9 @@ class SqlAlchemyIdentityAccessStore:
|
|
|
237
56
|
) -> CurrentUser | None:
|
|
238
57
|
async with self._session() as session:
|
|
239
58
|
user = await session.scalar(
|
|
240
|
-
select(
|
|
59
|
+
select(self._users.model).where(
|
|
60
|
+
self._users.model.user_id == user_id, self._users.model.is_active.is_(True)
|
|
61
|
+
)
|
|
241
62
|
)
|
|
242
63
|
refresh = await session.scalar(
|
|
243
64
|
select(RefreshSessionRow).where(
|
|
@@ -245,7 +66,11 @@ class SqlAlchemyIdentityAccessStore:
|
|
|
245
66
|
RefreshSessionRow.user_id == user_id,
|
|
246
67
|
)
|
|
247
68
|
)
|
|
248
|
-
if
|
|
69
|
+
if (
|
|
70
|
+
user is None
|
|
71
|
+
or refresh is None
|
|
72
|
+
or not _session_is_active(refresh, now, self._options)
|
|
73
|
+
):
|
|
249
74
|
return None
|
|
250
75
|
role_rows = (
|
|
251
76
|
await session.execute(
|
|
@@ -300,17 +125,20 @@ class SqlAlchemyIdentityAccessStore:
|
|
|
300
125
|
|
|
301
126
|
|
|
302
127
|
class SqlAlchemyIdentityLoginStore:
|
|
303
|
-
__slots__ = ("_engines",)
|
|
128
|
+
__slots__ = ("_engines", "_users")
|
|
304
129
|
|
|
305
|
-
def __init__(self, engines: _SqlAlchemyEngineRegistry) -> None:
|
|
130
|
+
def __init__(self, engines: _SqlAlchemyEngineRegistry, models: SqlAlchemyModelCatalog) -> None:
|
|
306
131
|
self._engines = engines
|
|
132
|
+
self._users = models.extension_model(IDENTITY_USER_EXTENSION, UserRowValues)
|
|
307
133
|
|
|
308
134
|
async def find_user(self, normalized_username: str) -> IdentityUser | None:
|
|
309
135
|
async with self._session() as session:
|
|
310
136
|
row = await session.scalar(
|
|
311
|
-
select(
|
|
137
|
+
select(self._users.model).where(
|
|
138
|
+
self._users.model.normalized_username == normalized_username
|
|
139
|
+
)
|
|
312
140
|
)
|
|
313
|
-
return None if row is None else _user_value(row)
|
|
141
|
+
return None if row is None else _user_value(row, self._users.read(row))
|
|
314
142
|
|
|
315
143
|
async def record_failure(
|
|
316
144
|
self,
|
|
@@ -321,8 +149,11 @@ class SqlAlchemyIdentityLoginStore:
|
|
|
321
149
|
lockout_duration_seconds: int,
|
|
322
150
|
) -> None:
|
|
323
151
|
async with self._session() as session, session.begin():
|
|
152
|
+
await _lock_identity_user(session, user_id)
|
|
324
153
|
row = await session.scalar(
|
|
325
|
-
select(
|
|
154
|
+
select(self._users.model)
|
|
155
|
+
.where(self._users.model.user_id == user_id)
|
|
156
|
+
.with_for_update()
|
|
326
157
|
)
|
|
327
158
|
if row is None or not row.is_active:
|
|
328
159
|
return
|
|
@@ -349,8 +180,11 @@ class SqlAlchemyIdentityLoginStore:
|
|
|
349
180
|
now: datetime,
|
|
350
181
|
) -> bool:
|
|
351
182
|
async with self._session() as database, database.begin():
|
|
183
|
+
await _lock_identity_user(database, user_id)
|
|
352
184
|
row = await database.scalar(
|
|
353
|
-
select(
|
|
185
|
+
select(self._users.model)
|
|
186
|
+
.where(self._users.model.user_id == user_id)
|
|
187
|
+
.with_for_update()
|
|
354
188
|
)
|
|
355
189
|
if (
|
|
356
190
|
row is None
|
|
@@ -371,10 +205,13 @@ class SqlAlchemyIdentityLoginStore:
|
|
|
371
205
|
|
|
372
206
|
|
|
373
207
|
class SqlAlchemyRefreshSessionStore:
|
|
374
|
-
__slots__ = ("_engines",)
|
|
208
|
+
__slots__ = ("_engines", "_options")
|
|
375
209
|
|
|
376
|
-
def __init__(
|
|
210
|
+
def __init__(
|
|
211
|
+
self, engines: _SqlAlchemyEngineRegistry, options: Options[IdentityOptions]
|
|
212
|
+
) -> None:
|
|
377
213
|
self._engines = engines
|
|
214
|
+
self._options = options.value
|
|
378
215
|
|
|
379
216
|
async def rotate(
|
|
380
217
|
self,
|
|
@@ -385,13 +222,17 @@ class SqlAlchemyRefreshSessionStore:
|
|
|
385
222
|
now: datetime,
|
|
386
223
|
) -> RefreshRotationResult:
|
|
387
224
|
async with self._session() as session, session.begin():
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
RefreshSessionRow.
|
|
225
|
+
identity = (
|
|
226
|
+
await session.execute(
|
|
227
|
+
select(RefreshSessionRow.user_id, RefreshSessionRow.family_id).where(
|
|
228
|
+
RefreshSessionRow.token_hash == token_hash
|
|
229
|
+
)
|
|
391
230
|
)
|
|
392
|
-
)
|
|
393
|
-
if
|
|
231
|
+
).one_or_none()
|
|
232
|
+
if identity is None:
|
|
394
233
|
return RefreshRotationResult(RefreshRotationStatus.NOT_FOUND, None)
|
|
234
|
+
user_id, family_id = identity
|
|
235
|
+
await _lock_identity_user(session, user_id)
|
|
395
236
|
await _lock_refresh_family(session, family_id)
|
|
396
237
|
current = await session.scalar(
|
|
397
238
|
select(RefreshSessionRow)
|
|
@@ -404,7 +245,7 @@ class SqlAlchemyRefreshSessionStore:
|
|
|
404
245
|
if current.revoked_at is not None or current.replaced_by_session_id is not None:
|
|
405
246
|
await _revoke_family(session, current.family_id, now=now)
|
|
406
247
|
return RefreshRotationResult(RefreshRotationStatus.REUSED, None)
|
|
407
|
-
if not _session_is_active(current, now):
|
|
248
|
+
if not _session_is_active(current, now, self._options):
|
|
408
249
|
await _revoke_family(session, current.family_id, now=now)
|
|
409
250
|
return RefreshRotationResult(RefreshRotationStatus.EXPIRED, None)
|
|
410
251
|
|
|
@@ -425,13 +266,17 @@ class SqlAlchemyRefreshSessionStore:
|
|
|
425
266
|
|
|
426
267
|
async def revoke(self, token_hash: str, *, now: datetime) -> None:
|
|
427
268
|
async with self._session() as session, session.begin():
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
RefreshSessionRow.
|
|
269
|
+
identity = (
|
|
270
|
+
await session.execute(
|
|
271
|
+
select(RefreshSessionRow.user_id, RefreshSessionRow.family_id).where(
|
|
272
|
+
RefreshSessionRow.token_hash == token_hash
|
|
273
|
+
)
|
|
431
274
|
)
|
|
432
|
-
)
|
|
433
|
-
if
|
|
275
|
+
).one_or_none()
|
|
276
|
+
if identity is None:
|
|
434
277
|
return
|
|
278
|
+
user_id, family_id = identity
|
|
279
|
+
await _lock_identity_user(session, user_id)
|
|
435
280
|
await _lock_refresh_family(session, family_id)
|
|
436
281
|
current = await session.scalar(
|
|
437
282
|
select(RefreshSessionRow)
|
|
@@ -446,72 +291,6 @@ class SqlAlchemyRefreshSessionStore:
|
|
|
446
291
|
return FrameworkAsyncSession(self._engines.get("default"), expire_on_commit=False)
|
|
447
292
|
|
|
448
293
|
|
|
449
|
-
async def _revoke_family(
|
|
450
|
-
session: AsyncSession,
|
|
451
|
-
family_id: UUID,
|
|
452
|
-
*,
|
|
453
|
-
now: datetime,
|
|
454
|
-
) -> None:
|
|
455
|
-
rows = tuple(
|
|
456
|
-
(
|
|
457
|
-
await session.scalars(
|
|
458
|
-
select(RefreshSessionRow)
|
|
459
|
-
.where(RefreshSessionRow.family_id == family_id)
|
|
460
|
-
.with_for_update()
|
|
461
|
-
)
|
|
462
|
-
).all()
|
|
463
|
-
)
|
|
464
|
-
for row in rows:
|
|
465
|
-
if row.revoked_at is None:
|
|
466
|
-
row.revoked_at = now
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
async def _lock_refresh_family(session: AsyncSession, family_id: UUID) -> None:
|
|
470
|
-
# family 没有可锁的稳定 parent row;transaction advisory lock 在同一短事务内提供
|
|
471
|
-
# family -> token/family rows 的统一顺序,并随 commit/rollback 自动释放。
|
|
472
|
-
await session.scalar(select(func.pg_advisory_xact_lock(_refresh_family_lock_key(family_id))))
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
def _refresh_family_lock_key(family_id: UUID) -> int:
|
|
476
|
-
digest = blake2b(
|
|
477
|
-
family_id.bytes,
|
|
478
|
-
digest_size=8,
|
|
479
|
-
person=_REFRESH_FAMILY_LOCK_PERSON,
|
|
480
|
-
).digest()
|
|
481
|
-
return int.from_bytes(digest, byteorder="big", signed=True)
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
def _session_is_active(row: RefreshSessionRow, now: datetime) -> bool:
|
|
485
|
-
return (
|
|
486
|
-
row.revoked_at is None
|
|
487
|
-
and row.replaced_by_session_id is None
|
|
488
|
-
and now - row.last_used_at < REFRESH_INACTIVITY_LIFETIME
|
|
489
|
-
and now - row.family_created_at < REFRESH_ABSOLUTE_LIFETIME
|
|
490
|
-
)
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
def _user_value(row: UserRow) -> IdentityUser:
|
|
494
|
-
return IdentityUser(
|
|
495
|
-
user_id=row.user_id,
|
|
496
|
-
username=row.username,
|
|
497
|
-
normalized_username=row.normalized_username,
|
|
498
|
-
password_hash=row.password_hash,
|
|
499
|
-
is_active=row.is_active,
|
|
500
|
-
failed_access_count=row.failed_access_count,
|
|
501
|
-
lockout_end=row.lockout_end,
|
|
502
|
-
permission_version=row.permission_version,
|
|
503
|
-
)
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
def _role_value(row: RoleRow) -> IdentityRole:
|
|
507
|
-
return IdentityRole(
|
|
508
|
-
role_id=row.role_id,
|
|
509
|
-
name=row.name,
|
|
510
|
-
normalized_name=row.normalized_name,
|
|
511
|
-
permission_version=row.permission_version,
|
|
512
|
-
)
|
|
513
|
-
|
|
514
|
-
|
|
515
294
|
def _refresh_row(value: RefreshSession) -> RefreshSessionRow:
|
|
516
295
|
return RefreshSessionRow(
|
|
517
296
|
session_id=value.session_id,
|
|
@@ -5,7 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
import hashlib
|
|
6
6
|
import secrets
|
|
7
7
|
from dataclasses import dataclass
|
|
8
|
-
from datetime import UTC, datetime
|
|
8
|
+
from datetime import UTC, datetime
|
|
9
9
|
from uuid import UUID, uuid4
|
|
10
10
|
|
|
11
11
|
import jwt
|
|
@@ -14,9 +14,6 @@ from ..options import Options
|
|
|
14
14
|
from .contracts import IdentityOptions
|
|
15
15
|
from .errors import InvalidCredentialsError
|
|
16
16
|
|
|
17
|
-
ACCESS_TOKEN_LIFETIME = timedelta(minutes=15)
|
|
18
|
-
REFRESH_INACTIVITY_LIFETIME = timedelta(days=30)
|
|
19
|
-
REFRESH_ABSOLUTE_LIFETIME = timedelta(days=90)
|
|
20
17
|
ACCESS_TOKEN_TYPE = "access"
|
|
21
18
|
BEARER_TOKEN_TYPE = "Bearer"
|
|
22
19
|
|
|
@@ -43,7 +40,7 @@ class JwtTokenService:
|
|
|
43
40
|
"jti": str(uuid4()),
|
|
44
41
|
"iat": issued,
|
|
45
42
|
"nbf": issued,
|
|
46
|
-
"exp": issued +
|
|
43
|
+
"exp": issued + self._options.access_token_lifetime,
|
|
47
44
|
"typ": ACCESS_TOKEN_TYPE,
|
|
48
45
|
}
|
|
49
46
|
key = self._options.keys[self._options.active_kid].get_secret_value()
|
|
@@ -6,13 +6,15 @@ from collections.abc import Mapping
|
|
|
6
6
|
|
|
7
7
|
from pydantic import ValidationError
|
|
8
8
|
|
|
9
|
-
from .
|
|
10
|
-
|
|
9
|
+
from ..application_services.errors import (
|
|
10
|
+
ApplicationServiceValidationError,
|
|
11
|
+
ApplicationServiceValidationIssue,
|
|
12
|
+
)
|
|
13
|
+
from ..invocation.methods import _CompiledMethod
|
|
11
14
|
|
|
12
15
|
|
|
13
16
|
async def _validate_arguments(
|
|
14
17
|
method: _CompiledMethod,
|
|
15
|
-
registration: ApplicationServiceRegistration,
|
|
16
18
|
args: tuple[object, ...],
|
|
17
19
|
kwargs: dict[str, object],
|
|
18
20
|
) -> tuple[tuple[object, ...], dict[str, object]]:
|
|
@@ -22,7 +24,7 @@ async def _validate_arguments(
|
|
|
22
24
|
except ValidationError as original_error:
|
|
23
25
|
validation_error = _sanitize_validation_error(
|
|
24
26
|
original_error,
|
|
25
|
-
|
|
27
|
+
service=method.descriptor.service,
|
|
26
28
|
method_name=method.name,
|
|
27
29
|
parameter_names=method.parameter_names,
|
|
28
30
|
)
|
|
@@ -33,7 +35,7 @@ async def _validate_arguments(
|
|
|
33
35
|
def _sanitize_validation_error(
|
|
34
36
|
error: ValidationError,
|
|
35
37
|
*,
|
|
36
|
-
|
|
38
|
+
service: type[object],
|
|
37
39
|
method_name: str,
|
|
38
40
|
parameter_names: frozenset[str],
|
|
39
41
|
) -> ApplicationServiceValidationError:
|
|
@@ -51,7 +53,7 @@ def _sanitize_validation_error(
|
|
|
51
53
|
)
|
|
52
54
|
)
|
|
53
55
|
return ApplicationServiceValidationError(
|
|
54
|
-
service=
|
|
56
|
+
service=service,
|
|
55
57
|
method_name=method_name,
|
|
56
58
|
issues=issues,
|
|
57
59
|
)
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""拦截规则归调用能力持有,贡献有效期复用模块 hook 屏障。"""
|
|
2
|
+
|
|
3
|
+
import inspect
|
|
4
|
+
from collections.abc import Callable
|
|
5
|
+
|
|
6
|
+
from ..application_services.errors import ApplicationServiceDefinitionError
|
|
7
|
+
from ..diagnostics.source import caller_source
|
|
8
|
+
from ..invocation.interceptors import (
|
|
9
|
+
InterceptionStage,
|
|
10
|
+
Interceptor,
|
|
11
|
+
InterceptorRegistration,
|
|
12
|
+
_InterceptorRule,
|
|
13
|
+
)
|
|
14
|
+
from ..invocation.methods import ServiceMethod
|
|
15
|
+
from ..lifecycle import ConfigureContext, PostConfigureContext
|
|
16
|
+
from .module import InvocationModule
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def add_interceptor(
|
|
20
|
+
context: ConfigureContext | PostConfigureContext,
|
|
21
|
+
interceptor: type[Interceptor],
|
|
22
|
+
*,
|
|
23
|
+
match: Callable[[ServiceMethod], bool],
|
|
24
|
+
before: InterceptionStage = InterceptionStage.METHOD,
|
|
25
|
+
) -> None:
|
|
26
|
+
context.services.require_active()
|
|
27
|
+
context.modules.require_dependency(
|
|
28
|
+
context.descriptor.key,
|
|
29
|
+
InvocationModule,
|
|
30
|
+
source=context.descriptor.declaration_source,
|
|
31
|
+
)
|
|
32
|
+
if (
|
|
33
|
+
not isinstance(interceptor, type)
|
|
34
|
+
or not issubclass(interceptor, Interceptor)
|
|
35
|
+
or inspect.isabstract(interceptor)
|
|
36
|
+
or not inspect.iscoroutinefunction(interceptor.intercept)
|
|
37
|
+
):
|
|
38
|
+
raise ApplicationServiceDefinitionError(
|
|
39
|
+
symbol=str(interceptor),
|
|
40
|
+
reason="interceptor must implement async intercept",
|
|
41
|
+
)
|
|
42
|
+
if (
|
|
43
|
+
not callable(match)
|
|
44
|
+
or inspect.iscoroutinefunction(match)
|
|
45
|
+
or not isinstance(before, InterceptionStage)
|
|
46
|
+
):
|
|
47
|
+
raise ApplicationServiceDefinitionError(
|
|
48
|
+
symbol=interceptor.__qualname__,
|
|
49
|
+
reason="interceptor requires a synchronous match and valid position",
|
|
50
|
+
)
|
|
51
|
+
context.modules.instance(InvocationModule)._rules.append(
|
|
52
|
+
_InterceptorRule(
|
|
53
|
+
InterceptorRegistration(
|
|
54
|
+
interceptor,
|
|
55
|
+
before,
|
|
56
|
+
context.descriptor.key,
|
|
57
|
+
caller_source(str(context.descriptor.key), depth=2),
|
|
58
|
+
),
|
|
59
|
+
match,
|
|
60
|
+
)
|
|
61
|
+
)
|