fastapi-rtk 2.12.3__tar.gz → 2.12.4__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.
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/PKG-INFO +1 -1
- fastapi_rtk-2.12.4/fastapi_rtk/_version.py +1 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/bases/__init__.py +0 -1
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/bases/interface.py +3 -68
- fastapi_rtk-2.12.4/fastapi_rtk/exceptions.py +235 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/globals.py +21 -2
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/conftest.py +67 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/unit/bases/test_interface.py +76 -5
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/unit/test_exceptions.py +30 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/unit/test_globals.py +38 -0
- fastapi_rtk-2.12.3/fastapi_rtk/_version.py +0 -1
- fastapi_rtk-2.12.3/fastapi_rtk/exceptions.py +0 -80
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/.gitignore +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/LICENSE +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/README.md +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/__init__.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/api/__init__.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/api/base_api.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/api/model_rest_api/__init__.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/api/model_rest_api/_add_mixin.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/api/model_rest_api/_bulk_mixin.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/api/model_rest_api/_columns_mixin.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/api/model_rest_api/_delete_mixin.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/api/model_rest_api/_download_mixin.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/api/model_rest_api/_edit_mixin.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/api/model_rest_api/_file_mixin.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/api/model_rest_api/_info_mixin.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/api/model_rest_api/_list_mixin.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/api/model_rest_api/_params.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/api/model_rest_api/_show_mixin.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/api/model_rest_api/_typed_base.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/api/model_rest_api/_upload_mixin.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/apis/__init__.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/apis/info.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/apis/license.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/auth/__init__.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/auth/auth.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/auth/hashers/__init__.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/auth/hashers/pbkdf2.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/auth/hashers/scrypt.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/auth/ldap.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/auth/password_helpers/__init__.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/auth/password_helpers/fab.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/auth/strategies/__init__.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/auth/strategies/config.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/auth/strategies/db.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/auth/strategies/jwt.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/backends/__init__.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/backends/generic/__init__.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/backends/generic/column.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/backends/generic/db.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/backends/generic/exceptions.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/backends/generic/filters.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/backends/generic/interface.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/backends/generic/model.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/backends/generic/session.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/backends/sqla/__init__.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/backends/sqla/column.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/backends/sqla/db.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/backends/sqla/exceptions.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/backends/sqla/extensions/__init__.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/backends/sqla/extensions/audit/__init__.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/backends/sqla/extensions/audit/_decorators_mixin.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/backends/sqla/extensions/audit/_lifecycle_mixin.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/backends/sqla/extensions/audit/_logger.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/backends/sqla/extensions/audit/_processing_mixin.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/backends/sqla/extensions/audit/audit.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/backends/sqla/extensions/audit/types.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/backends/sqla/extensions/geoalchemy2/__init__.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/backends/sqla/extensions/geoalchemy2/filters.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/backends/sqla/extensions/geoalchemy2/geometry_converter.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/backends/sqla/filters.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/backends/sqla/interface.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/backends/sqla/model.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/backends/sqla/session.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/bases/cache.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/bases/db.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/bases/file_manager.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/bases/filter.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/bases/loop_manager.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/bases/model.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/bases/session.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/bases/user_cache.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/caches/__init__.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/caches/cache.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/caches/serializers.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/caches/storages.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/cli/__init__.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/cli/cli.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/cli/commands/__init__.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/cli/commands/_security_crypto.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/cli/commands/db/__init__.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/cli/commands/db/templates/fastapi/README +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/cli/commands/db/templates/fastapi/alembic.ini.mako +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/cli/commands/db/templates/fastapi/env.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/cli/commands/db/templates/fastapi/script.py.mako +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/cli/commands/db/templates/fastapi-multidb/README +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/cli/commands/db/templates/fastapi-multidb/alembic.ini.mako +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/cli/commands/db/templates/fastapi-multidb/env.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/cli/commands/db/templates/fastapi-multidb/script.py.mako +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/cli/commands/export.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/cli/commands/security.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/cli/commands/translate.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/cli/const.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/cli/decorators.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/cli/types.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/cli/utils.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/config.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/const.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/db.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/decorators.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/dependencies.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/fastapi_react_toolkit.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/file_managers/__init__.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/file_managers/file_manager.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/file_managers/image_manager.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/file_managers/s3_file_manager.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/file_managers/s3_image_manager.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/filters.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/lang/__init__.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/lang/babel/__init__.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/lang/babel/cli.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/lang/babel/config.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/lang/babel.cfg +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/lang/lazy_text.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/lang/messages.pot +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/lang/translations/de/LC_MESSAGES/messages.mo +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/lang/translations/de/LC_MESSAGES/messages.po +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/lang/translations/en/LC_MESSAGES/messages.mo +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/lang/translations/en/LC_MESSAGES/messages.po +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/loop_managers/__init__.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/loop_managers/loop_manager.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/manager.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/middlewares.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/mixins.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/models.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/py.typed +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/registry.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/routers.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/schemas.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/security/__init__.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/security/sqla/__init__.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/security/sqla/apis.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/security/sqla/models.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/security/sqla/security_manager/__init__.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/security/sqla/security_manager/_association_mixin.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/security/sqla/security_manager/_builtin_role_mixin.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/security/sqla/security_manager/_cleanup_mixin.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/security/sqla/security_manager/_exception_filters.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/security/sqla/security_manager/_export.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/security/sqla/security_manager/_import.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/security/sqla/security_manager/_role_mixin.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/security/sqla/security_manager/_user_mixin.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/security/sqla/user_cache.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/setting.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/types.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/utils/__init__.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/utils/async_task_runner.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/utils/class_factory.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/utils/csv_json_converter.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/utils/deep_merge.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/utils/extender_mixin.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/utils/flask_appbuilder_utils.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/utils/formatter.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/utils/hooks.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/utils/lazy.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/utils/merge_schema.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/utils/multiple_async_contexts.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/utils/pydantic.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/utils/run_utils.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/utils/self_dependencies.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/utils/smartdefaultdict.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/utils/sqla.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/utils/timezone.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/utils/update_signature.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/utils/use_default_when_none.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/fastapi_rtk/version.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/pyproject.toml +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/docker-compose.gis.yml +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/integration/__init__.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/integration/api/__init__.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/integration/api/test_model_rest_api.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/integration/apis/__init__.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/integration/apis/test_info_callable_schema.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/integration/apis/test_info_contract.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/integration/auth/__init__.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/integration/auth/test_auth_flow.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/integration/auth/test_oauth.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/integration/conftest.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/integration/file_managers/__init__.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/integration/file_managers/test_file_routes.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/integration/security/__init__.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/integration/security/sqla/__init__.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/integration/security/sqla/test_security_manager.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/integration/test_app_smoke.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/unit/__init__.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/unit/api/__init__.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/unit/api/conftest.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/unit/api/model_rest_api/__init__.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/unit/api/model_rest_api/test_columns.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/unit/api/model_rest_api/test_file_mixin.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/unit/api/model_rest_api/test_init.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/unit/api/model_rest_api/test_typed_base_consistency.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/unit/api/test_base_api.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/unit/apis/__init__.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/unit/apis/test_info.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/unit/apis/test_license.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/unit/auth/__init__.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/unit/auth/hashers/__init__.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/unit/auth/hashers/test_pbkdf2.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/unit/auth/hashers/test_scrypt.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/unit/auth/password_helpers/__init__.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/unit/auth/password_helpers/test_fab.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/unit/auth/strategies/__init__.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/unit/auth/strategies/test_config.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/unit/auth/strategies/test_db.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/unit/auth/strategies/test_jwt.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/unit/auth/test_auth.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/unit/backends/__init__.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/unit/backends/generic/__init__.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/unit/backends/generic/test_column.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/unit/backends/generic/test_db.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/unit/backends/generic/test_exceptions.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/unit/backends/generic/test_filters.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/unit/backends/generic/test_interface.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/unit/backends/generic/test_model.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/unit/backends/generic/test_session.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/unit/backends/sqla/__init__.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/unit/backends/sqla/conftest.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/unit/backends/sqla/test_audit.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/unit/backends/sqla/test_audit_types.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/unit/backends/sqla/test_column.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/unit/backends/sqla/test_db.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/unit/backends/sqla/test_filters.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/unit/backends/sqla/test_geoalchemy2.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/unit/backends/sqla/test_interface.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/unit/backends/sqla/test_model.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/unit/backends/sqla/test_session.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/unit/bases/__init__.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/unit/bases/test_db.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/unit/bases/test_file_manager.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/unit/bases/test_filter.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/unit/bases/test_model.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/unit/bases/test_session.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/unit/cli/__init__.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/unit/cli/commands/__init__.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/unit/cli/commands/conftest.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/unit/cli/commands/test_db.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/unit/cli/commands/test_export.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/unit/cli/commands/test_security.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/unit/cli/commands/test_security_crypto.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/unit/cli/commands/test_translate.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/unit/cli/conftest.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/unit/cli/test_cli.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/unit/cli/test_decorators.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/unit/cli/test_types.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/unit/cli/test_utils.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/unit/file_managers/__init__.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/unit/file_managers/conftest.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/unit/file_managers/test_file_manager.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/unit/file_managers/test_image_manager.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/unit/file_managers/test_s3_file_manager.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/unit/file_managers/test_s3_image_manager.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/unit/lang/__init__.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/unit/lang/test_babel_cli.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/unit/lang/test_babel_config.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/unit/lang/test_lazy_text.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/unit/security/__init__.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/unit/security/sqla/__init__.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/unit/security/sqla/conftest.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/unit/security/sqla/test_apis.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/unit/security/sqla/test_models.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/unit/security/sqla/test_security_manager.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/unit/test_caches.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/unit/test_config.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/unit/test_const.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/unit/test_db.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/unit/test_decorators.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/unit/test_dependencies.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/unit/test_fastapi_react_toolkit.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/unit/test_filters.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/unit/test_manager.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/unit/test_middlewares.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/unit/test_mixins.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/unit/test_models.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/unit/test_registry.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/unit/test_routers.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/unit/test_schemas.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/unit/test_setting.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/unit/test_types.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/unit/test_user_cache.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/unit/test_version.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/unit/utils/__init__.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/unit/utils/test_async_task_runner.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/unit/utils/test_class_factory.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/unit/utils/test_csv_json_converter.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/unit/utils/test_deep_merge.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/unit/utils/test_extender_mixin.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/unit/utils/test_flask_appbuilder_utils.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/unit/utils/test_formatter.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/unit/utils/test_hooks.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/unit/utils/test_lazy.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/unit/utils/test_merge_schema.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/unit/utils/test_multiple_async_contexts.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/unit/utils/test_pydantic.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/unit/utils/test_run_utils.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/unit/utils/test_self_dependencies.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/unit/utils/test_smartdefaultdict.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/unit/utils/test_sqla.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/unit/utils/test_timezone.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/unit/utils/test_update_signature.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/tests/unit/utils/test_use_default_when_none.py +0 -0
- {fastapi_rtk-2.12.3 → fastapi_rtk-2.12.4}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: fastapi-rtk
|
|
3
|
-
Version: 2.12.
|
|
3
|
+
Version: 2.12.4
|
|
4
4
|
Summary: A package that provides a set of tools to build a FastAPI application with a Class-Based CRUD API.
|
|
5
5
|
Project-URL: Homepage, https://codeberg.org/datatactics/fastapi-rtk
|
|
6
6
|
Project-URL: Issues, https://codeberg.org/datatactics/fastapi-rtk/issues
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "2.12.4"
|
|
@@ -11,7 +11,7 @@ import pydantic
|
|
|
11
11
|
import sqlalchemy.exc
|
|
12
12
|
|
|
13
13
|
from ..const import logger
|
|
14
|
-
from ..exceptions import
|
|
14
|
+
from ..exceptions import MissingGreenletError, raise_exception
|
|
15
15
|
from ..globals import g
|
|
16
16
|
from ..schemas import PRIMARY_KEY, ColumnEnumInfo, ColumnInfo, ColumnRelationInfo
|
|
17
17
|
from ..utils import AsyncTaskRunner, C, T, deep_merge, lazy_self, smart_run
|
|
@@ -32,7 +32,6 @@ if typing.TYPE_CHECKING:
|
|
|
32
32
|
__all__ = [
|
|
33
33
|
"PydanticGenerationSchema",
|
|
34
34
|
"AbstractInterface",
|
|
35
|
-
"MissingGreenletError",
|
|
36
35
|
]
|
|
37
36
|
|
|
38
37
|
|
|
@@ -56,70 +55,6 @@ class PydanticGenerationSchema(typing.TypedDict):
|
|
|
56
55
|
name_: tuple[type[str], pydantic.Field] | None
|
|
57
56
|
|
|
58
57
|
|
|
59
|
-
class MissingGreenletError(FastAPIReactToolkitException):
|
|
60
|
-
"""Raised when an ORM attribute access during pydantic schema validation
|
|
61
|
-
triggers a SQLAlchemy lazy-load outside the asyncio greenlet.
|
|
62
|
-
|
|
63
|
-
Covers both async-property columns (awaited explicitly by
|
|
64
|
-
``_populate_async_column``) and regular columns / relationships read by
|
|
65
|
-
pydantic's ``from_attributes`` machinery.
|
|
66
|
-
|
|
67
|
-
The most common causes are:
|
|
68
|
-
|
|
69
|
-
- The ORM instance is detached from its session (commit/close happened
|
|
70
|
-
before the schema was rendered).
|
|
71
|
-
- The async session was expired between the query and the
|
|
72
|
-
response-serialisation step.
|
|
73
|
-
- A relationship the schema reads has not been eager-loaded. Prefer
|
|
74
|
-
``selectinload`` / ``joinedload`` on the query, call
|
|
75
|
-
``await instance.load("<col>")`` before returning the instance, or
|
|
76
|
-
add the column to ``list_columns`` / ``show_columns`` (or the
|
|
77
|
-
narrower ``list_select_columns`` / ``show_select_columns``, or
|
|
78
|
-
``extra_list_fetch_columns`` / ``extra_show_fetch_columns`` when
|
|
79
|
-
the column should be fetched but not surfaced in the response) on
|
|
80
|
-
the ``ModelRestApi`` subclass so the toolkit eager-loads it for
|
|
81
|
-
you.
|
|
82
|
-
"""
|
|
83
|
-
|
|
84
|
-
def __init__(self, *, model, col: str | None, original: Exception):
|
|
85
|
-
self.model = model
|
|
86
|
-
self.col = col
|
|
87
|
-
self.original = original
|
|
88
|
-
model_label = type(model).__name__
|
|
89
|
-
if col is None:
|
|
90
|
-
super().__init__(
|
|
91
|
-
f"Failed to validate schema for '{model_label}': SQLAlchemy "
|
|
92
|
-
f"raised MissingGreenlet while pydantic accessed an attribute. "
|
|
93
|
-
f"Fix by eager-loading the missing relationship "
|
|
94
|
-
f"(`selectinload` / `joinedload`), by calling "
|
|
95
|
-
f'`await instance.load("<col>")` before the instance leaves '
|
|
96
|
-
f"the async context, or by adding the column to "
|
|
97
|
-
f"`list_columns` / `show_columns` (or the narrower "
|
|
98
|
-
f"`list_select_columns` / `show_select_columns`, or "
|
|
99
|
-
f"`extra_list_fetch_columns` / `extra_show_fetch_columns` "
|
|
100
|
-
f"when the column should be fetched but not surfaced in the "
|
|
101
|
-
f"response) on the ModelRestApi subclass. Original error: "
|
|
102
|
-
f"{original!r}."
|
|
103
|
-
)
|
|
104
|
-
else:
|
|
105
|
-
super().__init__(
|
|
106
|
-
f"Failed to populate column '{col}' on '{model_label}': "
|
|
107
|
-
f"SQLAlchemy raised MissingGreenlet. The ORM attempted I/O "
|
|
108
|
-
f"outside the async greenlet. Fix by eager-loading the "
|
|
109
|
-
f"underlying relationship "
|
|
110
|
-
f"(e.g. add `selectinload({model_label}.{col})` or "
|
|
111
|
-
f"`joinedload({model_label}.{col})` to the query), by "
|
|
112
|
-
f'calling `await instance.load("{col}")` before the instance '
|
|
113
|
-
f'leaves the async context, or by adding `"{col}"` to '
|
|
114
|
-
f"`list_columns` / `show_columns` (or the narrower "
|
|
115
|
-
f"`list_select_columns` / `show_select_columns`, or "
|
|
116
|
-
f"`extra_list_fetch_columns` / `extra_show_fetch_columns` "
|
|
117
|
-
f"when the column should be fetched but not surfaced in the "
|
|
118
|
-
f"response) on the ModelRestApi subclass. Original error: "
|
|
119
|
-
f"{original!r}."
|
|
120
|
-
)
|
|
121
|
-
|
|
122
|
-
|
|
123
58
|
class lazy(lazy_self["AbstractInterface", T]): ...
|
|
124
59
|
|
|
125
60
|
|
|
@@ -1081,7 +1016,7 @@ class AbstractInterface(abc.ABC, typing.Generic[T, C, CT]):
|
|
|
1081
1016
|
try:
|
|
1082
1017
|
value = await getattr(model, col)
|
|
1083
1018
|
except sqlalchemy.exc.MissingGreenlet as e:
|
|
1084
|
-
raise MissingGreenletError(
|
|
1019
|
+
raise MissingGreenletError.from_attribute_read(
|
|
1085
1020
|
model=model,
|
|
1086
1021
|
col=col,
|
|
1087
1022
|
original=e,
|
|
@@ -1119,7 +1054,7 @@ class AbstractInterface(abc.ABC, typing.Generic[T, C, CT]):
|
|
|
1119
1054
|
try:
|
|
1120
1055
|
getattr(value, field_name)
|
|
1121
1056
|
except sqlalchemy.exc.MissingGreenlet as e:
|
|
1122
|
-
raise MissingGreenletError(
|
|
1057
|
+
raise MissingGreenletError.from_attribute_read(
|
|
1123
1058
|
model=value,
|
|
1124
1059
|
col=field_name,
|
|
1125
1060
|
original=e,
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import types
|
|
2
|
+
import typing
|
|
3
|
+
|
|
4
|
+
import fastapi
|
|
5
|
+
import pydantic
|
|
6
|
+
import sqlalchemy.exc
|
|
7
|
+
|
|
8
|
+
from .utils import T, prettify_dict
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"raise_exception",
|
|
12
|
+
"FastAPIReactToolkitException",
|
|
13
|
+
"RolesMismatchException",
|
|
14
|
+
"HTTPWithValidationException",
|
|
15
|
+
"MissingGreenletError",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def raise_exception(message: str, return_type: type[T] | None = None) -> T:
|
|
20
|
+
"""
|
|
21
|
+
Raise a FastAPIReactToolkitException with the given message.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
message (str): The error message.
|
|
25
|
+
return_type (type[T] | None, optional): The expected return type. Defaults to None.
|
|
26
|
+
|
|
27
|
+
Raises:
|
|
28
|
+
FastAPIReactToolkitException: Always raised.
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
T: The return value.
|
|
32
|
+
"""
|
|
33
|
+
raise FastAPIReactToolkitException(message)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class FastAPIReactToolkitException(Exception):
|
|
37
|
+
"""Base exception for FastAPI React Toolkit errors."""
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class RolesMismatchException(FastAPIReactToolkitException):
|
|
41
|
+
"""Exception raised when the roles do not match the expected roles."""
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class HTTPWithValidationException(fastapi.HTTPException):
|
|
45
|
+
"""
|
|
46
|
+
Custom HTTP Exception for FastAPI with validation details.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
URL = f"https://errors.pydantic.dev/{pydantic.version.version_short()}/v"
|
|
50
|
+
_no_input = object()
|
|
51
|
+
|
|
52
|
+
def __init__(
|
|
53
|
+
self,
|
|
54
|
+
status_code: int,
|
|
55
|
+
type: str,
|
|
56
|
+
location_type: typing.Literal["path", "query", "body"],
|
|
57
|
+
field: str,
|
|
58
|
+
msg: str,
|
|
59
|
+
input: typing.Any = _no_input,
|
|
60
|
+
headers: dict[str, str] | None = None,
|
|
61
|
+
):
|
|
62
|
+
"""
|
|
63
|
+
Initialize the HTTPWithValidationException.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
status_code (int): HTTP status code for the exception.
|
|
67
|
+
type (str): Type of the error (e.g., "string_type", "validation_error").
|
|
68
|
+
location_type (typing.Literal["path", "query", "body"]): Location type of the error, either "path", "query", or "body".
|
|
69
|
+
field (str): Field name where the error occurred.
|
|
70
|
+
msg (str): Error message describing the validation issue.
|
|
71
|
+
input (typing.Any, optional): Optional input value that caused the error. Defaults to _no_input.
|
|
72
|
+
headers (dict[str, str] | None, optional): Optional headers to include in the response. Defaults to None.
|
|
73
|
+
"""
|
|
74
|
+
obj = {"type": type, "loc": [location_type, field], "msg": msg}
|
|
75
|
+
if input is not self._no_input:
|
|
76
|
+
if isinstance(input, pydantic.BaseModel):
|
|
77
|
+
input = input.model_dump(mode="json")
|
|
78
|
+
else:
|
|
79
|
+
input = str(input)
|
|
80
|
+
obj["input"] = input
|
|
81
|
+
obj["url"] = f"{self.URL}/{type}"
|
|
82
|
+
|
|
83
|
+
super().__init__(status_code, [obj], headers)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class MissingGreenletError(FastAPIReactToolkitException):
|
|
87
|
+
"""Raised when an ORM attribute access triggers a SQLAlchemy lazy-load
|
|
88
|
+
outside the asyncio greenlet.
|
|
89
|
+
|
|
90
|
+
Covers async-property columns (awaited explicitly by
|
|
91
|
+
``_populate_async_column``), regular columns / relationships read by
|
|
92
|
+
pydantic's ``from_attributes`` machinery, and any attribute read by
|
|
93
|
+
application code during a request (hooks, filters, custom endpoints),
|
|
94
|
+
which ``globals_middleware_dispatch`` converts through
|
|
95
|
+
``from_lazy_load``.
|
|
96
|
+
|
|
97
|
+
The most common causes are:
|
|
98
|
+
|
|
99
|
+
- The ORM instance is detached from its session (commit/close happened
|
|
100
|
+
before the schema was rendered).
|
|
101
|
+
- The async session was expired between the query and the
|
|
102
|
+
response-serialisation step.
|
|
103
|
+
- A relationship the schema reads has not been eager-loaded. Prefer
|
|
104
|
+
``selectinload`` / ``joinedload`` on the query, call
|
|
105
|
+
``await instance.load("<col>")`` before returning the instance, or
|
|
106
|
+
add the column to ``list_columns`` / ``show_columns`` (or the
|
|
107
|
+
narrower ``list_select_columns`` / ``show_select_columns``, or
|
|
108
|
+
``extra_list_fetch_columns`` / ``extra_show_fetch_columns`` when
|
|
109
|
+
the column should be fetched but not surfaced in the response) on
|
|
110
|
+
the ``ModelRestApi`` subclass so the toolkit eager-loads it for
|
|
111
|
+
you.
|
|
112
|
+
"""
|
|
113
|
+
|
|
114
|
+
def __init__(
|
|
115
|
+
self,
|
|
116
|
+
*,
|
|
117
|
+
model,
|
|
118
|
+
col: str | None,
|
|
119
|
+
source: str | None = None,
|
|
120
|
+
original: Exception,
|
|
121
|
+
):
|
|
122
|
+
self.model = model
|
|
123
|
+
self.col = col
|
|
124
|
+
self.original = original
|
|
125
|
+
self.source = source
|
|
126
|
+
model_label = type(model).__name__
|
|
127
|
+
col_label = col or "<col>"
|
|
128
|
+
detail = {}
|
|
129
|
+
if col is None:
|
|
130
|
+
detail["problem"] = (
|
|
131
|
+
f"Failed to validate schema for '{model_label}': SQLAlchemy raised "
|
|
132
|
+
f"MissingGreenlet while pydantic read an attribute."
|
|
133
|
+
)
|
|
134
|
+
else:
|
|
135
|
+
detail["problem"] = (
|
|
136
|
+
f"Failed to populate column '{col}' on '{model_label}': SQLAlchemy "
|
|
137
|
+
f"raised MissingGreenlet, the ORM attempted I/O outside the async "
|
|
138
|
+
f"greenlet."
|
|
139
|
+
)
|
|
140
|
+
if source:
|
|
141
|
+
detail["accessed at"] = source
|
|
142
|
+
detail["fix by any of"] = [
|
|
143
|
+
f"Eager-load the relationship: add `selectinload({model_label}.{col_label})` or `joinedload({model_label}.{col_label})` to the query.",
|
|
144
|
+
f'Load it before the instance leaves the async context: `await instance.load("{col_label}")`.',
|
|
145
|
+
f'Add `"{col_label}"` to `list_columns` / `show_columns` on the ModelRestApi subclass, or to the narrower `list_select_columns` / `show_select_columns`.',
|
|
146
|
+
f'Add `"{col_label}"` to `extra_list_fetch_columns` / `extra_show_fetch_columns` when the column should be fetched but not surfaced in the response.',
|
|
147
|
+
]
|
|
148
|
+
detail["original error"] = repr(original)
|
|
149
|
+
super().__init__("\n" + prettify_dict(detail))
|
|
150
|
+
|
|
151
|
+
@classmethod
|
|
152
|
+
def from_lazy_load(cls, original: sqlalchemy.exc.MissingGreenlet):
|
|
153
|
+
"""
|
|
154
|
+
Build an enriched error from a MissingGreenlet raised by an ORM attribute read.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
original (sqlalchemy.exc.MissingGreenlet): The exception SQLAlchemy raised.
|
|
158
|
+
|
|
159
|
+
Returns:
|
|
160
|
+
MissingGreenletError | None: The enriched error, or None when the traceback carries no ORM attribute read to name.
|
|
161
|
+
"""
|
|
162
|
+
model, col = _lazy_load_target(original.__traceback__)
|
|
163
|
+
if model is None or col is None:
|
|
164
|
+
return None
|
|
165
|
+
return cls.from_attribute_read(model=model, col=col, original=original)
|
|
166
|
+
|
|
167
|
+
@classmethod
|
|
168
|
+
def from_attribute_read(cls, *, model, col: str | None, original: Exception):
|
|
169
|
+
"""
|
|
170
|
+
Build the error for a known model and column, taking the call site from the traceback.
|
|
171
|
+
|
|
172
|
+
Args:
|
|
173
|
+
model: The model whose attribute was read.
|
|
174
|
+
col (str | None): The column that was read, or None when pydantic validation failed on an unnamed attribute.
|
|
175
|
+
original (Exception): The exception SQLAlchemy raised.
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
MissingGreenletError: The error, carrying the call site when the traceback holds one.
|
|
179
|
+
"""
|
|
180
|
+
return cls(
|
|
181
|
+
model=model,
|
|
182
|
+
col=col,
|
|
183
|
+
source=_lazy_load_source(original.__traceback__),
|
|
184
|
+
original=original,
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _lazy_load_target(tb: types.TracebackType | None):
|
|
189
|
+
"""
|
|
190
|
+
Find the ORM instance and attribute name whose read triggered a lazy load.
|
|
191
|
+
|
|
192
|
+
Args:
|
|
193
|
+
tb (types.TracebackType | None): Traceback of the raised MissingGreenlet.
|
|
194
|
+
|
|
195
|
+
Returns:
|
|
196
|
+
tuple: The ORM instance and its column name, or (None, None) when the traceback holds no attribute read.
|
|
197
|
+
"""
|
|
198
|
+
while tb is not None:
|
|
199
|
+
frame = tb.tb_frame
|
|
200
|
+
if (
|
|
201
|
+
frame.f_code.co_name == "__get__"
|
|
202
|
+
and frame.f_globals.get("__name__") == "sqlalchemy.orm.attributes"
|
|
203
|
+
):
|
|
204
|
+
instance = frame.f_locals.get("instance")
|
|
205
|
+
col = getattr(frame.f_locals.get("self"), "key", None)
|
|
206
|
+
if instance is not None and col:
|
|
207
|
+
return instance, col
|
|
208
|
+
tb = tb.tb_next
|
|
209
|
+
return None, None
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _lazy_load_source(tb: types.TracebackType | None):
|
|
213
|
+
"""
|
|
214
|
+
Locate the call site that triggered the lazy load, the last non-toolkit frame before SQLAlchemy took over.
|
|
215
|
+
|
|
216
|
+
Toolkit frames are skipped because the schema-validation probe reads every field itself, so its own line names no code the reader can act on.
|
|
217
|
+
|
|
218
|
+
Args:
|
|
219
|
+
tb (types.TracebackType | None): Traceback of the raised MissingGreenlet.
|
|
220
|
+
|
|
221
|
+
Returns:
|
|
222
|
+
str | None: A "path:line in function" label, or None when only SQLAlchemy and toolkit frames read the attribute.
|
|
223
|
+
"""
|
|
224
|
+
caller = None
|
|
225
|
+
while tb is not None:
|
|
226
|
+
module = tb.tb_frame.f_globals.get("__name__", "")
|
|
227
|
+
if module.startswith("sqlalchemy."):
|
|
228
|
+
break
|
|
229
|
+
if not module.startswith("fastapi_rtk."):
|
|
230
|
+
caller = tb
|
|
231
|
+
tb = tb.tb_next
|
|
232
|
+
if caller is None:
|
|
233
|
+
return None
|
|
234
|
+
code = caller.tb_frame.f_code
|
|
235
|
+
return f"{code.co_filename}:{caller.tb_lineno} in {code.co_name}"
|
|
@@ -40,6 +40,7 @@ import contextvars
|
|
|
40
40
|
import typing
|
|
41
41
|
|
|
42
42
|
import fastapi
|
|
43
|
+
import sqlalchemy.exc
|
|
43
44
|
import starlette.middleware.base
|
|
44
45
|
import starlette.types
|
|
45
46
|
import werkzeug.local
|
|
@@ -51,6 +52,7 @@ from .const import (
|
|
|
51
52
|
COOKIE_CONFIG,
|
|
52
53
|
COOKIE_STRATEGY_CONFIG,
|
|
53
54
|
)
|
|
55
|
+
from .exceptions import MissingGreenletError
|
|
54
56
|
from .utils import lazy, lazy_import
|
|
55
57
|
|
|
56
58
|
if typing.TYPE_CHECKING:
|
|
@@ -240,14 +242,31 @@ async def globals_middleware_dispatch(
|
|
|
240
242
|
request: fastapi.Request,
|
|
241
243
|
call_next: collections.abc.Callable,
|
|
242
244
|
) -> fastapi.Response:
|
|
243
|
-
"""
|
|
245
|
+
"""
|
|
246
|
+
Dispatch the request in a new context to allow globals to be used.
|
|
247
|
+
|
|
248
|
+
Args:
|
|
249
|
+
request (fastapi.Request): The incoming request.
|
|
250
|
+
call_next (collections.abc.Callable): The next handler in the middleware chain.
|
|
244
251
|
|
|
252
|
+
Raises:
|
|
253
|
+
MissingGreenletError: When a lazy-load of an ORM attribute happened outside the asyncio greenlet, naming the model, the column, and the call site.
|
|
254
|
+
|
|
255
|
+
Returns:
|
|
256
|
+
fastapi.Response: The response produced by the rest of the chain.
|
|
257
|
+
"""
|
|
245
258
|
ctx = contextvars.copy_context()
|
|
246
259
|
|
|
247
260
|
def _call_next() -> collections.abc.Awaitable[fastapi.Response]:
|
|
248
261
|
return call_next(request)
|
|
249
262
|
|
|
250
|
-
|
|
263
|
+
try:
|
|
264
|
+
return await ctx.run(_call_next)
|
|
265
|
+
except sqlalchemy.exc.MissingGreenlet as e:
|
|
266
|
+
enriched = MissingGreenletError.from_lazy_load(e)
|
|
267
|
+
if not enriched:
|
|
268
|
+
raise
|
|
269
|
+
raise enriched from e
|
|
251
270
|
|
|
252
271
|
|
|
253
272
|
class GlobalsMiddleware(starlette.middleware.base.BaseHTTPMiddleware): # noqa
|
|
@@ -14,6 +14,9 @@ import typing
|
|
|
14
14
|
|
|
15
15
|
import fastapi
|
|
16
16
|
import pytest
|
|
17
|
+
import sqlalchemy
|
|
18
|
+
import sqlalchemy.ext.asyncio
|
|
19
|
+
import sqlalchemy.orm
|
|
17
20
|
|
|
18
21
|
from fastapi_rtk.globals import g
|
|
19
22
|
|
|
@@ -205,3 +208,67 @@ def make_toolkit_app(tmp_path, config_setter):
|
|
|
205
208
|
# Tear down in reverse order.
|
|
206
209
|
while cleanup_stack:
|
|
207
210
|
cleanup_stack.pop()()
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
@pytest.fixture
|
|
214
|
+
def lazy_parent():
|
|
215
|
+
"""Factory fixture: returns an async context manager yielding a persistent ORM instance whose `child` relationship is unloaded, so reading it raises a real sqlalchemy MissingGreenlet."""
|
|
216
|
+
|
|
217
|
+
return _lazy_parent
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
@pytest.fixture
|
|
221
|
+
def raise_lazy_load(lazy_parent):
|
|
222
|
+
"""Factory fixture: returns an async callable that reads the unloaded relationship itself, so the raising frame is application code.
|
|
223
|
+
|
|
224
|
+
Args:
|
|
225
|
+
lazy_parent: The factory yielding the ORM instance.
|
|
226
|
+
|
|
227
|
+
Returns:
|
|
228
|
+
collections.abc.Callable: The async callable that raises.
|
|
229
|
+
"""
|
|
230
|
+
|
|
231
|
+
async def _raise():
|
|
232
|
+
async with lazy_parent() as parent:
|
|
233
|
+
parent.child
|
|
234
|
+
|
|
235
|
+
return _raise
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
@contextlib.asynccontextmanager
|
|
239
|
+
async def _lazy_parent():
|
|
240
|
+
"""
|
|
241
|
+
Yield a persistent ORM instance backed by an in-memory aiosqlite engine, with its `child` relationship left unloaded.
|
|
242
|
+
|
|
243
|
+
Yields:
|
|
244
|
+
Parent: The instance, still attached to an open AsyncSession.
|
|
245
|
+
"""
|
|
246
|
+
base = sqlalchemy.orm.declarative_base()
|
|
247
|
+
|
|
248
|
+
class Child(base):
|
|
249
|
+
__tablename__ = "greenlet_child"
|
|
250
|
+
id = sqlalchemy.Column(sqlalchemy.Integer, primary_key=True)
|
|
251
|
+
|
|
252
|
+
class Parent(base):
|
|
253
|
+
__tablename__ = "greenlet_parent"
|
|
254
|
+
id = sqlalchemy.Column(sqlalchemy.Integer, primary_key=True)
|
|
255
|
+
child_id = sqlalchemy.Column(
|
|
256
|
+
sqlalchemy.Integer, sqlalchemy.ForeignKey("greenlet_child.id")
|
|
257
|
+
)
|
|
258
|
+
child = sqlalchemy.orm.relationship("Child")
|
|
259
|
+
|
|
260
|
+
@property
|
|
261
|
+
def child_via_property(self):
|
|
262
|
+
return self.child
|
|
263
|
+
|
|
264
|
+
engine = sqlalchemy.ext.asyncio.create_async_engine("sqlite+aiosqlite://")
|
|
265
|
+
try:
|
|
266
|
+
async with engine.begin() as conn:
|
|
267
|
+
await conn.run_sync(base.metadata.create_all)
|
|
268
|
+
async with sqlalchemy.ext.asyncio.AsyncSession(engine) as session:
|
|
269
|
+
session.add(Parent(id=1, child=Child(id=1)))
|
|
270
|
+
await session.commit()
|
|
271
|
+
async with sqlalchemy.ext.asyncio.AsyncSession(engine) as session:
|
|
272
|
+
yield await session.get(Parent, 1)
|
|
273
|
+
finally:
|
|
274
|
+
await engine.dispose()
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import types
|
|
1
2
|
import typing
|
|
2
3
|
import warnings
|
|
3
4
|
|
|
@@ -6,11 +7,8 @@ import pytest
|
|
|
6
7
|
import sqlalchemy.exc
|
|
7
8
|
|
|
8
9
|
from fastapi_rtk import AsyncTaskRunner
|
|
9
|
-
from fastapi_rtk.bases.interface import
|
|
10
|
-
|
|
11
|
-
MissingGreenletError,
|
|
12
|
-
PydanticGenerationSchema,
|
|
13
|
-
)
|
|
10
|
+
from fastapi_rtk.bases.interface import AbstractInterface, PydanticGenerationSchema
|
|
11
|
+
from fastapi_rtk.exceptions import MissingGreenletError
|
|
14
12
|
|
|
15
13
|
|
|
16
14
|
class TestPydanticGenerationSchema:
|
|
@@ -346,6 +344,21 @@ async def test_populate_async_column_raises_on_missing_greenlet():
|
|
|
346
344
|
assert isinstance(excinfo.value.__cause__, sqlalchemy.exc.MissingGreenlet)
|
|
347
345
|
|
|
348
346
|
|
|
347
|
+
@pytest.mark.asyncio
|
|
348
|
+
async def test_populate_async_column_names_the_call_site(raise_lazy_load):
|
|
349
|
+
"""The async column body is user code, so its frame must reach the message."""
|
|
350
|
+
|
|
351
|
+
interface = _make_concrete_subclass()(obj=type("M", (), {}))
|
|
352
|
+
model = types.SimpleNamespace(boom=raise_lazy_load())
|
|
353
|
+
|
|
354
|
+
with pytest.raises(MissingGreenletError) as excinfo:
|
|
355
|
+
await interface._populate_async_column(model, "boom")
|
|
356
|
+
|
|
357
|
+
assert excinfo.value.source.endswith("in _raise")
|
|
358
|
+
assert "conftest.py" in excinfo.value.source
|
|
359
|
+
assert "accessed at" in str(excinfo.value)
|
|
360
|
+
|
|
361
|
+
|
|
349
362
|
@pytest.mark.asyncio
|
|
350
363
|
async def test_populate_async_column_raises_on_non_greenlet_error():
|
|
351
364
|
interface = _make_concrete_subclass()(obj=type("M", (), {}))
|
|
@@ -407,6 +420,64 @@ def test_before_validator_raises_on_greenlet_for_regular_attribute():
|
|
|
407
420
|
assert isinstance(excinfo.value.__cause__, sqlalchemy.exc.MissingGreenlet)
|
|
408
421
|
|
|
409
422
|
|
|
423
|
+
def _probe_schema(interface, field: str, name: str):
|
|
424
|
+
"""Build a schema carrying only the greenlet probe and one field.
|
|
425
|
+
|
|
426
|
+
Args:
|
|
427
|
+
interface: The interface generating the schema.
|
|
428
|
+
field (str): The field the probe reads.
|
|
429
|
+
name (str): The schema name, kept unique per test to bypass the schema cache.
|
|
430
|
+
|
|
431
|
+
Returns:
|
|
432
|
+
type[pydantic.BaseModel]: The generated schema.
|
|
433
|
+
"""
|
|
434
|
+
schema_dict = PydanticGenerationSchema(
|
|
435
|
+
__config__=pydantic.ConfigDict(from_attributes=True),
|
|
436
|
+
__async_columns__=[],
|
|
437
|
+
__columns__=None,
|
|
438
|
+
__with_id__=False,
|
|
439
|
+
__with_name__=False,
|
|
440
|
+
__with_property__=True,
|
|
441
|
+
__optional__=False,
|
|
442
|
+
__hide_sensitive_columns__=False,
|
|
443
|
+
__with_fk__=True,
|
|
444
|
+
__name__=name,
|
|
445
|
+
)
|
|
446
|
+
schema_dict[field] = (typing.Any, pydantic.Field(default=None))
|
|
447
|
+
return interface._generate_schema_from_dict(schema_dict)
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
@pytest.mark.asyncio
|
|
451
|
+
async def test_probe_on_mapped_column_reports_no_call_site(lazy_parent):
|
|
452
|
+
"""The probe reads the attribute itself, so its own frame names no code the reader can act on."""
|
|
453
|
+
|
|
454
|
+
interface = _make_concrete_subclass()(obj=type("M", (), {}))
|
|
455
|
+
Schema = _probe_schema(interface, "child", "ProbeMappedSchema")
|
|
456
|
+
|
|
457
|
+
async with lazy_parent() as parent:
|
|
458
|
+
with pytest.raises(MissingGreenletError) as excinfo:
|
|
459
|
+
Schema.model_validate(parent)
|
|
460
|
+
|
|
461
|
+
assert excinfo.value.col == "child"
|
|
462
|
+
assert excinfo.value.source is None
|
|
463
|
+
assert "accessed at" not in str(excinfo.value)
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
@pytest.mark.asyncio
|
|
467
|
+
async def test_probe_through_property_names_the_property(lazy_parent):
|
|
468
|
+
"""A python property reading ORM state is application code, so its frame must reach the message."""
|
|
469
|
+
|
|
470
|
+
interface = _make_concrete_subclass()(obj=type("M", (), {}))
|
|
471
|
+
Schema = _probe_schema(interface, "child_via_property", "ProbePropertySchema")
|
|
472
|
+
|
|
473
|
+
async with lazy_parent() as parent:
|
|
474
|
+
with pytest.raises(MissingGreenletError) as excinfo:
|
|
475
|
+
Schema.model_validate(parent)
|
|
476
|
+
|
|
477
|
+
assert excinfo.value.source.endswith("in child_via_property")
|
|
478
|
+
assert "conftest.py" in excinfo.value.source
|
|
479
|
+
|
|
480
|
+
|
|
410
481
|
@pytest.mark.asyncio
|
|
411
482
|
async def test_before_validator_skips_async_columns_no_unawaited_coroutine():
|
|
412
483
|
"""The greenlet probe must skip async-property columns. Probing them via
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import fastapi
|
|
2
2
|
import pydantic
|
|
3
3
|
import pytest
|
|
4
|
+
import sqlalchemy.exc
|
|
4
5
|
from fastapi_rtk.exceptions import (
|
|
5
6
|
FastAPIReactToolkitException,
|
|
6
7
|
HTTPWithValidationException,
|
|
8
|
+
MissingGreenletError,
|
|
7
9
|
RolesMismatchException,
|
|
8
10
|
raise_exception,
|
|
9
11
|
)
|
|
@@ -105,3 +107,31 @@ class TestHTTPWithValidationException:
|
|
|
105
107
|
msg="m",
|
|
106
108
|
)
|
|
107
109
|
assert "input" not in exc.detail[0]
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class TestFromLazyLoad:
|
|
113
|
+
@pytest.mark.asyncio
|
|
114
|
+
async def test_names_model_column_and_call_site(self, raise_lazy_load):
|
|
115
|
+
with pytest.raises(sqlalchemy.exc.MissingGreenlet) as excinfo:
|
|
116
|
+
await raise_lazy_load()
|
|
117
|
+
|
|
118
|
+
error = MissingGreenletError.from_lazy_load(excinfo.value)
|
|
119
|
+
|
|
120
|
+
assert error is not None
|
|
121
|
+
assert error.col == "child"
|
|
122
|
+
assert type(error.model).__name__ == "Parent"
|
|
123
|
+
assert error.source.endswith("in _raise")
|
|
124
|
+
assert "conftest.py" in error.source
|
|
125
|
+
msg = str(error)
|
|
126
|
+
assert "'child'" in msg
|
|
127
|
+
assert "'Parent'" in msg
|
|
128
|
+
assert "accessed at" in msg
|
|
129
|
+
assert "selectinload" in msg
|
|
130
|
+
|
|
131
|
+
def test_returns_none_without_attribute_read(self):
|
|
132
|
+
try:
|
|
133
|
+
raise sqlalchemy.exc.MissingGreenlet(
|
|
134
|
+
"greenlet_spawn has not been called; can't call await_only() here"
|
|
135
|
+
)
|
|
136
|
+
except sqlalchemy.exc.MissingGreenlet as e:
|
|
137
|
+
assert MissingGreenletError.from_lazy_load(e) is None
|
|
@@ -2,6 +2,7 @@ import asyncio
|
|
|
2
2
|
import contextvars
|
|
3
3
|
|
|
4
4
|
import pytest
|
|
5
|
+
import sqlalchemy.exc
|
|
5
6
|
|
|
6
7
|
from fastapi_rtk.config import Config
|
|
7
8
|
from fastapi_rtk.globals import (
|
|
@@ -172,3 +173,40 @@ class TestBasicCallback:
|
|
|
172
173
|
assert ac.bearer_config["tokenUrl"] == "/custom/login"
|
|
173
174
|
assert ac.cookie_strategy_config["lifetime_seconds"] == 1234
|
|
174
175
|
assert ac.bearer_strategy_config["lifetime_seconds"] == 5678
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
class TestGlobalsMiddlewareDispatch:
|
|
179
|
+
@pytest.mark.asyncio
|
|
180
|
+
async def test_lazy_load_error_is_enriched(self, raise_lazy_load):
|
|
181
|
+
from fastapi_rtk.exceptions import MissingGreenletError
|
|
182
|
+
from fastapi_rtk.globals import globals_middleware_dispatch
|
|
183
|
+
|
|
184
|
+
async def call_next(request):
|
|
185
|
+
await raise_lazy_load()
|
|
186
|
+
|
|
187
|
+
with pytest.raises(MissingGreenletError) as excinfo:
|
|
188
|
+
await globals_middleware_dispatch(None, call_next)
|
|
189
|
+
|
|
190
|
+
assert excinfo.value.col == "child"
|
|
191
|
+
assert isinstance(excinfo.value.__cause__, sqlalchemy.exc.MissingGreenlet)
|
|
192
|
+
|
|
193
|
+
@pytest.mark.asyncio
|
|
194
|
+
async def test_missing_greenlet_without_attribute_read_propagates(self):
|
|
195
|
+
from fastapi_rtk.globals import globals_middleware_dispatch
|
|
196
|
+
|
|
197
|
+
async def call_next(request):
|
|
198
|
+
raise sqlalchemy.exc.MissingGreenlet(
|
|
199
|
+
"greenlet_spawn has not been called; can't call await_only() here"
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
with pytest.raises(sqlalchemy.exc.MissingGreenlet):
|
|
203
|
+
await globals_middleware_dispatch(None, call_next)
|
|
204
|
+
|
|
205
|
+
@pytest.mark.asyncio
|
|
206
|
+
async def test_response_passes_through(self):
|
|
207
|
+
from fastapi_rtk.globals import globals_middleware_dispatch
|
|
208
|
+
|
|
209
|
+
async def call_next(request):
|
|
210
|
+
return "response"
|
|
211
|
+
|
|
212
|
+
assert await globals_middleware_dispatch(None, call_next) == "response"
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "2.12.3"
|