hexcore 11.2.0__tar.gz → 11.3.0__tar.gz
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.
- {hexcore-11.2.0/hexcore.egg-info → hexcore-11.3.0}/PKG-INFO +1 -1
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/config.py +29 -3
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/infrastructure/api/routers.py +62 -1
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/fastapi.py +4 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/fastapi.pyi +3 -1
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/infrastructure/api/app.py +34 -9
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/infrastructure/api/middlewares.py +64 -0
- {hexcore-11.2.0 → hexcore-11.3.0/hexcore.egg-info}/PKG-INFO +1 -1
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore.egg-info/SOURCES.txt +1 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/pyproject.toml +1 -1
- hexcore-11.3.0/tests/test_api_cors_dinamico.py +250 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/tests/test_darwin_http.py +164 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/LICENSE +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/MANIFEST.in +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/README.md +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/__init__.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/__main__.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/_deprecation.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/application/__init__.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/application/cqrs/__init__.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/application/cqrs/adapters.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/application/cqrs/config.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/application/cqrs/factory.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/application/cqrs/in_memory_buses.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/application/cqrs/pipeline.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/application/cqrs/registry.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/application/cqrs/scheduler.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/application/dtos/__init__.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/application/dtos/base.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/application/dtos/cursor.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/application/dtos/errors.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/application/dtos/query.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/application/eventsourcing/__init__.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/application/eventsourcing/config.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/application/eventsourcing/factory.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/application/eventsourcing/projector.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/application/eventsourcing/relay.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/application/eventsourcing/repository.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/application/use_cases/__init__.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/application/use_cases/base.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/application/use_cases/query.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/capabilities.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/cqrs.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/cqrs.pyi +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/__init__.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/__init__.pyi +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/application/__init__.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/application/authorization.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/application/authz_middleware.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/application/commands.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/application/config.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/application/container.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/application/hooks.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/application/plugins.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/application/services.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/domain/__init__.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/domain/authorization.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/domain/context.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/domain/entities.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/domain/events.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/domain/exceptions.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/domain/permissions.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/domain/plugins.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/domain/ports.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/domain/value_objects.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/infrastructure/__init__.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/infrastructure/api/__init__.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/infrastructure/api/authorization.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/infrastructure/api/dependencies.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/infrastructure/api/middlewares.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/infrastructure/cli.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/infrastructure/clock.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/infrastructure/envelope.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/infrastructure/hashing.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/infrastructure/keys.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/infrastructure/lifespan.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/infrastructure/orms/__init__.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/infrastructure/orms/beanie/__init__.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/infrastructure/orms/beanie/documents.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/infrastructure/orms/beanie/repositories.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/infrastructure/orms/beanie/schema.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/infrastructure/orms/selection.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/infrastructure/orms/sqlalchemy/__init__.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/infrastructure/orms/sqlalchemy/models.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/infrastructure/orms/sqlalchemy/models_mixins.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/infrastructure/orms/sqlalchemy/registry.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/infrastructure/orms/sqlalchemy/repositories.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/infrastructure/orms/sqlalchemy/schema.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/infrastructure/revocation.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/infrastructure/secretbox.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/infrastructure/tokens.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/infrastructure/transports.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/plugins/__init__.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/plugins/drbac/__init__.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/plugins/drbac/cache.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/plugins/drbac/conditions.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/plugins/drbac/domain.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/plugins/drbac/orms/__init__.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/plugins/drbac/orms/beanie/__init__.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/plugins/drbac/orms/beanie/repository.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/plugins/drbac/orms/sqlalchemy/__init__.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/plugins/drbac/orms/sqlalchemy/models.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/plugins/drbac/orms/sqlalchemy/models_mixins.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/plugins/drbac/orms/sqlalchemy/repository.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/plugins/drbac/pdp.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/plugins/drbac/pip.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/plugins/drbac/predicates.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/plugins/drbac/provider.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/plugins/drbac/router.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/plugins/drbac/service.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/plugins/impersonate/__init__.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/plugins/impersonate/commands.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/plugins/impersonate/domain.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/plugins/impersonate/router.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/plugins/impersonate/service.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/plugins/magic_link/__init__.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/plugins/magic_link/commands.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/plugins/magic_link/router.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/plugins/oauth/__init__.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/plugins/oauth/domain.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/plugins/oauth/http.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/plugins/oauth/orms/__init__.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/plugins/oauth/orms/beanie/__init__.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/plugins/oauth/orms/beanie/repository.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/plugins/oauth/orms/sqlalchemy/__init__.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/plugins/oauth/orms/sqlalchemy/models.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/plugins/oauth/orms/sqlalchemy/models_mixins.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/plugins/oauth/orms/sqlalchemy/repository.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/plugins/oauth/providers.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/plugins/oauth/router.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/plugins/oauth/service.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/plugins/organization/__init__.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/plugins/organization/domain.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/plugins/organization/orms/__init__.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/plugins/organization/orms/beanie/__init__.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/plugins/organization/orms/beanie/repository.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/plugins/organization/orms/sqlalchemy/__init__.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/plugins/organization/orms/sqlalchemy/models.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/plugins/organization/orms/sqlalchemy/models_mixins.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/plugins/organization/orms/sqlalchemy/repository.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/plugins/organization/router.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/plugins/organization/service.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/plugins/passkey/__init__.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/plugins/passkey/domain.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/plugins/passkey/orms/__init__.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/plugins/passkey/orms/beanie/__init__.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/plugins/passkey/orms/beanie/repository.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/plugins/passkey/orms/sqlalchemy/__init__.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/plugins/passkey/orms/sqlalchemy/models.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/plugins/passkey/orms/sqlalchemy/models_mixins.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/plugins/passkey/orms/sqlalchemy/repository.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/plugins/passkey/router.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/plugins/passkey/service.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/plugins/passkey/webauthn_adapter.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/plugins/rbac/__init__.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/plugins/rbac/cache.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/plugins/rbac/cli.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/plugins/rbac/domain.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/plugins/rbac/matcher.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/plugins/rbac/orms/__init__.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/plugins/rbac/orms/beanie/__init__.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/plugins/rbac/orms/beanie/repository.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/plugins/rbac/orms/sqlalchemy/__init__.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/plugins/rbac/orms/sqlalchemy/models.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/plugins/rbac/orms/sqlalchemy/models_mixins.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/plugins/rbac/orms/sqlalchemy/repository.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/plugins/rbac/provider.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/plugins/rbac/resolver.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/plugins/rbac/router.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/plugins/rbac/service.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/plugins/storage.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/plugins/two_factor/__init__.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/plugins/two_factor/commands.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/plugins/two_factor/crypto.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/plugins/two_factor/domain.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/plugins/two_factor/orms/__init__.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/plugins/two_factor/orms/beanie/__init__.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/plugins/two_factor/orms/beanie/repository.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/plugins/two_factor/orms/sqlalchemy/__init__.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/plugins/two_factor/orms/sqlalchemy/models.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/plugins/two_factor/orms/sqlalchemy/models_mixins.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/plugins/two_factor/orms/sqlalchemy/repository.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/plugins/two_factor/router.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/plugins/two_factor/service.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/plugins/two_factor/totp.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/testing/__init__.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/testing/fakes.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/testing/fixtures.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/darwin/testing/helpers.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/domain/__init__.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/domain/base.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/domain/cqrs/__init__.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/domain/cqrs/buses.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/domain/cqrs/commands.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/domain/cqrs/context.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/domain/cqrs/cron.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/domain/cqrs/decorators.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/domain/cqrs/dispatch.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/domain/cqrs/envelope.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/domain/cqrs/exceptions.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/domain/cqrs/handlers.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/domain/cqrs/middleware.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/domain/cqrs/queries.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/domain/cqrs/resolution.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/domain/cqrs/serializer.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/domain/cqrs/task_queues.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/domain/events.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/domain/eventsourcing/__init__.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/domain/eventsourcing/aggregate.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/domain/eventsourcing/exceptions.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/domain/eventsourcing/projections.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/domain/eventsourcing/snapshots.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/domain/eventsourcing/store.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/domain/eventsourcing/stored.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/domain/exceptions.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/domain/repositories.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/domain/services.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/domain/uow.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/eventsourcing.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/eventsourcing.pyi +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/infrastructure/__init__.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/infrastructure/api/__init__.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/infrastructure/api/cqrs.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/infrastructure/api/eventsourcing.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/infrastructure/api/exception_handlers.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/infrastructure/api/health.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/infrastructure/api/lifespan.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/infrastructure/api/rate_limit.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/infrastructure/api/routing.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/infrastructure/api/streaming.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/infrastructure/api/utils.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/infrastructure/cache/__init__.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/infrastructure/cache/cache_backends/__init__.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/infrastructure/cache/cache_backends/memory.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/infrastructure/cache/cache_backends/redis.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/infrastructure/cli.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/infrastructure/cqrs/__init__.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/infrastructure/cqrs/cron_sql.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/infrastructure/cqrs/middlewares.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/infrastructure/cqrs/postgres_bus.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/infrastructure/cqrs/postgres_lock.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/infrastructure/cqrs/procrastinate.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/infrastructure/cqrs/pydantic_serializer.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/infrastructure/cqrs/rabbitmq.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/infrastructure/cqrs/redis_bus.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/infrastructure/cqrs/redis_lock.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/infrastructure/eventsourcing/__init__.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/infrastructure/eventsourcing/beanie_documents.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/infrastructure/eventsourcing/beanie_store.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/infrastructure/eventsourcing/memory.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/infrastructure/eventsourcing/redis_store.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/infrastructure/eventsourcing/sqlalchemy_models.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/infrastructure/eventsourcing/sqlalchemy_models_mixins.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/infrastructure/eventsourcing/sqlalchemy_store.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/infrastructure/repositories/__init__.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/infrastructure/repositories/_beanie_impl.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/infrastructure/repositories/_sqlalchemy_impl.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/infrastructure/repositories/base.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/infrastructure/repositories/decorators.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/infrastructure/repositories/implementations.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/infrastructure/repositories/orms/__init__.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/infrastructure/repositories/orms/beanie/__init__.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/infrastructure/repositories/orms/beanie/utils.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/infrastructure/repositories/orms/sqlalchemy/__init__.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/infrastructure/repositories/orms/sqlalchemy/id_types.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/infrastructure/repositories/orms/sqlalchemy/session.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/infrastructure/repositories/orms/sqlalchemy/utils.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/infrastructure/repositories/utils.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/infrastructure/task_queues/__init__.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/infrastructure/task_queues/celery_adapter.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/infrastructure/task_queues/procrastinate_adapter.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/infrastructure/uow/__init__.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/infrastructure/uow/decorators.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/infrastructure/uow/helpers.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/infrastructure/uow/scopes.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/infrastructure/workers/__init__.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/infrastructure/workers/consumer.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/infrastructure/workers/rabbitmq_worker.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/infrastructure/workers/runner.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/py.typed +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/sql.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/sql.pyi +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/testing/__init__.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/testing/fakes.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/testing/fixtures.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/testing/helpers.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/testing/repositories.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore/types.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore.egg-info/dependency_links.txt +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore.egg-info/entry_points.txt +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore.egg-info/requires.txt +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/hexcore.egg-info/top_level.txt +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/setup.cfg +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/tests/test_alembic_metadata_completeness.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/tests/test_anclas_del_repo.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/tests/test_api_app_and_lifespan.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/tests/test_api_cqrs_providers.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/tests/test_api_exception_handlers.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/tests/test_api_health.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/tests/test_api_middlewares_and_routing.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/tests/test_api_rate_limit.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/tests/test_api_streaming.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/tests/test_basic.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/tests/test_beanie_query_utils.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/tests/test_celery_event_loop.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/tests/test_config_cors_security.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/tests/test_config_loading.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/tests/test_consumer_optional_event_bus.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/tests/test_cqrs.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/tests/test_cqrs_envelope.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/tests/test_cqrs_factory.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/tests/test_cqrs_middlewares.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/tests/test_cron_sql.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/tests/test_darwin_10_4_hardening.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/tests/test_darwin_authorization_engine.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/tests/test_darwin_authorization_http.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/tests/test_darwin_backend_neutrality.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/tests/test_darwin_beanie.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/tests/test_darwin_cli.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/tests/test_darwin_container.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/tests/test_darwin_context.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/tests/test_darwin_custom_plugin.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/tests/test_darwin_drbac_conditions.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/tests/test_darwin_drbac_pdp.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/tests/test_darwin_drbac_router.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/tests/test_darwin_drbac_service.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/tests/test_darwin_envelope.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/tests/test_darwin_facade_and_events.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/tests/test_darwin_flows.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/tests/test_darwin_hashing.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/tests/test_darwin_impersonate.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/tests/test_darwin_magic_link.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/tests/test_darwin_models.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/tests/test_darwin_oauth.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/tests/test_darwin_openapi.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/tests/test_darwin_organization.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/tests/test_darwin_passkey.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/tests/test_darwin_permissions.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/tests/test_darwin_plugin_decoupling.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/tests/test_darwin_plugins.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/tests/test_darwin_principals.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/tests/test_darwin_production_defaults.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/tests/test_darwin_rbac.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/tests/test_darwin_rbac_service.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/tests/test_darwin_repositories.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/tests/test_darwin_revocation.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/tests/test_darwin_schema_registration.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/tests/test_darwin_storage_selection.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/tests/test_darwin_tokens.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/tests/test_darwin_two_factor.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/tests/test_darwin_username.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/tests/test_darwin_value_objects.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/tests/test_deprecations.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/tests/test_documentation_examples.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/tests/test_domain_base_contract.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/tests/test_domain_service_query.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/tests/test_dotted_resolution.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/tests/test_event_dispatch_mro.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/tests/test_eventsourcing_aggregate.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/tests/test_eventsourcing_beanie.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/tests/test_eventsourcing_config.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/tests/test_eventsourcing_ports.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/tests/test_eventsourcing_projections.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/tests/test_eventsourcing_redis.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/tests/test_eventsourcing_relay.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/tests/test_eventsourcing_repository.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/tests/test_eventsourcing_snapshot_checkpoint.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/tests/test_eventsourcing_sql_schema.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/tests/test_eventsourcing_store_contract.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/tests/test_eventsourcing_uow_integration.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/tests/test_facades.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/tests/test_handler_registry.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/tests/test_infrastructure_query_path.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/tests/test_lock_error_policy.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/tests/test_optional_dependencies.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/tests/test_packaging.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/tests/test_postgres_bus.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/tests/test_postgres_lock.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/tests/test_query_cursor_and_endpoint.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/tests/test_query_field_validation.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/tests/test_rabbitmq_bus.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/tests/test_redis_bus.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/tests/test_redis_lock.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/tests/test_repositories_utils.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/tests/test_scheduler.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/tests/test_scheduler_catchup.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/tests/test_smart_routing.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/tests/test_sql_naming_convention.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/tests/test_sql_session_layer.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/tests/test_task_queues_adapters.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/tests/test_testing_kit.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/tests/test_testing_utilities.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/tests/test_typing_gate.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/tests/test_uow_session_regression.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/tests/test_use_cases_query.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/tests/test_worker_bus_integration.py +0 -0
- {hexcore-11.2.0 → hexcore-11.3.0}/tests/test_worker_runner.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: hexcore
|
|
3
|
-
Version: 11.
|
|
3
|
+
Version: 11.3.0
|
|
4
4
|
Summary: Reusable core for Python projects with hexagonal architecture, DDD, CQRS and background tasks. Ships the abstractions and the infrastructure: SQL session layer, FastAPI factories, worker runner, dynamic cron and identity.
|
|
5
5
|
Author-email: "David Latosefki (Indroic)" <indroic@outlook.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -78,6 +78,26 @@ class ServerConfig(BaseModel):
|
|
|
78
78
|
allow_methods: list[str] = ["*"]
|
|
79
79
|
allow_headers: list[str] = ["*"]
|
|
80
80
|
|
|
81
|
+
# CORS para una SPA en otro origen con la cookie de sesión. Los tres van con el prefijo
|
|
82
|
+
# `cors_` porque `max_age` y `expose_headers` a secas, en una config que también tiene
|
|
83
|
+
# cookies y TTLs, se leen como otra cosa.
|
|
84
|
+
#
|
|
85
|
+
# `cors_origin_predicate` cubre los orígenes que `allow_origins` no puede enumerar —un
|
|
86
|
+
# subdominio por tenant creado en caliente—, igual que
|
|
87
|
+
# `IdentityConfig.trusted_origin_predicate` hace con el CSRF. **Se suma** a `allow_origins`,
|
|
88
|
+
# no lo reemplaza. Es síncrono, recibe el origen tal cual llega en el header `Origin`
|
|
89
|
+
# (`"https://a.ejemplo.com"`) y si lanza se trata como «no confiable».
|
|
90
|
+
#
|
|
91
|
+
# `cors_expose_headers`: las cabeceras de la respuesta que el JS puede leer entre orígenes.
|
|
92
|
+
# Sin declararlas, el navegador sólo deja leer las «seguras» (`Content-Type`, etc.).
|
|
93
|
+
#
|
|
94
|
+
# `cors_max_age`: cuántos segundos el navegador cachea el preflight. Sin caché, toda
|
|
95
|
+
# escritura JSON con un header propio (`X-CSRF-Token`) cuesta un `OPTIONS` previo. 600 es el
|
|
96
|
+
# default de Starlette; Chrome topea en 7200.
|
|
97
|
+
cors_origin_predicate: t.Callable[[str], bool] | None = None
|
|
98
|
+
cors_expose_headers: list[str] = Field(default_factory=list)
|
|
99
|
+
cors_max_age: int = Field(default=600, ge=0)
|
|
100
|
+
|
|
81
101
|
# caching
|
|
82
102
|
cache_backend: ICache = (
|
|
83
103
|
MemoryCache()
|
|
@@ -171,6 +191,9 @@ class ServerConfig(BaseModel):
|
|
|
171
191
|
1. Si no pasaste `allow_origins`, se completa: en `debug` queda `["*"]` (comodidad
|
|
172
192
|
de desarrollo, que era la intención original), y fuera de `debug` queda
|
|
173
193
|
`["http://localhost:<port>"]`. Un valor explícito —incluso `[]`— se respeta.
|
|
194
|
+
**Con un `cors_origin_predicate` declarado queda `[]`**: el predicado es la lista, y
|
|
195
|
+
derivar `["*"]` bajaría `allow_credentials` a `False` en silencio y las cookies
|
|
196
|
+
dejarían de funcionar sin que nadie lo pidiera.
|
|
174
197
|
2. **`"*"` con `allow_credentials=True` nunca es válido, y el invariante vale
|
|
175
198
|
siempre, no sólo fuera de `debug`.** Es que `debug` viene en `True` por
|
|
176
199
|
defecto, así que condicionarlo al entorno dejaba la combinación peligrosa
|
|
@@ -194,9 +217,12 @@ class ServerConfig(BaseModel):
|
|
|
194
217
|
lo_pidio_el_usuario = "allow_origins" in self.model_fields_set
|
|
195
218
|
|
|
196
219
|
if not lo_pidio_el_usuario and not self.allow_origins:
|
|
197
|
-
self.
|
|
198
|
-
|
|
199
|
-
|
|
220
|
+
if self.cors_origin_predicate is not None:
|
|
221
|
+
self.allow_origins = []
|
|
222
|
+
else:
|
|
223
|
+
self.allow_origins = (
|
|
224
|
+
["*"] if self.debug else [f"http://localhost:{self.port}"]
|
|
225
|
+
)
|
|
200
226
|
|
|
201
227
|
if "*" not in self.allow_origins or not self.allow_credentials:
|
|
202
228
|
return self
|
|
@@ -31,6 +31,7 @@ from hexcore.darwin.infrastructure.api.dependencies import (
|
|
|
31
31
|
provide_auth,
|
|
32
32
|
require_not_impersonated,
|
|
33
33
|
)
|
|
34
|
+
from hexcore.darwin.infrastructure.api.middlewares import CSRF_HEADER
|
|
34
35
|
|
|
35
36
|
if t.TYPE_CHECKING:
|
|
36
37
|
from hexcore.darwin.application.config import IdentityConfig
|
|
@@ -42,6 +43,7 @@ __all__ = [
|
|
|
42
43
|
"VerifyEmailRequest",
|
|
43
44
|
"SignInRequest",
|
|
44
45
|
"SessionResponse",
|
|
46
|
+
"CsrfTokenResponse",
|
|
45
47
|
"MeResponse",
|
|
46
48
|
"SignedOutResponse",
|
|
47
49
|
"RevokedResponse",
|
|
@@ -122,6 +124,17 @@ class SignInRequest(BaseModel):
|
|
|
122
124
|
return t.cast(str, self.identifier or self.email or self.username)
|
|
123
125
|
|
|
124
126
|
|
|
127
|
+
class CsrfTokenResponse(BaseModel):
|
|
128
|
+
"""
|
|
129
|
+
La respuesta de `GET /auth/csrf`: el valor anti-CSRF de la sesión en curso.
|
|
130
|
+
|
|
131
|
+
Es el mismo que trae la cookie de CSRF —`HMAC(secreto, sid)`, ver `derive_csrf_token`— y
|
|
132
|
+
que las respuestas que la emiten repiten en la cabecera `X-CSRF-Token`.
|
|
133
|
+
"""
|
|
134
|
+
|
|
135
|
+
csrf_token: str
|
|
136
|
+
|
|
137
|
+
|
|
125
138
|
class SessionResponse(BaseModel):
|
|
126
139
|
"""
|
|
127
140
|
La respuesta de un sign-in o un refresh.
|
|
@@ -212,6 +225,14 @@ def emit_tokens(
|
|
|
212
225
|
a propósito: el cliente tiene que poder leerla para devolverla en el header
|
|
213
226
|
`X-CSRF-Token`. Que sea legible es lo que obliga a que su valor sea derivado del `sid` y
|
|
214
227
|
no aleatorio — ver `derive_csrf_token`.
|
|
228
|
+
|
|
229
|
+
**Además va en la cabecera de la respuesta** (`X-CSRF-Token`, la misma que el cliente
|
|
230
|
+
devuelve en cada escritura). Una SPA en otro origen que la API —`app.ejemplo.com` contra
|
|
231
|
+
`api.ejemplo.com`— no puede leer esa cookie con `document.cookie`, porque pertenece al host
|
|
232
|
+
de la API: sin la cabecera no tiene de dónde sacar el valor, y toda escritura responde 403 sin
|
|
233
|
+
que nada apunte a la causa. La cabecera es legible cross-origin sólo si el CORS la expone
|
|
234
|
+
(`create_app` lo hace cuando `AppFeatures(csrf=True)`), o sea que no se la regala a un origen
|
|
235
|
+
que el CORS no permite; y el valor no sirve sin la cookie de sesión que el navegador ya tiene.
|
|
215
236
|
"""
|
|
216
237
|
transport.emit(response, tokens)
|
|
217
238
|
|
|
@@ -234,15 +255,17 @@ def emit_tokens(
|
|
|
234
255
|
#
|
|
235
256
|
# El valor es un HMAC del `sid`, así que es válido mientras viva la sesión: alinearlo con
|
|
236
257
|
# el refresh es lo coherente, y es el mismo número que la cookie de refresh.
|
|
258
|
+
valor = derive_csrf_token(str(tokens.session_id), clave.get_secret_value())
|
|
237
259
|
response.set_cookie(
|
|
238
260
|
config.cookies.name_for("csrf"),
|
|
239
|
-
|
|
261
|
+
valor,
|
|
240
262
|
max_age=int(config.tokens.refresh_ttl.total_seconds()),
|
|
241
263
|
httponly=False,
|
|
242
264
|
secure=config.cookies.secure,
|
|
243
265
|
samesite=config.cookies.same_site,
|
|
244
266
|
path=config.cookies.path,
|
|
245
267
|
)
|
|
268
|
+
response.headers[CSRF_HEADER] = valor
|
|
246
269
|
|
|
247
270
|
|
|
248
271
|
def session_response_body(
|
|
@@ -464,6 +487,44 @@ def build_identity_router(
|
|
|
464
487
|
transport.clear(respuesta)
|
|
465
488
|
return respuesta
|
|
466
489
|
|
|
490
|
+
@router.get("/csrf", response_model=CsrfTokenResponse)
|
|
491
|
+
async def csrf(
|
|
492
|
+
response: Response,
|
|
493
|
+
auth: "AuthContext[t.Any]" = Depends(provide_auth),
|
|
494
|
+
) -> CsrfTokenResponse:
|
|
495
|
+
"""
|
|
496
|
+
El valor anti-CSRF de la sesión por cookie en curso.
|
|
497
|
+
|
|
498
|
+
Para el cliente que **no puede leer la cookie de CSRF**: una SPA en otro origen que la
|
|
499
|
+
API (`document.cookie` sólo ve las cookies de su propio host). Lo pide al abrir la
|
|
500
|
+
página con una sesión ya existente y lo guarda **en memoria** — nunca en
|
|
501
|
+
`localStorage`—; el sign-in y el refresh lo entregan además en la cabecera
|
|
502
|
+
`X-CSRF-Token`, así que sólo hace falta acá cuando la página se recarga.
|
|
503
|
+
|
|
504
|
+
Es seguro darlo: sólo un origen que el CORS permita puede leer la respuesta, y el valor
|
|
505
|
+
no sirve sin la cookie de sesión que el navegador ya tiene. Es `HMAC(secreto, sid)`, el
|
|
506
|
+
mismo cálculo que `emit_tokens`.
|
|
507
|
+
|
|
508
|
+
**401 sin sesión por cookie.** Un cliente Bearer no hace CSRF —adjunta su token a
|
|
509
|
+
propósito— y pedirlo es un error de uso, no algo que haya que contestar.
|
|
510
|
+
"""
|
|
511
|
+
from hexcore.darwin.application.container import get_identity_container
|
|
512
|
+
from hexcore.darwin.domain.exceptions import UnauthenticatedError
|
|
513
|
+
from hexcore.darwin.infrastructure.hashing import derive_csrf_token
|
|
514
|
+
|
|
515
|
+
config = get_identity_container().config
|
|
516
|
+
session_id = getattr(auth.actor, "session_id", None)
|
|
517
|
+
if auth.transport != "cookie" or session_id is None or config.secret_key is None:
|
|
518
|
+
raise UnauthenticatedError(
|
|
519
|
+
"El token anti-CSRF sólo existe para una sesión por cookie: un cliente Bearer "
|
|
520
|
+
"no hace CSRF."
|
|
521
|
+
)
|
|
522
|
+
|
|
523
|
+
response.headers["Cache-Control"] = "no-store"
|
|
524
|
+
return CsrfTokenResponse(
|
|
525
|
+
csrf_token=derive_csrf_token(str(session_id), config.secret_key.get_secret_value())
|
|
526
|
+
)
|
|
527
|
+
|
|
467
528
|
@router.get("/me", response_model=MeResponse)
|
|
468
529
|
async def me(auth: "AuthContext[t.Any]" = Depends(provide_auth)) -> MeResponse:
|
|
469
530
|
"""
|
|
@@ -37,6 +37,10 @@ _EXPORTS: dict[str, tuple[str, str]] = {
|
|
|
37
37
|
"RequestIDMiddleware",
|
|
38
38
|
),
|
|
39
39
|
"TimingMiddleware": ("hexcore.infrastructure.api.middlewares", "TimingMiddleware"),
|
|
40
|
+
"PredicateCORSMiddleware": (
|
|
41
|
+
"hexcore.infrastructure.api.middlewares",
|
|
42
|
+
"PredicateCORSMiddleware",
|
|
43
|
+
),
|
|
40
44
|
"RequestIDLogFilter": (
|
|
41
45
|
"hexcore.infrastructure.api.middlewares",
|
|
42
46
|
"RequestIDLogFilter",
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
#
|
|
10
10
|
# Existe porque la fachada resuelve sus exports con `__getattr__` y declara
|
|
11
11
|
# `__all__ = sorted(_EXPORTS)`: las dos son expresiones de runtime, así que sin este stub
|
|
12
|
-
# los
|
|
12
|
+
# los 49 símbolos de `hexcore.fastapi` tipan `Any`. El runtime no cambia — Python usa
|
|
13
13
|
# el `.py` y el checker usa el `.pyi`, así que la carga perezosa se mantiene.
|
|
14
14
|
|
|
15
15
|
|
|
@@ -42,6 +42,7 @@ from hexcore.infrastructure.api.lifespan import ProcrastinateStep as Procrastina
|
|
|
42
42
|
from hexcore.infrastructure.api.lifespan import SqlEngineStep as SqlEngineStep
|
|
43
43
|
from hexcore.infrastructure.api.lifespan import StartupStep as StartupStep
|
|
44
44
|
from hexcore.infrastructure.api.lifespan import build_lifespan as build_lifespan
|
|
45
|
+
from hexcore.infrastructure.api.middlewares import PredicateCORSMiddleware as PredicateCORSMiddleware
|
|
45
46
|
from hexcore.infrastructure.api.middlewares import RequestIDLogFilter as RequestIDLogFilter
|
|
46
47
|
from hexcore.infrastructure.api.middlewares import RequestIDMiddleware as RequestIDMiddleware
|
|
47
48
|
from hexcore.infrastructure.api.middlewares import TimingMiddleware as TimingMiddleware
|
|
@@ -74,6 +75,7 @@ __all__ = [
|
|
|
74
75
|
"EventBusStep",
|
|
75
76
|
"HealthReport",
|
|
76
77
|
"HealthRoutes",
|
|
78
|
+
"PredicateCORSMiddleware",
|
|
77
79
|
"Probe",
|
|
78
80
|
"ProcrastinateStep",
|
|
79
81
|
"RequestIDLogFilter",
|
|
@@ -16,7 +16,7 @@ from pydantic import BaseModel
|
|
|
16
16
|
|
|
17
17
|
from .exception_handlers import HeadersFactory, PayloadFactory, register_exception_handlers
|
|
18
18
|
from .health import Probe, ResponseFactory, register_health_routes
|
|
19
|
-
from .middlewares import RequestIDMiddleware, TimingMiddleware
|
|
19
|
+
from .middlewares import PredicateCORSMiddleware, RequestIDMiddleware, TimingMiddleware
|
|
20
20
|
from .routing import MountableRouter, mount_routers
|
|
21
21
|
|
|
22
22
|
__all__ = ["AppFeatures", "HealthRoutes", "create_app"]
|
|
@@ -139,14 +139,6 @@ def create_app(
|
|
|
139
139
|
# Los middlewares de Starlette se ejecutan en orden inverso al de registro, así que
|
|
140
140
|
# el request-id se añade último para que envuelva a todo lo demás y su ContextVar
|
|
141
141
|
# esté disponible en el resto de la pila.
|
|
142
|
-
if resolved_features.cors:
|
|
143
|
-
app.add_middleware(
|
|
144
|
-
CORSMiddleware,
|
|
145
|
-
allow_origins=list(getattr(config, "allow_origins", ["*"])),
|
|
146
|
-
allow_credentials=bool(getattr(config, "allow_credentials", True)),
|
|
147
|
-
allow_methods=list(getattr(config, "allow_methods", ["*"])),
|
|
148
|
-
allow_headers=list(getattr(config, "allow_headers", ["*"])),
|
|
149
|
-
)
|
|
150
142
|
if resolved_features.timing:
|
|
151
143
|
app.add_middleware(TimingMiddleware)
|
|
152
144
|
|
|
@@ -164,6 +156,39 @@ def create_app(
|
|
|
164
156
|
|
|
165
157
|
app.add_middleware(AuthContextMiddleware)
|
|
166
158
|
|
|
159
|
+
# El CORS va **después** de los de Darwin —o sea, por fuera de ellos— y antes del
|
|
160
|
+
# request-id. Un middleware sólo le agrega cabeceras a lo que responde por dentro de él:
|
|
161
|
+
# con el CORS más adentro que el CSRF, el 403 de un `POST` cross-origin sin token salía
|
|
162
|
+
# sin `Access-Control-Allow-Origin`, y el navegador lo convertía en un error de red opaco
|
|
163
|
+
# en vez de dejarle leer a la app *por qué* falló (y reintentar con un token nuevo). Y
|
|
164
|
+
# el preflight se contesta acá, antes de gastar la resolución de la credencial.
|
|
165
|
+
if resolved_features.cors:
|
|
166
|
+
expose_headers = list(getattr(config, "cors_expose_headers", []))
|
|
167
|
+
if resolved_features.csrf:
|
|
168
|
+
# Darwin entrega el token anti-CSRF también en esta cabecera (para la SPA en otro
|
|
169
|
+
# origen, que no puede leer la cookie): sin exponerla, el JS no la ve.
|
|
170
|
+
from hexcore.darwin.infrastructure.api.middlewares import CSRF_HEADER
|
|
171
|
+
|
|
172
|
+
if CSRF_HEADER.lower() not in (h.lower() for h in expose_headers):
|
|
173
|
+
expose_headers.append(CSRF_HEADER)
|
|
174
|
+
cors_kwargs: dict[str, t.Any] = {
|
|
175
|
+
"allow_origins": list(getattr(config, "allow_origins", ["*"])),
|
|
176
|
+
"allow_credentials": bool(getattr(config, "allow_credentials", True)),
|
|
177
|
+
"allow_methods": list(getattr(config, "allow_methods", ["*"])),
|
|
178
|
+
"allow_headers": list(getattr(config, "allow_headers", ["*"])),
|
|
179
|
+
"expose_headers": expose_headers,
|
|
180
|
+
"max_age": int(getattr(config, "cors_max_age", 600)),
|
|
181
|
+
}
|
|
182
|
+
origin_predicate = getattr(config, "cors_origin_predicate", None)
|
|
183
|
+
if origin_predicate is None:
|
|
184
|
+
# Sin predicado, la clase es la de Starlette tal cual: nada cambia para quien no
|
|
185
|
+
# lo pide.
|
|
186
|
+
app.add_middleware(CORSMiddleware, **cors_kwargs)
|
|
187
|
+
else:
|
|
188
|
+
app.add_middleware(
|
|
189
|
+
PredicateCORSMiddleware, origin_predicate=origin_predicate, **cors_kwargs
|
|
190
|
+
)
|
|
191
|
+
|
|
167
192
|
if resolved_features.request_id:
|
|
168
193
|
app.add_middleware(RequestIDMiddleware)
|
|
169
194
|
|
|
@@ -15,12 +15,16 @@ import uuid
|
|
|
15
15
|
import warnings
|
|
16
16
|
from contextvars import ContextVar
|
|
17
17
|
|
|
18
|
+
from starlette.datastructures import Headers, MutableHeaders
|
|
18
19
|
from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint
|
|
20
|
+
from starlette.middleware.cors import CORSMiddleware
|
|
19
21
|
from starlette.requests import Request
|
|
20
22
|
from starlette.responses import Response
|
|
23
|
+
from starlette.types import Message, Receive, Scope, Send
|
|
21
24
|
|
|
22
25
|
__all__ = [
|
|
23
26
|
"REQUEST_ID",
|
|
27
|
+
"PredicateCORSMiddleware",
|
|
24
28
|
"RequestIDMiddleware",
|
|
25
29
|
"TimingMiddleware",
|
|
26
30
|
"RequestIDLogFilter",
|
|
@@ -102,6 +106,66 @@ class TimingMiddleware(BaseHTTPMiddleware):
|
|
|
102
106
|
return response
|
|
103
107
|
|
|
104
108
|
|
|
109
|
+
cors_logger = logging.getLogger("hexcore.api.cors")
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class PredicateCORSMiddleware(CORSMiddleware):
|
|
113
|
+
"""
|
|
114
|
+
`CORSMiddleware` que además acepta los orígenes que un predicado apruebe.
|
|
115
|
+
|
|
116
|
+
`allow_origins` es una lista fija, y un subdominio por tenant creado en caliente no se puede
|
|
117
|
+
enumerar de antemano: sin esto hay que redesplegar por cada alta, o abrir `"*"` — que con
|
|
118
|
+
credenciales no es válido. El predicado se consulta **sólo** cuando el origen no está ya en
|
|
119
|
+
la lista, así que declarar los dos no duplica trabajo.
|
|
120
|
+
|
|
121
|
+
Dos decisiones que no son obvias:
|
|
122
|
+
|
|
123
|
+
* **Falla cerrado.** Un predicado que lanza se trata como «no confiable» y se loguea en
|
|
124
|
+
`hexcore.api.cors`: un chequeo de confianza que explota nunca decide «confío» por default.
|
|
125
|
+
El log importa porque el síntoma —un origen legítimo sin CORS— no apunta a ninguna causa.
|
|
126
|
+
* **`Vary: Origin` siempre.** Con orígenes dinámicos la respuesta depende del `Origin`
|
|
127
|
+
también cuando se rechaza; sin `Vary`, una caché compartida podría servirle a un origen
|
|
128
|
+
aceptado la respuesta que guardó sin `Access-Control-Allow-Origin`. Starlette sólo lo
|
|
129
|
+
agrega cuando el origen es aceptado.
|
|
130
|
+
"""
|
|
131
|
+
|
|
132
|
+
def __init__(
|
|
133
|
+
self,
|
|
134
|
+
app: t.Any,
|
|
135
|
+
*,
|
|
136
|
+
origin_predicate: t.Callable[[str], bool],
|
|
137
|
+
**kwargs: t.Any,
|
|
138
|
+
) -> None:
|
|
139
|
+
super().__init__(app, **kwargs)
|
|
140
|
+
self._origin_predicate = origin_predicate
|
|
141
|
+
|
|
142
|
+
def is_allowed_origin(self, origin: str) -> bool:
|
|
143
|
+
if super().is_allowed_origin(origin):
|
|
144
|
+
return True
|
|
145
|
+
try:
|
|
146
|
+
return bool(self._origin_predicate(origin))
|
|
147
|
+
except Exception:
|
|
148
|
+
cors_logger.exception(
|
|
149
|
+
"cors_origin_predicate lanzó evaluando el origen %r; se lo trata como no "
|
|
150
|
+
"confiable.",
|
|
151
|
+
origin,
|
|
152
|
+
)
|
|
153
|
+
return False
|
|
154
|
+
|
|
155
|
+
async def simple_response(
|
|
156
|
+
self, scope: Scope, receive: Receive, send: Send, request_headers: Headers
|
|
157
|
+
) -> None:
|
|
158
|
+
async def con_vary(message: Message) -> None:
|
|
159
|
+
if message["type"] == "http.response.start":
|
|
160
|
+
message.setdefault("headers", [])
|
|
161
|
+
headers = MutableHeaders(scope=message)
|
|
162
|
+
if "origin" not in headers.get("vary", "").lower():
|
|
163
|
+
headers.add_vary_header("Origin")
|
|
164
|
+
await send(message)
|
|
165
|
+
|
|
166
|
+
await super().simple_response(scope, receive, con_vary, request_headers)
|
|
167
|
+
|
|
168
|
+
|
|
105
169
|
class RequestIDLogFilter(logging.Filter):
|
|
106
170
|
"""
|
|
107
171
|
Inyecta el request-id en cada `LogRecord` como ``record.request_id``.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: hexcore
|
|
3
|
-
Version: 11.
|
|
3
|
+
Version: 11.3.0
|
|
4
4
|
Summary: Reusable core for Python projects with hexagonal architecture, DDD, CQRS and background tasks. Ships the abstractions and the infrastructure: SQL session layer, FastAPI factories, worker runner, dynamic cron and identity.
|
|
5
5
|
Author-email: "David Latosefki (Indroic)" <indroic@outlook.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -290,6 +290,7 @@ hexcore/testing/repositories.py
|
|
|
290
290
|
tests/test_alembic_metadata_completeness.py
|
|
291
291
|
tests/test_anclas_del_repo.py
|
|
292
292
|
tests/test_api_app_and_lifespan.py
|
|
293
|
+
tests/test_api_cors_dinamico.py
|
|
293
294
|
tests/test_api_cqrs_providers.py
|
|
294
295
|
tests/test_api_exception_handlers.py
|
|
295
296
|
tests/test_api_health.py
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CORS de `create_app`: por fuera de la auth y del CSRF, y con orígenes que no se enumeran.
|
|
3
|
+
|
|
4
|
+
Dos problemas del mismo caso de uso —una SPA en otro origen que le habla a la API con la cookie
|
|
5
|
+
de sesión—, y los dos fallan **en silencio**:
|
|
6
|
+
|
|
7
|
+
1. `create_app` registraba el CORS **primero**, o sea que quedaba *por dentro* de
|
|
8
|
+
`AuthContextMiddleware` y de `CsrfMiddleware`. Un 403 de CSRF salía sin
|
|
9
|
+
`Access-Control-Allow-Origin` y el navegador lo convertía en un error de red opaco: la app
|
|
10
|
+
no podía leer *por qué* falló (ni reintentar con un token nuevo). El orden se prueba acá; que
|
|
11
|
+
el 403 real lleve las cabeceras, en `test_darwin_http.py`.
|
|
12
|
+
2. `allow_origins` es una lista fija. Con un subdominio por tenant creado en caliente hay que
|
|
13
|
+
redesplegar por cada alta, o abrir `"*"` —que con credenciales no es válido—. Un
|
|
14
|
+
`cors_origin_predicate` cubre lo que la lista no enumera, igual que
|
|
15
|
+
`IdentityConfig.trusted_origin_predicate` hace con el CSRF.
|
|
16
|
+
|
|
17
|
+
Además, sin `cors_max_age` cada escritura JSON con un header propio (`X-CSRF-Token`) cuesta un
|
|
18
|
+
`OPTIONS` previo sin caché, y sin `cors_expose_headers` el JS no puede leer las cabeceras de la
|
|
19
|
+
respuesta que la app le quiere entregar.
|
|
20
|
+
"""
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import logging
|
|
24
|
+
import warnings
|
|
25
|
+
|
|
26
|
+
import pytest
|
|
27
|
+
|
|
28
|
+
pytest.importorskip("fastapi")
|
|
29
|
+
pytest.importorskip("httpx")
|
|
30
|
+
|
|
31
|
+
from fastapi.testclient import TestClient # noqa: E402
|
|
32
|
+
from pydantic import ValidationError # noqa: E402
|
|
33
|
+
from starlette.middleware.cors import CORSMiddleware # noqa: E402
|
|
34
|
+
|
|
35
|
+
from hexcore.config import ServerConfig # noqa: E402
|
|
36
|
+
from hexcore.fastapi import AppFeatures, create_app # noqa: E402
|
|
37
|
+
from hexcore.infrastructure.api.middlewares import PredicateCORSMiddleware # noqa: E402
|
|
38
|
+
|
|
39
|
+
TENANT = "https://a.tenant.test"
|
|
40
|
+
AJENO = "https://evil.example"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _es_de_un_tenant(origen: str) -> bool:
|
|
44
|
+
return origen.endswith(".tenant.test")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _app(monkeypatch: pytest.MonkeyPatch, **campos):
|
|
48
|
+
"""Una app con una `ServerConfig` propia (el `_config()` de `create_app` apunta a ella)."""
|
|
49
|
+
config = ServerConfig(**campos)
|
|
50
|
+
monkeypatch.setattr("hexcore.infrastructure.api.app._config", lambda: config)
|
|
51
|
+
return create_app(), config
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _nombres(app) -> list[str]:
|
|
55
|
+
"""De **afuera hacia adentro**: `user_middleware[0]` es el primero que ve el request."""
|
|
56
|
+
return [m.cls.__name__ for m in app.user_middleware]
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
# ── 1. Orden ───────────────────────────────────────────────────────────────────
|
|
60
|
+
def test_el_cors_envuelve_a_la_auth_y_al_csrf(monkeypatch):
|
|
61
|
+
"""
|
|
62
|
+
Un middleware de Starlette sólo puede agregarle cabeceras a lo que responde **por dentro**
|
|
63
|
+
de él. Si el CORS está adentro del CSRF, el 403 del CSRF no las lleva.
|
|
64
|
+
"""
|
|
65
|
+
config = ServerConfig(allow_origins=[TENANT])
|
|
66
|
+
monkeypatch.setattr("hexcore.infrastructure.api.app._config", lambda: config)
|
|
67
|
+
|
|
68
|
+
app = create_app(features=AppFeatures(auth_context=True, csrf=True))
|
|
69
|
+
nombres = _nombres(app)
|
|
70
|
+
|
|
71
|
+
assert nombres.index("CORSMiddleware") < nombres.index("AuthContextMiddleware")
|
|
72
|
+
assert nombres.index("CORSMiddleware") < nombres.index("CsrfMiddleware")
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def test_el_request_id_sigue_siendo_el_mas_externo(monkeypatch):
|
|
76
|
+
"""
|
|
77
|
+
El request-id envuelve a todo lo demás para que su `ContextVar` esté disponible en la pila
|
|
78
|
+
entera, y el 403 del CSRF —que ahora sale por dentro del CORS— lo sigue llevando.
|
|
79
|
+
"""
|
|
80
|
+
config = ServerConfig(allow_origins=[TENANT])
|
|
81
|
+
monkeypatch.setattr("hexcore.infrastructure.api.app._config", lambda: config)
|
|
82
|
+
|
|
83
|
+
app = create_app(features=AppFeatures(auth_context=True, csrf=True))
|
|
84
|
+
|
|
85
|
+
assert _nombres(app)[0] == "RequestIDMiddleware"
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def test_sin_predicado_el_middleware_es_el_de_starlette_tal_cual(monkeypatch):
|
|
89
|
+
"""Quien no pide un predicado no cambia de clase: el comportamiento es el de siempre."""
|
|
90
|
+
app, _ = _app(monkeypatch, allow_origins=[TENANT])
|
|
91
|
+
|
|
92
|
+
clases = [m.cls for m in app.user_middleware]
|
|
93
|
+
|
|
94
|
+
assert CORSMiddleware in clases
|
|
95
|
+
assert PredicateCORSMiddleware not in clases
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
# ── 2. cors_origin_predicate ───────────────────────────────────────────────────
|
|
99
|
+
def test_un_origen_que_el_predicado_acepta_recibe_cors_con_credenciales(monkeypatch):
|
|
100
|
+
app, _ = _app(monkeypatch, allow_origins=[], cors_origin_predicate=_es_de_un_tenant)
|
|
101
|
+
|
|
102
|
+
with TestClient(app) as client:
|
|
103
|
+
r = client.get("/health", headers={"Origin": TENANT})
|
|
104
|
+
|
|
105
|
+
assert r.headers["access-control-allow-origin"] == TENANT
|
|
106
|
+
assert r.headers["access-control-allow-credentials"] == "true"
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def test_un_origen_que_el_predicado_rechaza_no_recibe_cors(monkeypatch):
|
|
110
|
+
app, _ = _app(monkeypatch, allow_origins=[], cors_origin_predicate=_es_de_un_tenant)
|
|
111
|
+
|
|
112
|
+
with TestClient(app) as client:
|
|
113
|
+
r = client.get("/health", headers={"Origin": AJENO})
|
|
114
|
+
|
|
115
|
+
assert "access-control-allow-origin" not in r.headers
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def test_el_preflight_de_un_origen_aceptado_pasa_y_el_de_uno_rechazado_no(monkeypatch):
|
|
119
|
+
app, _ = _app(monkeypatch, allow_origins=[], cors_origin_predicate=_es_de_un_tenant)
|
|
120
|
+
pedido = {
|
|
121
|
+
"Access-Control-Request-Method": "POST",
|
|
122
|
+
"Access-Control-Request-Headers": "content-type, x-csrf-token",
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
with TestClient(app) as client:
|
|
126
|
+
bueno = client.options("/health", headers={"Origin": TENANT, **pedido})
|
|
127
|
+
malo = client.options("/health", headers={"Origin": AJENO, **pedido})
|
|
128
|
+
|
|
129
|
+
assert bueno.status_code == 200
|
|
130
|
+
assert bueno.headers["access-control-allow-origin"] == TENANT
|
|
131
|
+
assert malo.status_code == 400
|
|
132
|
+
assert "access-control-allow-origin" not in malo.headers
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def test_la_lista_fija_y_el_predicado_se_suman(monkeypatch):
|
|
136
|
+
"""La lista sigue mandando para lo que enumera; el predicado cubre el resto."""
|
|
137
|
+
fijo = "https://portal.example"
|
|
138
|
+
app, _ = _app(monkeypatch, allow_origins=[fijo], cors_origin_predicate=_es_de_un_tenant)
|
|
139
|
+
|
|
140
|
+
with TestClient(app) as client:
|
|
141
|
+
de_la_lista = client.get("/health", headers={"Origin": fijo})
|
|
142
|
+
del_predicado = client.get("/health", headers={"Origin": TENANT})
|
|
143
|
+
ajeno = client.get("/health", headers={"Origin": AJENO})
|
|
144
|
+
|
|
145
|
+
assert de_la_lista.headers["access-control-allow-origin"] == fijo
|
|
146
|
+
assert del_predicado.headers["access-control-allow-origin"] == TENANT
|
|
147
|
+
assert "access-control-allow-origin" not in ajeno.headers
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def test_un_predicado_que_lanza_se_trata_como_no_confiable(monkeypatch, caplog):
|
|
151
|
+
"""
|
|
152
|
+
Falla cerrado, igual que `trusted_origin_predicate` y que un `AuthorizationProvider` que
|
|
153
|
+
revienta: un chequeo de confianza que explota nunca decide «confío» por default. Y se
|
|
154
|
+
loguea, porque el síntoma —un origen legítimo sin CORS— si no, no se diagnostica.
|
|
155
|
+
"""
|
|
156
|
+
|
|
157
|
+
def explota(origen: str) -> bool:
|
|
158
|
+
raise RuntimeError("la base de tenants no responde")
|
|
159
|
+
|
|
160
|
+
app, _ = _app(monkeypatch, allow_origins=[], cors_origin_predicate=explota)
|
|
161
|
+
|
|
162
|
+
with caplog.at_level(logging.ERROR, logger="hexcore.api.cors"):
|
|
163
|
+
with TestClient(app) as client:
|
|
164
|
+
r = client.get("/health", headers={"Origin": TENANT})
|
|
165
|
+
|
|
166
|
+
assert r.status_code == 200
|
|
167
|
+
assert "access-control-allow-origin" not in r.headers
|
|
168
|
+
assert any("cors_origin_predicate" in m for m in caplog.messages)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def test_la_respuesta_a_un_origen_rechazado_igual_declara_vary_origin(monkeypatch):
|
|
172
|
+
"""
|
|
173
|
+
Con orígenes dinámicos la respuesta depende del `Origin` **también cuando se rechaza**. Sin
|
|
174
|
+
`Vary: Origin`, una caché compartida podría servirle a un origen aceptado la respuesta que
|
|
175
|
+
guardó sin `Access-Control-Allow-Origin`.
|
|
176
|
+
"""
|
|
177
|
+
app, _ = _app(monkeypatch, allow_origins=[], cors_origin_predicate=_es_de_un_tenant)
|
|
178
|
+
|
|
179
|
+
with TestClient(app) as client:
|
|
180
|
+
r = client.get("/health", headers={"Origin": AJENO})
|
|
181
|
+
|
|
182
|
+
assert "Origin" in r.headers["vary"]
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def test_con_predicado_y_sin_allow_origins_no_se_deriva_el_comodin(monkeypatch):
|
|
186
|
+
"""
|
|
187
|
+
El default de `allow_origins` en `debug` es `["*"]`, y con credenciales eso **baja
|
|
188
|
+
`allow_credentials` a `False` en silencio** (con un warning). Con un predicado declarado, el
|
|
189
|
+
predicado es la lista: derivar `["*"]` apagaría las cookies sin que nadie lo pidiera.
|
|
190
|
+
"""
|
|
191
|
+
with warnings.catch_warnings(record=True) as avisos:
|
|
192
|
+
warnings.simplefilter("always")
|
|
193
|
+
config = ServerConfig(cors_origin_predicate=_es_de_un_tenant)
|
|
194
|
+
|
|
195
|
+
assert config.allow_origins == []
|
|
196
|
+
assert config.allow_credentials is True
|
|
197
|
+
assert not [a for a in avisos if "allow_credentials" in str(a.message)]
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def test_un_comodin_explicito_sigue_bajando_las_credenciales():
|
|
201
|
+
"""Lo que ya no era válido sigue sin serlo: el predicado no relaja esa regla."""
|
|
202
|
+
with pytest.warns(UserWarning, match="allow_credentials"):
|
|
203
|
+
config = ServerConfig(allow_origins=["*"], cors_origin_predicate=_es_de_un_tenant)
|
|
204
|
+
|
|
205
|
+
assert config.allow_credentials is False
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
# ── 3. cors_max_age y cors_expose_headers ──────────────────────────────────────
|
|
209
|
+
def test_cors_max_age_llega_al_preflight(monkeypatch):
|
|
210
|
+
app, _ = _app(monkeypatch, allow_origins=[TENANT], cors_max_age=7200)
|
|
211
|
+
|
|
212
|
+
with TestClient(app) as client:
|
|
213
|
+
r = client.options(
|
|
214
|
+
"/health",
|
|
215
|
+
headers={"Origin": TENANT, "Access-Control-Request-Method": "POST"},
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
assert r.headers["access-control-max-age"] == "7200"
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def test_el_max_age_por_defecto_es_el_de_starlette(monkeypatch):
|
|
222
|
+
"""Quien no lo declara no ve cambiar nada: 600 s, el default de `CORSMiddleware`."""
|
|
223
|
+
app, config = _app(monkeypatch, allow_origins=[TENANT])
|
|
224
|
+
|
|
225
|
+
with TestClient(app) as client:
|
|
226
|
+
r = client.options(
|
|
227
|
+
"/health",
|
|
228
|
+
headers={"Origin": TENANT, "Access-Control-Request-Method": "GET"},
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
assert config.cors_max_age == 600
|
|
232
|
+
assert r.headers["access-control-max-age"] == "600"
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def test_un_max_age_negativo_no_es_valido():
|
|
236
|
+
with pytest.raises(ValidationError):
|
|
237
|
+
ServerConfig(cors_max_age=-1)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def test_cors_expose_headers_se_declara_en_la_respuesta(monkeypatch):
|
|
241
|
+
app, _ = _app(
|
|
242
|
+
monkeypatch,
|
|
243
|
+
allow_origins=[TENANT],
|
|
244
|
+
cors_expose_headers=["X-Request-ID", "Retry-After"],
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
with TestClient(app) as client:
|
|
248
|
+
r = client.get("/health", headers={"Origin": TENANT})
|
|
249
|
+
|
|
250
|
+
assert r.headers["access-control-expose-headers"] == "X-Request-ID, Retry-After"
|