fastapi-rtk 2.11.2__tar.gz → 2.11.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.11.2 → fastapi_rtk-2.11.4}/PKG-INFO +3 -3
- fastapi_rtk-2.11.4/fastapi_rtk/_version.py +1 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/api/model_rest_api/__init__.py +49 -17
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/api/model_rest_api/_info_mixin.py +1 -1
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/backends/sqla/db.py +10 -7
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/backends/sqla/extensions/audit/_lifecycle_mixin.py +47 -17
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/backends/sqla/extensions/audit/audit.py +10 -10
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/bases/interface.py +16 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/pyproject.toml +1 -1
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/unit/api/model_rest_api/test_init.py +227 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/unit/backends/sqla/test_audit.py +233 -25
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/unit/backends/sqla/test_db.py +44 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/unit/bases/test_interface.py +65 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/uv.lock +1 -1
- fastapi_rtk-2.11.2/fastapi_rtk/_version.py +0 -1
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/.gitignore +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/LICENSE +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/README.md +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/__init__.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/api/__init__.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/api/base_api.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/api/model_rest_api/_add_mixin.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/api/model_rest_api/_bulk_mixin.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/api/model_rest_api/_columns_mixin.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/api/model_rest_api/_delete_mixin.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/api/model_rest_api/_download_mixin.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/api/model_rest_api/_edit_mixin.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/api/model_rest_api/_file_mixin.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/api/model_rest_api/_list_mixin.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/api/model_rest_api/_params.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/api/model_rest_api/_show_mixin.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/api/model_rest_api/_typed_base.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/api/model_rest_api/_upload_mixin.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/apis/__init__.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/apis/info.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/apis/license.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/auth/__init__.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/auth/auth.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/auth/hashers/__init__.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/auth/hashers/pbkdf2.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/auth/hashers/scrypt.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/auth/ldap.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/auth/password_helpers/__init__.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/auth/password_helpers/fab.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/auth/strategies/__init__.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/auth/strategies/config.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/auth/strategies/db.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/auth/strategies/jwt.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/backends/__init__.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/backends/generic/__init__.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/backends/generic/column.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/backends/generic/db.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/backends/generic/exceptions.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/backends/generic/filters.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/backends/generic/interface.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/backends/generic/model.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/backends/generic/session.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/backends/sqla/__init__.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/backends/sqla/column.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/backends/sqla/exceptions.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/backends/sqla/extensions/__init__.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/backends/sqla/extensions/audit/__init__.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/backends/sqla/extensions/audit/_decorators_mixin.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/backends/sqla/extensions/audit/_logger.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/backends/sqla/extensions/audit/_processing_mixin.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/backends/sqla/extensions/audit/types.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/backends/sqla/extensions/geoalchemy2/__init__.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/backends/sqla/extensions/geoalchemy2/filters.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/backends/sqla/extensions/geoalchemy2/geometry_converter.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/backends/sqla/filters.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/backends/sqla/interface.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/backends/sqla/model.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/backends/sqla/session.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/bases/__init__.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/bases/db.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/bases/file_manager.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/bases/filter.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/bases/model.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/bases/session.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/cli/__init__.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/cli/cli.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/cli/commands/__init__.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/cli/commands/_security_crypto.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/cli/commands/db/__init__.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/cli/commands/db/templates/fastapi/README +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/cli/commands/db/templates/fastapi/alembic.ini.mako +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/cli/commands/db/templates/fastapi/env.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/cli/commands/db/templates/fastapi/script.py.mako +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/cli/commands/db/templates/fastapi-multidb/README +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/cli/commands/db/templates/fastapi-multidb/alembic.ini.mako +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/cli/commands/db/templates/fastapi-multidb/env.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/cli/commands/db/templates/fastapi-multidb/script.py.mako +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/cli/commands/export.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/cli/commands/security.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/cli/commands/translate.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/cli/const.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/cli/decorators.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/cli/types.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/cli/utils.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/config.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/const.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/db.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/decorators.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/dependencies.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/exceptions.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/fastapi_react_toolkit.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/file_managers/__init__.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/file_managers/file_manager.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/file_managers/image_manager.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/file_managers/s3_file_manager.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/file_managers/s3_image_manager.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/filters.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/globals.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/lang/__init__.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/lang/babel/__init__.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/lang/babel/cli.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/lang/babel/config.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/lang/babel.cfg +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/lang/lazy_text.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/lang/messages.pot +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/lang/translations/de/LC_MESSAGES/messages.mo +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/lang/translations/de/LC_MESSAGES/messages.po +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/lang/translations/en/LC_MESSAGES/messages.mo +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/lang/translations/en/LC_MESSAGES/messages.po +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/manager.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/middlewares.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/mixins.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/models.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/py.typed +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/registry.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/routers.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/schemas.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/security/__init__.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/security/sqla/__init__.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/security/sqla/apis.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/security/sqla/models.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/security/sqla/security_manager/__init__.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/security/sqla/security_manager/_association_mixin.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/security/sqla/security_manager/_builtin_role_mixin.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/security/sqla/security_manager/_cleanup_mixin.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/security/sqla/security_manager/_exception_filters.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/security/sqla/security_manager/_export.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/security/sqla/security_manager/_import.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/security/sqla/security_manager/_role_mixin.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/security/sqla/security_manager/_user_mixin.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/setting.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/types.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/utils/__init__.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/utils/async_task_runner.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/utils/class_factory.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/utils/csv_json_converter.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/utils/deep_merge.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/utils/extender_mixin.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/utils/flask_appbuilder_utils.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/utils/formatter.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/utils/hooks.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/utils/lazy.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/utils/merge_schema.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/utils/multiple_async_contexts.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/utils/pydantic.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/utils/run_utils.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/utils/self_dependencies.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/utils/smartdefaultdict.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/utils/sqla.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/utils/timezone.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/utils/update_signature.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/utils/use_default_when_none.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/version.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/conftest.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/docker-compose.gis.yml +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/integration/__init__.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/integration/api/__init__.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/integration/api/test_model_rest_api.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/integration/apis/__init__.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/integration/apis/test_info_callable_schema.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/integration/apis/test_info_contract.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/integration/auth/__init__.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/integration/auth/test_auth_flow.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/integration/auth/test_oauth.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/integration/conftest.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/integration/file_managers/__init__.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/integration/file_managers/test_file_routes.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/integration/security/__init__.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/integration/security/sqla/__init__.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/integration/security/sqla/test_security_manager.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/integration/test_app_smoke.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/unit/__init__.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/unit/api/__init__.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/unit/api/conftest.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/unit/api/model_rest_api/__init__.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/unit/api/model_rest_api/test_columns.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/unit/api/model_rest_api/test_file_mixin.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/unit/api/model_rest_api/test_typed_base_consistency.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/unit/api/test_base_api.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/unit/apis/__init__.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/unit/apis/test_info.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/unit/apis/test_license.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/unit/auth/__init__.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/unit/auth/hashers/__init__.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/unit/auth/hashers/test_pbkdf2.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/unit/auth/hashers/test_scrypt.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/unit/auth/password_helpers/__init__.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/unit/auth/password_helpers/test_fab.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/unit/auth/strategies/__init__.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/unit/auth/strategies/test_config.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/unit/auth/strategies/test_db.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/unit/auth/strategies/test_jwt.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/unit/auth/test_auth.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/unit/backends/__init__.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/unit/backends/generic/__init__.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/unit/backends/generic/test_column.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/unit/backends/generic/test_db.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/unit/backends/generic/test_exceptions.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/unit/backends/generic/test_filters.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/unit/backends/generic/test_interface.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/unit/backends/generic/test_model.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/unit/backends/generic/test_session.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/unit/backends/sqla/__init__.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/unit/backends/sqla/conftest.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/unit/backends/sqla/test_audit_types.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/unit/backends/sqla/test_column.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/unit/backends/sqla/test_filters.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/unit/backends/sqla/test_geoalchemy2.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/unit/backends/sqla/test_interface.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/unit/backends/sqla/test_model.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/unit/backends/sqla/test_session.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/unit/bases/__init__.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/unit/bases/test_db.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/unit/bases/test_file_manager.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/unit/bases/test_filter.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/unit/bases/test_model.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/unit/bases/test_session.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/unit/cli/__init__.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/unit/cli/commands/__init__.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/unit/cli/commands/conftest.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/unit/cli/commands/test_db.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/unit/cli/commands/test_export.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/unit/cli/commands/test_security.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/unit/cli/commands/test_security_crypto.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/unit/cli/commands/test_translate.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/unit/cli/conftest.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/unit/cli/test_cli.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/unit/cli/test_decorators.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/unit/cli/test_types.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/unit/cli/test_utils.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/unit/file_managers/__init__.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/unit/file_managers/conftest.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/unit/file_managers/test_file_manager.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/unit/file_managers/test_image_manager.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/unit/file_managers/test_s3_file_manager.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/unit/file_managers/test_s3_image_manager.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/unit/lang/__init__.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/unit/lang/test_babel_cli.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/unit/lang/test_babel_config.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/unit/lang/test_lazy_text.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/unit/security/__init__.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/unit/security/sqla/__init__.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/unit/security/sqla/conftest.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/unit/security/sqla/test_apis.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/unit/security/sqla/test_models.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/unit/security/sqla/test_security_manager.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/unit/test_config.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/unit/test_const.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/unit/test_db.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/unit/test_decorators.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/unit/test_dependencies.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/unit/test_exceptions.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/unit/test_fastapi_react_toolkit.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/unit/test_filters.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/unit/test_globals.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/unit/test_manager.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/unit/test_middlewares.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/unit/test_mixins.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/unit/test_models.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/unit/test_registry.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/unit/test_routers.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/unit/test_schemas.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/unit/test_setting.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/unit/test_types.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/unit/test_version.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/unit/utils/__init__.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/unit/utils/test_async_task_runner.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/unit/utils/test_class_factory.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/unit/utils/test_csv_json_converter.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/unit/utils/test_deep_merge.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/unit/utils/test_extender_mixin.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/unit/utils/test_flask_appbuilder_utils.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/unit/utils/test_formatter.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/unit/utils/test_hooks.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/unit/utils/test_lazy.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/unit/utils/test_merge_schema.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/unit/utils/test_multiple_async_contexts.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/unit/utils/test_pydantic.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/unit/utils/test_run_utils.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/unit/utils/test_self_dependencies.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/unit/utils/test_smartdefaultdict.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/unit/utils/test_sqla.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/unit/utils/test_timezone.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/unit/utils/test_update_signature.py +0 -0
- {fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/tests/unit/utils/test_use_default_when_none.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: fastapi-rtk
|
|
3
|
-
Version: 2.11.
|
|
3
|
+
Version: 2.11.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
|
|
@@ -24,7 +24,7 @@ Requires-Dist: marshmallow-sqlalchemy>=1.4.2
|
|
|
24
24
|
Requires-Dist: pip-licenses>=5.5.1
|
|
25
25
|
Requires-Dist: polib>=1.2.0
|
|
26
26
|
Requires-Dist: prometheus-fastapi-instrumentator>=7.1.0
|
|
27
|
-
Requires-Dist: secweb
|
|
27
|
+
Requires-Dist: secweb<2,>=1.25.2
|
|
28
28
|
Requires-Dist: sqlalchemy-utils>=0.42.0
|
|
29
29
|
Requires-Dist: werkzeug>=3.1.5
|
|
30
30
|
Provides-Extra: audit-bg
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "2.11.4"
|
|
@@ -24,6 +24,7 @@ the usual ``class MyApi(ModelRestApi): list_columns = [...]`` idiom.
|
|
|
24
24
|
|
|
25
25
|
import collections.abc
|
|
26
26
|
import enum
|
|
27
|
+
import inspect
|
|
27
28
|
import typing
|
|
28
29
|
|
|
29
30
|
import fastapi
|
|
@@ -31,7 +32,7 @@ import pydantic
|
|
|
31
32
|
|
|
32
33
|
from ...backends.sqla.filters import BaseFilter, BaseOprFilter
|
|
33
34
|
from ...backends.sqla.interface import SQLAInterface
|
|
34
|
-
from ...bases import DBQueryParams
|
|
35
|
+
from ...bases import AbstractInterface, DBQueryParams
|
|
35
36
|
from ...const import AVAILABLE_ROUTES
|
|
36
37
|
from ...dependencies import current_permissions
|
|
37
38
|
from ...exceptions import HTTPWithValidationException, raise_exception
|
|
@@ -1062,6 +1063,22 @@ class ModelRestApi(
|
|
|
1062
1063
|
DO NOT MODIFY.
|
|
1063
1064
|
"""
|
|
1064
1065
|
|
|
1066
|
+
def __init_subclass__(cls, **kwargs):
|
|
1067
|
+
"""
|
|
1068
|
+
Gives the subclass its own copy of an inherited datamodel.
|
|
1069
|
+
|
|
1070
|
+
A datamodel declared in the class body is left untouched, so sharing one interface between Api classes stays possible.
|
|
1071
|
+
|
|
1072
|
+
Args:
|
|
1073
|
+
**kwargs: Forwarded to the parent implementation.
|
|
1074
|
+
"""
|
|
1075
|
+
super().__init_subclass__(**kwargs)
|
|
1076
|
+
if "datamodel" in cls.__dict__:
|
|
1077
|
+
return
|
|
1078
|
+
inherited = inspect.getattr_static(cls, "datamodel", None)
|
|
1079
|
+
if isinstance(inherited, AbstractInterface):
|
|
1080
|
+
cls.datamodel = inherited.clone()
|
|
1081
|
+
|
|
1065
1082
|
def __init__(self):
|
|
1066
1083
|
if not self.datamodel:
|
|
1067
1084
|
raise Exception( # pragma: no cover
|
|
@@ -1098,11 +1115,18 @@ class ModelRestApi(
|
|
|
1098
1115
|
|
|
1099
1116
|
for key, value in self.search_filters.items():
|
|
1100
1117
|
datamodel, _ = self._get_datamodel_and_column(key)
|
|
1101
|
-
|
|
1118
|
+
# One interface can still be assigned to several Api classes, or one
|
|
1119
|
+
# Api instantiated twice, so skip filter classes it already carries
|
|
1120
|
+
# instead of stacking duplicates.
|
|
1121
|
+
registered = {type(x) for x in self.datamodel.filters[key]}
|
|
1122
|
+
self.datamodel.filters[key].extend(
|
|
1123
|
+
[v(datamodel) for v in value if v not in registered]
|
|
1124
|
+
)
|
|
1102
1125
|
|
|
1103
1126
|
for key, value in self.search_exclude_filters.items():
|
|
1104
1127
|
value = [v.arg_name if isinstance(v, BaseFilter) else v for v in value]
|
|
1105
|
-
self.datamodel.exclude_filters[key]
|
|
1128
|
+
excluded = self.datamodel.exclude_filters[key]
|
|
1129
|
+
excluded.extend([v for v in value if v not in excluded])
|
|
1106
1130
|
self.datamodel.filters[key] = [
|
|
1107
1131
|
x for x in self.datamodel.filters[key] if x.arg_name not in value
|
|
1108
1132
|
]
|
|
@@ -1157,25 +1181,33 @@ class ModelRestApi(
|
|
|
1157
1181
|
"search_query_rel_fields",
|
|
1158
1182
|
]
|
|
1159
1183
|
for key in field_keys:
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1184
|
+
# Build a new dict bound to the instance: the source may be a class
|
|
1185
|
+
# attribute shared with subclasses, and instantiating in place turns
|
|
1186
|
+
# the filter classes into instances the next Api cannot call.
|
|
1187
|
+
setattr(
|
|
1188
|
+
self,
|
|
1189
|
+
key,
|
|
1190
|
+
{
|
|
1191
|
+
field: self._init_filters(
|
|
1192
|
+
self.datamodel.get_related_interface(field), filters
|
|
1193
|
+
)
|
|
1194
|
+
for field, filters in getattr(self, key).items()
|
|
1195
|
+
},
|
|
1196
|
+
)
|
|
1165
1197
|
|
|
1166
1198
|
def _init_routes(self):
|
|
1167
1199
|
"""
|
|
1168
1200
|
Init routes for the API.
|
|
1169
1201
|
"""
|
|
1170
|
-
|
|
1171
|
-
if not
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1202
|
+
unavailable = []
|
|
1203
|
+
if not self.datamodel.get_image_column_list():
|
|
1204
|
+
unavailable.append("image")
|
|
1205
|
+
if not self.datamodel.get_file_column_list():
|
|
1206
|
+
unavailable.append("file")
|
|
1207
|
+
|
|
1208
|
+
# Rebind on the instance instead of removing in place: `routes` may be a
|
|
1209
|
+
# class attribute shared with subclasses.
|
|
1210
|
+
self.routes = [x for x in self.routes if x not in unavailable]
|
|
1179
1211
|
|
|
1180
1212
|
for route in self.routes:
|
|
1181
1213
|
getattr(self, route)()
|
|
@@ -205,7 +205,7 @@ class InfoMixin(_TypedModelRestApi):
|
|
|
205
205
|
|
|
206
206
|
filters = self.cache.get(f"info_filters_{col}", None)
|
|
207
207
|
if not filters:
|
|
208
|
-
filters = datamodel.filters[col_to_get]
|
|
208
|
+
filters = list(datamodel.filters[col_to_get])
|
|
209
209
|
# Add search filters and search exclude filters to the filters
|
|
210
210
|
if "." in col:
|
|
211
211
|
filters.extend(self.datamodel.filters[col])
|
|
@@ -140,15 +140,18 @@ class SQLAQueryBuilder(AbstractQueryBuilder[sqlalchemy.Select[tuple[T]]]):
|
|
|
140
140
|
if "." in col:
|
|
141
141
|
col, rel_col = col.split(".", 1)
|
|
142
142
|
rel_interface = self.datamodel.get_related_interface(col)
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
143
|
+
# The related interface is shared by every Api and query touching
|
|
144
|
+
# that relation, so resolve into a local list instead of growing its
|
|
145
|
+
# own.
|
|
146
|
+
excluded = self.datamodel.exclude_filters.get(filter.col, [])
|
|
147
|
+
filter_classes = [
|
|
147
148
|
f
|
|
148
|
-
for f in
|
|
149
|
-
|
|
149
|
+
for f in [
|
|
150
|
+
*rel_interface.filters[rel_col],
|
|
151
|
+
*self.datamodel.filters.get(filter.col, []),
|
|
152
|
+
]
|
|
153
|
+
if f.arg_name not in excluded
|
|
150
154
|
]
|
|
151
|
-
filter_classes = rel_interface.filters.get(rel_col, [])
|
|
152
155
|
else:
|
|
153
156
|
filter_classes = self.datamodel.filters.get(col, [])
|
|
154
157
|
filter_class = None
|
|
@@ -19,7 +19,7 @@ from ._logger import logger
|
|
|
19
19
|
from .types import AuditEntry
|
|
20
20
|
|
|
21
21
|
if typing.TYPE_CHECKING:
|
|
22
|
-
import
|
|
22
|
+
import weakref
|
|
23
23
|
|
|
24
24
|
from .types import SQLAModel
|
|
25
25
|
|
|
@@ -51,11 +51,11 @@ class LifecycleMixin:
|
|
|
51
51
|
None | collections.abc.Coroutine[typing.Any, typing.Any, None],
|
|
52
52
|
]
|
|
53
53
|
]
|
|
54
|
-
_session_pending_entries:
|
|
54
|
+
_session_pending_entries: weakref.WeakKeyDictionary[
|
|
55
55
|
sqlalchemy.orm.Session, list[AuditEntry]
|
|
56
56
|
]
|
|
57
|
-
_session_is_commited:
|
|
58
|
-
_session_callbacks_after_commit:
|
|
57
|
+
_session_is_commited: weakref.WeakKeyDictionary[sqlalchemy.orm.Session, bool]
|
|
58
|
+
_session_callbacks_after_commit: weakref.WeakKeyDictionary[
|
|
59
59
|
sqlalchemy.orm.Session,
|
|
60
60
|
list[
|
|
61
61
|
collections.abc.Callable[
|
|
@@ -194,13 +194,18 @@ class LifecycleMixin:
|
|
|
194
194
|
e: The exception raised during the insert.
|
|
195
195
|
"""
|
|
196
196
|
try:
|
|
197
|
-
|
|
197
|
+
session_key = _session_key(session) if session else None
|
|
198
|
+
is_commited = (
|
|
199
|
+
cls._session_is_commited.get(session_key, False)
|
|
200
|
+
if session_key
|
|
201
|
+
else False
|
|
202
|
+
)
|
|
198
203
|
if is_commited:
|
|
199
204
|
logger.warning(
|
|
200
205
|
"Session has already been committed, creating a new session to insert audit entries."
|
|
201
206
|
)
|
|
202
207
|
commit = is_commited
|
|
203
|
-
del cls._session_is_commited[
|
|
208
|
+
del cls._session_is_commited[session_key]
|
|
204
209
|
session = None
|
|
205
210
|
if not session:
|
|
206
211
|
async with db.session(cls.model.__bind_key__) as session:
|
|
@@ -242,7 +247,9 @@ class LifecycleMixin:
|
|
|
242
247
|
cls._run_callbacks, entries=entries
|
|
243
248
|
)
|
|
244
249
|
|
|
245
|
-
cls._session_callbacks_after_commit[
|
|
250
|
+
cls._session_callbacks_after_commit.setdefault(session_key, []).append(
|
|
251
|
+
callback_for_commit
|
|
252
|
+
)
|
|
246
253
|
except Exception as e:
|
|
247
254
|
logger.error(f"Error inserting audit entries: {e}")
|
|
248
255
|
if not raise_exception:
|
|
@@ -268,9 +275,10 @@ class LifecycleMixin:
|
|
|
268
275
|
logger.debug("No models to be audited in `after_flush`.")
|
|
269
276
|
return
|
|
270
277
|
|
|
271
|
-
|
|
278
|
+
session_key = _session_key(session)
|
|
279
|
+
pending_entries = cls._session_pending_entries.setdefault(session_key, [])
|
|
272
280
|
pending_entries.extend(audit_entries)
|
|
273
|
-
cls._session_pending_entries[
|
|
281
|
+
cls._session_pending_entries[session_key] = (
|
|
274
282
|
cls._merge_update_entries(pending_entries)
|
|
275
283
|
if cls._coalesce_updates
|
|
276
284
|
else pending_entries
|
|
@@ -284,7 +292,8 @@ class LifecycleMixin:
|
|
|
284
292
|
Args:
|
|
285
293
|
session (sqlalchemy.orm.Session): The SQLAlchemy session that is about to be committed.
|
|
286
294
|
"""
|
|
287
|
-
|
|
295
|
+
session_key = _session_key(session)
|
|
296
|
+
entries = cls._session_pending_entries.pop(session_key, [])
|
|
288
297
|
if not entries:
|
|
289
298
|
return
|
|
290
299
|
|
|
@@ -298,7 +307,9 @@ class LifecycleMixin:
|
|
|
298
307
|
name=f"{cls.__name__}.insert_entries",
|
|
299
308
|
)
|
|
300
309
|
|
|
301
|
-
cls._session_callbacks_after_commit[
|
|
310
|
+
cls._session_callbacks_after_commit.setdefault(session_key, []).append(
|
|
311
|
+
callback_for_commit
|
|
312
|
+
)
|
|
302
313
|
return
|
|
303
314
|
|
|
304
315
|
existing_session = (
|
|
@@ -329,7 +340,9 @@ class LifecycleMixin:
|
|
|
329
340
|
commit=True,
|
|
330
341
|
)
|
|
331
342
|
|
|
332
|
-
cls._session_callbacks_after_commit[
|
|
343
|
+
cls._session_callbacks_after_commit.setdefault(session_key, []).append(
|
|
344
|
+
callback_for_commit
|
|
345
|
+
)
|
|
333
346
|
|
|
334
347
|
@classmethod
|
|
335
348
|
def _after_commit(cls, session: sqlalchemy.orm.Session):
|
|
@@ -339,9 +352,10 @@ class LifecycleMixin:
|
|
|
339
352
|
Args:
|
|
340
353
|
session (sqlalchemy.orm.Session): The SQLAlchemy session that was committed.
|
|
341
354
|
"""
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
cls.
|
|
355
|
+
session_key = _session_key(session)
|
|
356
|
+
cls._session_is_commited[session_key] = True
|
|
357
|
+
callbacks = cls._session_callbacks_after_commit.pop(session_key, [])
|
|
358
|
+
cls._session_pending_entries.pop(session_key, None)
|
|
345
359
|
if not callbacks:
|
|
346
360
|
logger.debug("No callbacks to run in `after_commit`.")
|
|
347
361
|
return
|
|
@@ -356,8 +370,9 @@ class LifecycleMixin:
|
|
|
356
370
|
Args:
|
|
357
371
|
session (sqlalchemy.orm.Session): The SQLAlchemy session that was rolled back.
|
|
358
372
|
"""
|
|
359
|
-
|
|
360
|
-
cls.
|
|
373
|
+
session_key = _session_key(session)
|
|
374
|
+
cls._session_callbacks_after_commit.pop(session_key, None)
|
|
375
|
+
cls._session_pending_entries.pop(session_key, None)
|
|
361
376
|
logger.debug("Rolled back changes in session, clearing callbacks.")
|
|
362
377
|
|
|
363
378
|
"""
|
|
@@ -419,3 +434,18 @@ class LifecycleMixin:
|
|
|
419
434
|
logger.error(
|
|
420
435
|
f"Error running async function {async_function.__name__} in threadpool and no AsyncTaskRunner is available to delegate the task to: {e}"
|
|
421
436
|
)
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
def _session_key(session: SQLASession) -> sqlalchemy.orm.Session:
|
|
440
|
+
"""
|
|
441
|
+
Resolve the object a session is keyed by in the session-keyed class state.
|
|
442
|
+
|
|
443
|
+
The session event handlers always receive the sync session, so an async caller must be reduced to the same object or its lookups never match.
|
|
444
|
+
|
|
445
|
+
Args:
|
|
446
|
+
session (SQLASession): The session held by the caller.
|
|
447
|
+
|
|
448
|
+
Returns:
|
|
449
|
+
sqlalchemy.orm.Session: The sync session the event handlers see.
|
|
450
|
+
"""
|
|
451
|
+
return getattr(session, "sync_session", session)
|
{fastapi_rtk-2.11.2 → fastapi_rtk-2.11.4}/fastapi_rtk/backends/sqla/extensions/audit/audit.py
RENAMED
|
@@ -19,6 +19,7 @@ import collections
|
|
|
19
19
|
import collections.abc
|
|
20
20
|
import datetime
|
|
21
21
|
import typing
|
|
22
|
+
import weakref
|
|
22
23
|
|
|
23
24
|
import pydantic
|
|
24
25
|
import sqlalchemy.dialects.postgresql
|
|
@@ -35,7 +36,6 @@ from .....utils import (
|
|
|
35
36
|
from ...column import mapped_column
|
|
36
37
|
from ...interface import SQLAInterface
|
|
37
38
|
from ...model import Model
|
|
38
|
-
from ...session import SQLASession
|
|
39
39
|
from ._decorators_mixin import DecoratorsMixin
|
|
40
40
|
from ._lifecycle_mixin import LifecycleMixin
|
|
41
41
|
from ._processing_mixin import ProcessingMixin
|
|
@@ -220,26 +220,26 @@ class Audit(
|
|
|
220
220
|
The scheduler for running audit inserts and callbacks in the background.
|
|
221
221
|
"""
|
|
222
222
|
|
|
223
|
-
_session_callbacks_after_commit =
|
|
223
|
+
_session_callbacks_after_commit = weakref.WeakKeyDictionary[
|
|
224
224
|
sqlalchemy.orm.Session,
|
|
225
225
|
list[
|
|
226
226
|
collections.abc.Callable[
|
|
227
227
|
[], None | collections.abc.Coroutine[typing.Any, typing.Any, None]
|
|
228
228
|
]
|
|
229
|
-
]
|
|
230
|
-
](
|
|
229
|
+
],
|
|
230
|
+
]()
|
|
231
231
|
"""
|
|
232
|
-
Dictionary mapping SQLAlchemy sessions to their commit callbacks.
|
|
232
|
+
Dictionary mapping SQLAlchemy sessions to their commit callbacks. Weakly keyed, so an entry cannot outlive its session.
|
|
233
233
|
"""
|
|
234
|
-
_session_is_commited =
|
|
234
|
+
_session_is_commited = weakref.WeakKeyDictionary[sqlalchemy.orm.Session, bool]()
|
|
235
235
|
"""
|
|
236
|
-
Dictionary mapping SQLAlchemy sessions to a flag indicating if they have been committed.
|
|
236
|
+
Dictionary mapping SQLAlchemy sessions to a flag indicating if they have been committed. Weakly keyed, so an entry cannot outlive its session.
|
|
237
237
|
"""
|
|
238
|
-
_session_pending_entries =
|
|
238
|
+
_session_pending_entries = weakref.WeakKeyDictionary[
|
|
239
239
|
sqlalchemy.orm.Session, list[AuditEntry]
|
|
240
|
-
](
|
|
240
|
+
]()
|
|
241
241
|
"""
|
|
242
|
-
Dictionary mapping SQLAlchemy sessions to pending audit entries collected across flushes.
|
|
242
|
+
Dictionary mapping SQLAlchemy sessions to pending audit entries collected across flushes. Weakly keyed, so an entry cannot outlive its session.
|
|
243
243
|
"""
|
|
244
244
|
_coalesce_updates = True
|
|
245
245
|
"""
|
|
@@ -2,6 +2,7 @@ import abc
|
|
|
2
2
|
import asyncio
|
|
3
3
|
import collections
|
|
4
4
|
import collections.abc
|
|
5
|
+
import copy
|
|
5
6
|
import enum
|
|
6
7
|
import inspect
|
|
7
8
|
import typing
|
|
@@ -14,6 +15,7 @@ from ..exceptions import FastAPIReactToolkitException, raise_exception
|
|
|
14
15
|
from ..globals import g
|
|
15
16
|
from ..schemas import PRIMARY_KEY, ColumnEnumInfo, ColumnInfo, ColumnRelationInfo
|
|
16
17
|
from ..utils import AsyncTaskRunner, C, T, deep_merge, lazy_self, smart_run
|
|
18
|
+
from ..utils import lazy as o_lazy
|
|
17
19
|
from .db import DBQueryParams
|
|
18
20
|
from .file_manager import AbstractFileManager, AbstractImageManager
|
|
19
21
|
from .filter import AbstractBaseFilter, AbstractBaseOprFilter
|
|
@@ -222,6 +224,20 @@ class AbstractInterface(abc.ABC, typing.Generic[T, C, CT]):
|
|
|
222
224
|
self.obj = obj
|
|
223
225
|
self.with_fk = with_fk
|
|
224
226
|
|
|
227
|
+
def clone(self) -> typing.Self:
|
|
228
|
+
"""
|
|
229
|
+
Returns a copy of this interface with its cached lazy attributes reset.
|
|
230
|
+
|
|
231
|
+
Cached lazy attributes hold per-owner state, so a copy that kept them would still be mutated by every owner.
|
|
232
|
+
|
|
233
|
+
Returns:
|
|
234
|
+
typing.Self: A copy bound to the same model, with its cached lazy attributes unset.
|
|
235
|
+
"""
|
|
236
|
+
clone = copy.copy(self)
|
|
237
|
+
for name in [x for x in clone.__dict__ if o_lazy.is_lazy(type(self), x)]:
|
|
238
|
+
del clone.__dict__[name]
|
|
239
|
+
return clone
|
|
240
|
+
|
|
225
241
|
"""
|
|
226
242
|
--------------------------------------------------------------------------------------------------------
|
|
227
243
|
DEPENDENCIES METHODS - to be implemented
|
|
@@ -62,6 +62,79 @@ class TestConstructor:
|
|
|
62
62
|
assert called == ["ps"]
|
|
63
63
|
|
|
64
64
|
|
|
65
|
+
class TestDatamodelClonedPerSubclass:
|
|
66
|
+
"""A subclass that inherits ``datamodel`` gets its own copy of the
|
|
67
|
+
interface, so per-owner state (e.g. ``opr_filters``) is never shared."""
|
|
68
|
+
|
|
69
|
+
def test_subclass_without_datamodel_gets_own_interface(self, Widget_cls):
|
|
70
|
+
class TierDMBaseApi(ModelRestApi):
|
|
71
|
+
datamodel = SQLAInterface(Widget_cls)
|
|
72
|
+
|
|
73
|
+
class TierDMChildApi(TierDMBaseApi):
|
|
74
|
+
pass
|
|
75
|
+
|
|
76
|
+
class TierDMGrandchildApi(TierDMChildApi):
|
|
77
|
+
pass
|
|
78
|
+
|
|
79
|
+
assert TierDMChildApi.datamodel is not TierDMBaseApi.datamodel
|
|
80
|
+
assert TierDMChildApi.datamodel.obj is Widget_cls
|
|
81
|
+
assert TierDMGrandchildApi.datamodel is not TierDMChildApi.datamodel
|
|
82
|
+
assert TierDMGrandchildApi.datamodel is not TierDMBaseApi.datamodel
|
|
83
|
+
assert TierDMGrandchildApi.datamodel.obj is Widget_cls
|
|
84
|
+
|
|
85
|
+
def test_subclass_declaring_datamodel_keeps_identity(self, Widget_cls):
|
|
86
|
+
shared = SQLAInterface(Widget_cls)
|
|
87
|
+
|
|
88
|
+
class TierDMSharedAApi(ModelRestApi):
|
|
89
|
+
datamodel = shared
|
|
90
|
+
|
|
91
|
+
class TierDMSharedBApi(ModelRestApi):
|
|
92
|
+
datamodel = shared
|
|
93
|
+
|
|
94
|
+
assert TierDMSharedAApi.datamodel is shared
|
|
95
|
+
assert TierDMSharedBApi.datamodel is shared
|
|
96
|
+
|
|
97
|
+
def test_subclass_with_no_datamodel_anywhere_does_not_crash_at_definition(self):
|
|
98
|
+
class TierDMNoDatamodelApi(ModelRestApi):
|
|
99
|
+
pass
|
|
100
|
+
|
|
101
|
+
with pytest.raises(Exception, match="datamodel must be set"):
|
|
102
|
+
TierDMNoDatamodelApi()
|
|
103
|
+
|
|
104
|
+
def test_per_class_opr_filters_isolation(self, Widget_cls):
|
|
105
|
+
from fastapi_rtk.backends.sqla.filters import BaseOprFilter
|
|
106
|
+
|
|
107
|
+
class OprFilterA(BaseOprFilter):
|
|
108
|
+
name = "A"
|
|
109
|
+
arg_name = "opr_a"
|
|
110
|
+
|
|
111
|
+
def apply(self, statement, value):
|
|
112
|
+
return statement
|
|
113
|
+
|
|
114
|
+
class OprFilterB(BaseOprFilter):
|
|
115
|
+
name = "B"
|
|
116
|
+
arg_name = "opr_b"
|
|
117
|
+
|
|
118
|
+
def apply(self, statement, value):
|
|
119
|
+
return statement
|
|
120
|
+
|
|
121
|
+
class TierDMOprBaseApi(ModelRestApi):
|
|
122
|
+
datamodel = SQLAInterface(Widget_cls)
|
|
123
|
+
|
|
124
|
+
class TierDMOprAApi(TierDMOprBaseApi):
|
|
125
|
+
opr_filters = [OprFilterA]
|
|
126
|
+
|
|
127
|
+
class TierDMOprBApi(TierDMOprBaseApi):
|
|
128
|
+
opr_filters = [OprFilterB]
|
|
129
|
+
|
|
130
|
+
api_a = TierDMOprAApi()
|
|
131
|
+
api_b = TierDMOprBApi()
|
|
132
|
+
|
|
133
|
+
assert api_a.datamodel is not api_b.datamodel
|
|
134
|
+
assert [type(f) for f in api_a.datamodel.opr_filters] == [OprFilterA]
|
|
135
|
+
assert [type(f) for f in api_b.datamodel.opr_filters] == [OprFilterB]
|
|
136
|
+
|
|
137
|
+
|
|
65
138
|
class TestRoutesList:
|
|
66
139
|
def test_default_routes(self, Widget_cls):
|
|
67
140
|
class TierRApi(ModelRestApi):
|
|
@@ -354,3 +427,157 @@ class TestInitPropertiesBranches:
|
|
|
354
427
|
api = Api()
|
|
355
428
|
# Filters should be initialized (instantiated)
|
|
356
429
|
assert len(api.add_query_rel_fields["country"]) == 1
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
class TestSubclassIdempotency:
|
|
433
|
+
"""Instantiating several Api classes of one inheritance chain must not stack
|
|
434
|
+
state on the shared datamodel or rewrite shared class attributes."""
|
|
435
|
+
|
|
436
|
+
def test_search_filters_not_stacked_by_subclasses(self, Widget_cls):
|
|
437
|
+
from fastapi_rtk.backends.sqla.filters import FilterEqual
|
|
438
|
+
|
|
439
|
+
class Parent(ModelRestApi):
|
|
440
|
+
datamodel = SQLAInterface(Widget_cls)
|
|
441
|
+
search_filters = {"name": [FilterEqual]}
|
|
442
|
+
|
|
443
|
+
class Child(Parent):
|
|
444
|
+
pass
|
|
445
|
+
|
|
446
|
+
class GrandChild(Child):
|
|
447
|
+
pass
|
|
448
|
+
|
|
449
|
+
parent = Parent()
|
|
450
|
+
|
|
451
|
+
def count():
|
|
452
|
+
return sum(
|
|
453
|
+
isinstance(f, FilterEqual) for f in parent.datamodel.filters["name"]
|
|
454
|
+
)
|
|
455
|
+
|
|
456
|
+
expected = count()
|
|
457
|
+
assert expected >= 1
|
|
458
|
+
|
|
459
|
+
Child()
|
|
460
|
+
GrandChild()
|
|
461
|
+
Parent()
|
|
462
|
+
assert count() == expected
|
|
463
|
+
|
|
464
|
+
def test_search_exclude_filters_not_stacked_by_subclasses(self, Widget_cls):
|
|
465
|
+
class Parent(ModelRestApi):
|
|
466
|
+
datamodel = SQLAInterface(Widget_cls)
|
|
467
|
+
search_exclude_filters = {"name": ["eq"]}
|
|
468
|
+
|
|
469
|
+
class Child(Parent):
|
|
470
|
+
pass
|
|
471
|
+
|
|
472
|
+
Parent()
|
|
473
|
+
Child()
|
|
474
|
+
Child()
|
|
475
|
+
assert Parent.datamodel.exclude_filters["name"] == ["eq"]
|
|
476
|
+
assert Child.datamodel.exclude_filters["name"] == ["eq"]
|
|
477
|
+
|
|
478
|
+
def test_query_rel_fields_survive_second_instantiation(self, WidgetWithCountry_cls):
|
|
479
|
+
from fastapi_rtk.backends.sqla.filters import FilterEqual
|
|
480
|
+
|
|
481
|
+
class Parent(ModelRestApi):
|
|
482
|
+
datamodel = SQLAInterface(WidgetWithCountry_cls)
|
|
483
|
+
add_query_rel_fields = {"country": [["name", FilterEqual, "Germany"]]}
|
|
484
|
+
edit_query_rel_fields = {"country": [["name", FilterEqual, "Germany"]]}
|
|
485
|
+
search_query_rel_fields = {"country": [["name", FilterEqual, "Germany"]]}
|
|
486
|
+
|
|
487
|
+
class Child(Parent):
|
|
488
|
+
pass
|
|
489
|
+
|
|
490
|
+
Parent()
|
|
491
|
+
assert Parent.add_query_rel_fields["country"][0][1] is FilterEqual
|
|
492
|
+
|
|
493
|
+
child = Child()
|
|
494
|
+
assert isinstance(child.add_query_rel_fields["country"][0][1], FilterEqual)
|
|
495
|
+
assert isinstance(child.edit_query_rel_fields["country"][0][1], FilterEqual)
|
|
496
|
+
assert isinstance(child.search_query_rel_fields["country"][0][1], FilterEqual)
|
|
497
|
+
|
|
498
|
+
def test_routes_class_attribute_not_mutated(self, Widget_cls):
|
|
499
|
+
class Parent(ModelRestApi):
|
|
500
|
+
datamodel = SQLAInterface(Widget_cls)
|
|
501
|
+
routes = ["info", "get_list", "image"]
|
|
502
|
+
|
|
503
|
+
class Child(Parent):
|
|
504
|
+
pass
|
|
505
|
+
|
|
506
|
+
Parent()
|
|
507
|
+
assert Parent.routes == ["info", "get_list", "image"]
|
|
508
|
+
|
|
509
|
+
child = Child()
|
|
510
|
+
assert "image" not in child.routes
|
|
511
|
+
assert Parent.routes == ["info", "get_list", "image"]
|
|
512
|
+
|
|
513
|
+
def test_routes_and_permissions_are_per_instance(self, Widget_cls):
|
|
514
|
+
class Parent(ModelRestApi):
|
|
515
|
+
datamodel = SQLAInterface(Widget_cls)
|
|
516
|
+
|
|
517
|
+
class Child(Parent):
|
|
518
|
+
pass
|
|
519
|
+
|
|
520
|
+
parent = Parent()
|
|
521
|
+
child = Child()
|
|
522
|
+
assert len(child.routes_to_register) == len(parent.routes_to_register)
|
|
523
|
+
assert len(child.permissions) == len(parent.permissions)
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
class TestInfoSchemaDottedSearchFiltersNotStacked:
|
|
527
|
+
"""``_generate_info_schema`` builds the ``/_info`` filter list for a dotted
|
|
528
|
+
search column from ``related_interface.filters[rel_col]``, a list shared by
|
|
529
|
+
every Api instance touching that relation. Aliasing it instead of copying
|
|
530
|
+
let repeated instantiation of Api classes on one inheritance chain grow it
|
|
531
|
+
by one duplicate operator each time.
|
|
532
|
+
"""
|
|
533
|
+
|
|
534
|
+
@pytest.fixture
|
|
535
|
+
def dotted_search_api_cls(self, WidgetWithCountry_cls):
|
|
536
|
+
from fastapi_rtk.backends.sqla.filters import FilterEqual
|
|
537
|
+
|
|
538
|
+
class Api(ModelRestApi):
|
|
539
|
+
datamodel = SQLAInterface(WidgetWithCountry_cls)
|
|
540
|
+
search_columns = ["country.name"]
|
|
541
|
+
# `search_filters` on a dotted key lands on the root datamodel's own
|
|
542
|
+
# ``filters["country.name"]`` entry, which `_generate_info_schema`
|
|
543
|
+
# then extends the related interface's list with.
|
|
544
|
+
search_filters = {"country.name": [FilterEqual]}
|
|
545
|
+
|
|
546
|
+
return Api
|
|
547
|
+
|
|
548
|
+
async def test_related_filter_list_not_grown_across_instances(
|
|
549
|
+
self, dotted_search_api_cls
|
|
550
|
+
):
|
|
551
|
+
api1 = dotted_search_api_cls()
|
|
552
|
+
rel_interface = api1.datamodel.get_related_interface("country")
|
|
553
|
+
before = len(rel_interface.filters["name"])
|
|
554
|
+
|
|
555
|
+
info1 = await api1._generate_info_schema([], None, None)
|
|
556
|
+
after_first = len(rel_interface.filters["name"])
|
|
557
|
+
assert after_first == before
|
|
558
|
+
|
|
559
|
+
api2 = dotted_search_api_cls()
|
|
560
|
+
info2 = await api2._generate_info_schema([], None, None)
|
|
561
|
+
after_second = len(rel_interface.filters["name"])
|
|
562
|
+
assert after_second == before
|
|
563
|
+
|
|
564
|
+
assert (
|
|
565
|
+
info1.filters["country.name"]["filters"]
|
|
566
|
+
== (info2.filters["country.name"]["filters"])
|
|
567
|
+
)
|
|
568
|
+
|
|
569
|
+
async def test_search_exclude_filters_still_removes_excluded_operator(
|
|
570
|
+
self, WidgetWithCountry_cls
|
|
571
|
+
):
|
|
572
|
+
from fastapi_rtk.backends.sqla.filters import FilterEqual
|
|
573
|
+
|
|
574
|
+
class Api(ModelRestApi):
|
|
575
|
+
datamodel = SQLAInterface(WidgetWithCountry_cls)
|
|
576
|
+
search_columns = ["country.name"]
|
|
577
|
+
search_filters = {"country.name": [FilterEqual]}
|
|
578
|
+
search_exclude_filters = {"country.name": ["eq"]}
|
|
579
|
+
|
|
580
|
+
api = Api()
|
|
581
|
+
info = await api._generate_info_schema([], None, None)
|
|
582
|
+
operators = [f["operator"] for f in info.filters["country.name"]["filters"]]
|
|
583
|
+
assert "eq" not in operators
|