fastapi-rtk 2.11.0__tar.gz → 2.11.2__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.0 → fastapi_rtk-2.11.2}/PKG-INFO +1 -1
- fastapi_rtk-2.11.2/fastapi_rtk/_version.py +1 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/api/model_rest_api/_columns_mixin.py +32 -4
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/api/model_rest_api/_info_mixin.py +1 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/api/model_rest_api/_typed_base.py +2 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/backends/sqla/filters.py +34 -16
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/security/sqla/models.py +9 -3
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/api/model_rest_api/test_columns.py +343 -5
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/backends/sqla/test_filters.py +52 -1
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/security/sqla/test_models.py +7 -0
- fastapi_rtk-2.11.0/fastapi_rtk/_version.py +0 -1
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/.gitignore +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/LICENSE +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/README.md +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/__init__.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/api/__init__.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/api/base_api.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/api/model_rest_api/__init__.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/api/model_rest_api/_add_mixin.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/api/model_rest_api/_bulk_mixin.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/api/model_rest_api/_delete_mixin.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/api/model_rest_api/_download_mixin.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/api/model_rest_api/_edit_mixin.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/api/model_rest_api/_file_mixin.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/api/model_rest_api/_list_mixin.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/api/model_rest_api/_params.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/api/model_rest_api/_show_mixin.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/api/model_rest_api/_upload_mixin.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/apis/__init__.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/apis/info.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/apis/license.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/auth/__init__.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/auth/auth.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/auth/hashers/__init__.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/auth/hashers/pbkdf2.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/auth/hashers/scrypt.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/auth/ldap.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/auth/password_helpers/__init__.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/auth/password_helpers/fab.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/auth/strategies/__init__.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/auth/strategies/config.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/auth/strategies/db.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/auth/strategies/jwt.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/backends/__init__.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/backends/generic/__init__.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/backends/generic/column.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/backends/generic/db.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/backends/generic/exceptions.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/backends/generic/filters.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/backends/generic/interface.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/backends/generic/model.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/backends/generic/session.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/backends/sqla/__init__.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/backends/sqla/column.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/backends/sqla/db.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/backends/sqla/exceptions.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/backends/sqla/extensions/__init__.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/backends/sqla/extensions/audit/__init__.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/backends/sqla/extensions/audit/_decorators_mixin.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/backends/sqla/extensions/audit/_lifecycle_mixin.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/backends/sqla/extensions/audit/_logger.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/backends/sqla/extensions/audit/_processing_mixin.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/backends/sqla/extensions/audit/audit.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/backends/sqla/extensions/audit/types.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/backends/sqla/extensions/geoalchemy2/__init__.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/backends/sqla/extensions/geoalchemy2/filters.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/backends/sqla/extensions/geoalchemy2/geometry_converter.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/backends/sqla/interface.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/backends/sqla/model.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/backends/sqla/session.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/bases/__init__.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/bases/db.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/bases/file_manager.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/bases/filter.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/bases/interface.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/bases/model.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/bases/session.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/cli/__init__.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/cli/cli.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/cli/commands/__init__.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/cli/commands/_security_crypto.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/cli/commands/db/__init__.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/cli/commands/db/templates/fastapi/README +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/cli/commands/db/templates/fastapi/alembic.ini.mako +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/cli/commands/db/templates/fastapi/env.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/cli/commands/db/templates/fastapi/script.py.mako +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/cli/commands/db/templates/fastapi-multidb/README +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/cli/commands/db/templates/fastapi-multidb/alembic.ini.mako +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/cli/commands/db/templates/fastapi-multidb/env.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/cli/commands/db/templates/fastapi-multidb/script.py.mako +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/cli/commands/export.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/cli/commands/security.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/cli/commands/translate.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/cli/const.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/cli/decorators.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/cli/types.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/cli/utils.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/config.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/const.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/db.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/decorators.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/dependencies.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/exceptions.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/fastapi_react_toolkit.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/file_managers/__init__.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/file_managers/file_manager.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/file_managers/image_manager.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/file_managers/s3_file_manager.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/file_managers/s3_image_manager.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/filters.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/globals.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/lang/__init__.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/lang/babel/__init__.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/lang/babel/cli.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/lang/babel/config.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/lang/babel.cfg +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/lang/lazy_text.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/lang/messages.pot +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/lang/translations/de/LC_MESSAGES/messages.mo +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/lang/translations/de/LC_MESSAGES/messages.po +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/lang/translations/en/LC_MESSAGES/messages.mo +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/lang/translations/en/LC_MESSAGES/messages.po +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/manager.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/middlewares.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/mixins.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/models.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/py.typed +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/registry.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/routers.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/schemas.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/security/__init__.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/security/sqla/__init__.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/security/sqla/apis.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/security/sqla/security_manager/__init__.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/security/sqla/security_manager/_association_mixin.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/security/sqla/security_manager/_builtin_role_mixin.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/security/sqla/security_manager/_cleanup_mixin.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/security/sqla/security_manager/_exception_filters.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/security/sqla/security_manager/_export.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/security/sqla/security_manager/_import.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/security/sqla/security_manager/_role_mixin.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/security/sqla/security_manager/_user_mixin.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/setting.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/types.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/utils/__init__.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/utils/async_task_runner.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/utils/class_factory.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/utils/csv_json_converter.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/utils/deep_merge.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/utils/extender_mixin.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/utils/flask_appbuilder_utils.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/utils/formatter.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/utils/hooks.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/utils/lazy.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/utils/merge_schema.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/utils/multiple_async_contexts.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/utils/pydantic.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/utils/run_utils.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/utils/self_dependencies.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/utils/smartdefaultdict.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/utils/sqla.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/utils/timezone.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/utils/update_signature.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/utils/use_default_when_none.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/version.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/pyproject.toml +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/conftest.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/docker-compose.gis.yml +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/integration/__init__.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/integration/api/__init__.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/integration/api/test_model_rest_api.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/integration/apis/__init__.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/integration/apis/test_info_callable_schema.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/integration/apis/test_info_contract.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/integration/auth/__init__.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/integration/auth/test_auth_flow.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/integration/auth/test_oauth.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/integration/conftest.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/integration/file_managers/__init__.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/integration/file_managers/test_file_routes.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/integration/security/__init__.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/integration/security/sqla/__init__.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/integration/security/sqla/test_security_manager.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/integration/test_app_smoke.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/__init__.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/api/__init__.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/api/conftest.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/api/model_rest_api/__init__.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/api/model_rest_api/test_file_mixin.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/api/model_rest_api/test_init.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/api/model_rest_api/test_typed_base_consistency.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/api/test_base_api.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/apis/__init__.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/apis/test_info.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/apis/test_license.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/auth/__init__.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/auth/hashers/__init__.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/auth/hashers/test_pbkdf2.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/auth/hashers/test_scrypt.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/auth/password_helpers/__init__.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/auth/password_helpers/test_fab.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/auth/strategies/__init__.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/auth/strategies/test_config.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/auth/strategies/test_db.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/auth/strategies/test_jwt.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/auth/test_auth.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/backends/__init__.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/backends/generic/__init__.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/backends/generic/test_column.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/backends/generic/test_db.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/backends/generic/test_exceptions.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/backends/generic/test_filters.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/backends/generic/test_interface.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/backends/generic/test_model.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/backends/generic/test_session.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/backends/sqla/__init__.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/backends/sqla/conftest.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/backends/sqla/test_audit.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/backends/sqla/test_audit_types.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/backends/sqla/test_column.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/backends/sqla/test_db.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/backends/sqla/test_geoalchemy2.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/backends/sqla/test_interface.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/backends/sqla/test_model.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/backends/sqla/test_session.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/bases/__init__.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/bases/test_db.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/bases/test_file_manager.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/bases/test_filter.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/bases/test_interface.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/bases/test_model.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/bases/test_session.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/cli/__init__.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/cli/commands/__init__.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/cli/commands/conftest.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/cli/commands/test_db.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/cli/commands/test_export.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/cli/commands/test_security.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/cli/commands/test_security_crypto.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/cli/commands/test_translate.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/cli/conftest.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/cli/test_cli.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/cli/test_decorators.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/cli/test_types.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/cli/test_utils.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/file_managers/__init__.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/file_managers/conftest.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/file_managers/test_file_manager.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/file_managers/test_image_manager.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/file_managers/test_s3_file_manager.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/file_managers/test_s3_image_manager.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/lang/__init__.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/lang/test_babel_cli.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/lang/test_babel_config.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/lang/test_lazy_text.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/security/__init__.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/security/sqla/__init__.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/security/sqla/conftest.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/security/sqla/test_apis.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/security/sqla/test_security_manager.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/test_config.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/test_const.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/test_db.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/test_decorators.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/test_dependencies.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/test_exceptions.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/test_fastapi_react_toolkit.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/test_filters.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/test_globals.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/test_manager.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/test_middlewares.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/test_mixins.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/test_models.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/test_registry.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/test_routers.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/test_schemas.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/test_setting.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/test_types.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/test_version.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/utils/__init__.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/utils/test_async_task_runner.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/utils/test_class_factory.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/utils/test_csv_json_converter.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/utils/test_deep_merge.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/utils/test_extender_mixin.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/utils/test_flask_appbuilder_utils.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/utils/test_formatter.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/utils/test_hooks.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/utils/test_lazy.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/utils/test_merge_schema.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/utils/test_multiple_async_contexts.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/utils/test_pydantic.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/utils/test_run_utils.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/utils/test_self_dependencies.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/utils/test_smartdefaultdict.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/utils/test_sqla.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/utils/test_timezone.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/utils/test_update_signature.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/utils/test_use_default_when_none.py +0 -0
- {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fastapi-rtk
|
|
3
|
-
Version: 2.11.
|
|
3
|
+
Version: 2.11.2
|
|
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.11.2"
|
|
@@ -47,6 +47,8 @@ from ...utils import (
|
|
|
47
47
|
)
|
|
48
48
|
from ._typed_base import _TypedModelRestApi
|
|
49
49
|
|
|
50
|
+
_UNUSED_SCHEMA_KEYS = ("anyOf", "default", "$ref", "const")
|
|
51
|
+
|
|
50
52
|
|
|
51
53
|
class ColumnsMixin(_TypedModelRestApi):
|
|
52
54
|
"""
|
|
@@ -546,6 +548,8 @@ class ColumnsMixin(_TypedModelRestApi):
|
|
|
546
548
|
[ColumnInfo | ColumnEnumInfo | ColumnRelationInfo | None], bool
|
|
547
549
|
],
|
|
548
550
|
] = False,
|
|
551
|
+
*,
|
|
552
|
+
mark_non_nullable_required: bool = False,
|
|
549
553
|
):
|
|
550
554
|
"""
|
|
551
555
|
Generate the JSONForms schema for the API.
|
|
@@ -555,6 +559,7 @@ class ColumnsMixin(_TypedModelRestApi):
|
|
|
555
559
|
info_columns (list[ColumnInfo | ColumnEnumInfo | ColumnRelationInfo]): The list of columns to include in the JSONForms schema.
|
|
556
560
|
use_enum (bool | Callable[[ColumnInfo | ColumnEnumInfo | ColumnRelationInfo], bool] | dict[str, bool | Callable[[ColumnInfo | ColumnEnumInfo | ColumnRelationInfo], bool]], optional): A flag or a callable or a dictionary to determine whether to use enum instead of oneOf. Defaults to False.
|
|
557
561
|
list_as_object (bool | Callable[[ColumnInfo | ColumnEnumInfo | ColumnRelationInfo | None], bool] | dict[str, bool | Callable[[ColumnInfo | ColumnEnumInfo | ColumnRelationInfo | None], bool]], optional): A flag or a callable or a dictionary to determine whether to render a list column as a single `object` control instead of the per-item `array` table. The callable receives the column's `ColumnInfo` when available, else `None`. Defaults to False.
|
|
562
|
+
mark_non_nullable_required (bool, optional): Whether to also mark every non-nullable column present in `properties` as required, in addition to whatever pydantic already marked, excluding `password`-formatted properties, which are never sent back to the client and would force a retype on every edit, and `readOnly` properties, which cannot be filled at all. Defaults to False.
|
|
558
563
|
|
|
559
564
|
Returns:
|
|
560
565
|
dict[str, typing.Any]: The generated JSONForms schema.
|
|
@@ -594,8 +599,15 @@ class ColumnsMixin(_TypedModelRestApi):
|
|
|
594
599
|
)
|
|
595
600
|
if self.datamodel.is_files(key) or self.datamodel.is_images(key):
|
|
596
601
|
current_value = value.copy()
|
|
602
|
+
for unused_key in _UNUSED_SCHEMA_KEYS:
|
|
603
|
+
current_value.pop(unused_key, None)
|
|
604
|
+
# JsonForms seeds an inserted row from this default, and a null row must fail so an empty slot cannot be submitted.
|
|
605
|
+
current_value["default"] = None
|
|
597
606
|
value["type"] = "array"
|
|
598
607
|
value["items"] = current_value
|
|
608
|
+
# The file keys belong on the leaf; nothing reads them off the array wrapper.
|
|
609
|
+
value.pop("x-file-types", None)
|
|
610
|
+
value.pop("contentMediaType", None)
|
|
599
611
|
elif self.datamodel.is_boolean(key):
|
|
600
612
|
value["type"] = "boolean"
|
|
601
613
|
elif self.datamodel.is_integer(key):
|
|
@@ -676,12 +688,15 @@ class ColumnsMixin(_TypedModelRestApi):
|
|
|
676
688
|
# Check whether it is nullable
|
|
677
689
|
if self.datamodel.is_nullable(key):
|
|
678
690
|
value["type"] = [value["type"], "null"]
|
|
691
|
+
# With setNull on, a cleared field writes a real null, which ajv checks against enum / oneOf regardless of the null type member.
|
|
692
|
+
if "enum" in value:
|
|
693
|
+
value["enum"] = [None, *value["enum"]]
|
|
694
|
+
elif "oneOf" in value:
|
|
695
|
+
value["oneOf"] = [{"const": None, "title": ""}, *value["oneOf"]]
|
|
679
696
|
|
|
680
697
|
# Remove unused vars
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
value.pop("$ref", None)
|
|
684
|
-
value.pop("const", None)
|
|
698
|
+
for unused_key in _UNUSED_SCHEMA_KEYS:
|
|
699
|
+
value.pop(unused_key, None)
|
|
685
700
|
|
|
686
701
|
# Check whether the value should be a `secret`
|
|
687
702
|
if key in g.sensitive_data.get(self.datamodel.obj.__name__, []):
|
|
@@ -689,4 +704,17 @@ class ColumnsMixin(_TypedModelRestApi):
|
|
|
689
704
|
|
|
690
705
|
result["properties"][key] = value
|
|
691
706
|
|
|
707
|
+
if mark_non_nullable_required:
|
|
708
|
+
pydantic_required = result.get("required", [])
|
|
709
|
+
added_required = [
|
|
710
|
+
key
|
|
711
|
+
for key, value in result["properties"].items()
|
|
712
|
+
# A password is never sent back to the client and a readOnly field cannot be filled, so requiring either would fail the moment the form opens.
|
|
713
|
+
if key not in pydantic_required
|
|
714
|
+
and not self.datamodel.is_nullable(key)
|
|
715
|
+
and value.get("format") != "password"
|
|
716
|
+
and not value.get("readOnly")
|
|
717
|
+
]
|
|
718
|
+
result["required"] = [*pydantic_required, *added_required]
|
|
719
|
+
|
|
692
720
|
return result
|
|
@@ -204,6 +204,8 @@ class _TypedModelRestApi:
|
|
|
204
204
|
[ColumnInfo | ColumnEnumInfo | ColumnRelationInfo | None], bool
|
|
205
205
|
],
|
|
206
206
|
] = False,
|
|
207
|
+
*,
|
|
208
|
+
mark_non_nullable_required: bool = False,
|
|
207
209
|
) -> dict[str, typing.Any]: ...
|
|
208
210
|
|
|
209
211
|
def _get_datamodel_and_column(self, col: str) -> tuple[SQLAInterface, str]: ...
|
|
@@ -42,6 +42,10 @@ __all__ = [
|
|
|
42
42
|
]
|
|
43
43
|
|
|
44
44
|
|
|
45
|
+
# Oracle needs a length on the CAST target: CAST(x AS VARCHAR2) is ORA-00906.
|
|
46
|
+
_TEXT_CAST_TYPE = sa_types.String().with_variant(sa_types.String(4000), "oracle")
|
|
47
|
+
|
|
48
|
+
|
|
45
49
|
class BaseFilter(AbstractBaseFilter[sqlalchemy.Select[Model]]):
|
|
46
50
|
def _get_column(self, col_name: str) -> sqlalchemy.Column:
|
|
47
51
|
"""
|
|
@@ -143,6 +147,28 @@ class BaseFilter(AbstractBaseFilter[sqlalchemy.Select[Model]]):
|
|
|
143
147
|
|
|
144
148
|
|
|
145
149
|
class BaseFilterTextContains(BaseFilter):
|
|
150
|
+
def _build_concat_expr(self, col_objs: list[sqlalchemy.ColumnElement]):
|
|
151
|
+
"""
|
|
152
|
+
Concatenate columns as text, separated by a space.
|
|
153
|
+
|
|
154
|
+
Chains ``||`` instead of ``concat(*args)``: Oracle's CONCAT takes exactly two
|
|
155
|
+
arguments and other databases cap function-call args, which wide tables exceed.
|
|
156
|
+
``coalesce`` keeps concat's NULL-as-empty.
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
col_objs (list[sqlalchemy.ColumnElement]): Columns to concatenate.
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
sqlalchemy.ColumnElement | None: The concatenation, or None when col_objs is empty.
|
|
163
|
+
"""
|
|
164
|
+
concat_expr = None
|
|
165
|
+
for col_obj in col_objs:
|
|
166
|
+
piece = sqlalchemy.func.coalesce(
|
|
167
|
+
sqlalchemy.cast(col_obj, _TEXT_CAST_TYPE), ""
|
|
168
|
+
)
|
|
169
|
+
concat_expr = piece if concat_expr is None else concat_expr + " " + piece
|
|
170
|
+
return concat_expr
|
|
171
|
+
|
|
146
172
|
def _handle_relation_contains(self, col: str, value: str):
|
|
147
173
|
sub_col = ""
|
|
148
174
|
if "." in col:
|
|
@@ -168,16 +194,14 @@ class BaseFilterTextContains(BaseFilter):
|
|
|
168
194
|
else:
|
|
169
195
|
# If there is no sub-column, we filter all columns in the relation
|
|
170
196
|
rel_cols = rel_interface.list_columns.keys()
|
|
171
|
-
concat = []
|
|
172
197
|
rel_obj = rel_interface.obj
|
|
173
198
|
rel_pks = rel_interface.get_pk_attrs()
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
concat.append(" ")
|
|
199
|
+
concat_expr = self._build_concat_expr(
|
|
200
|
+
[getattr(rel_obj, col_name) for col_name in rel_cols]
|
|
201
|
+
)
|
|
178
202
|
rel_statements = [
|
|
179
203
|
sqlalchemy.select(getattr(rel_obj, pk)).filter(
|
|
180
|
-
|
|
204
|
+
concat_expr.ilike("%" + value + "%")
|
|
181
205
|
)
|
|
182
206
|
for pk in rel_pks
|
|
183
207
|
]
|
|
@@ -210,7 +234,7 @@ class BaseFilterTextContains(BaseFilter):
|
|
|
210
234
|
if any(value in keywords for keywords in true_keywords)
|
|
211
235
|
else False
|
|
212
236
|
)
|
|
213
|
-
return sqlalchemy.cast(self._cast_column(col),
|
|
237
|
+
return sqlalchemy.cast(self._cast_column(col), _TEXT_CAST_TYPE).ilike(
|
|
214
238
|
"%" + value + "%"
|
|
215
239
|
)
|
|
216
240
|
|
|
@@ -491,9 +515,7 @@ class FilterGlobal(BaseFilterTextContains):
|
|
|
491
515
|
filters = []
|
|
492
516
|
|
|
493
517
|
for val in value:
|
|
494
|
-
|
|
495
|
-
# which wide tables exceed. coalesce keeps concat's NULL-as-empty.
|
|
496
|
-
concat_expr = None
|
|
518
|
+
concat_cols = []
|
|
497
519
|
rel_filter_arr = []
|
|
498
520
|
for col in cols:
|
|
499
521
|
if (
|
|
@@ -503,13 +525,9 @@ class FilterGlobal(BaseFilterTextContains):
|
|
|
503
525
|
):
|
|
504
526
|
rel_filter_arr.append(self._handle_text_contains(col, val))
|
|
505
527
|
elif col in self.datamodel.list_columns:
|
|
506
|
-
|
|
507
|
-
sqlalchemy.cast(self._cast_column(col), sa_types.String), ""
|
|
508
|
-
)
|
|
509
|
-
concat_expr = (
|
|
510
|
-
piece if concat_expr is None else concat_expr + " " + piece
|
|
511
|
-
)
|
|
528
|
+
concat_cols.append(self._cast_column(col))
|
|
512
529
|
params = [*rel_filter_arr]
|
|
530
|
+
concat_expr = self._build_concat_expr(concat_cols)
|
|
513
531
|
if concat_expr is not None:
|
|
514
532
|
val = val.strip().lower()
|
|
515
533
|
params.append(concat_expr.ilike("%" + val + "%"))
|
|
@@ -122,7 +122,9 @@ class Api(Model):
|
|
|
122
122
|
id: Mapped[int] = mapped_column(
|
|
123
123
|
sqlalchemy.Integer, sqlalchemy.Sequence(API_SEQUENCE), primary_key=True
|
|
124
124
|
)
|
|
125
|
-
name: Mapped[str] = mapped_column(
|
|
125
|
+
name: Mapped[str] = mapped_column(
|
|
126
|
+
sqlalchemy.String(256), unique=True, nullable=False
|
|
127
|
+
)
|
|
126
128
|
|
|
127
129
|
permissions: Mapped[list[PermissionApi]] = relationship(
|
|
128
130
|
PermissionApi, back_populates="api", cascade="all, delete-orphan"
|
|
@@ -143,7 +145,9 @@ class Permission(Model):
|
|
|
143
145
|
id: Mapped[int] = mapped_column(
|
|
144
146
|
sqlalchemy.Integer, sqlalchemy.Sequence(PERMISSION_SEQUENCE), primary_key=True
|
|
145
147
|
)
|
|
146
|
-
name: Mapped[str] = mapped_column(
|
|
148
|
+
name: Mapped[str] = mapped_column(
|
|
149
|
+
sqlalchemy.String(256), unique=True, nullable=False
|
|
150
|
+
)
|
|
147
151
|
|
|
148
152
|
apis: Mapped[list[PermissionApi]] = relationship(
|
|
149
153
|
PermissionApi, back_populates="permission", cascade="all, delete-orphan"
|
|
@@ -158,7 +162,9 @@ class Role(Model):
|
|
|
158
162
|
id: Mapped[int] = mapped_column(
|
|
159
163
|
sqlalchemy.Integer, sqlalchemy.Sequence(ROLE_SEQUENCE), primary_key=True
|
|
160
164
|
)
|
|
161
|
-
name: Mapped[str] = mapped_column(
|
|
165
|
+
name: Mapped[str] = mapped_column(
|
|
166
|
+
sqlalchemy.String(256), unique=True, nullable=False
|
|
167
|
+
)
|
|
162
168
|
|
|
163
169
|
users: Mapped[list["User"]] = relationship(
|
|
164
170
|
"User", secondary=assoc_user_role, back_populates="roles"
|
|
@@ -6,11 +6,11 @@ Covers the prettify statics, label-column auto-generation,
|
|
|
6
6
|
``_get_datamodel_and_column``, and the ``_jsonforms_*`` schema helpers.
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
|
+
import enum
|
|
9
10
|
import typing
|
|
10
11
|
|
|
11
12
|
import pydantic
|
|
12
13
|
import pytest
|
|
13
|
-
|
|
14
14
|
from fastapi_rtk.api.model_rest_api import ModelRestApi
|
|
15
15
|
from fastapi_rtk.backends.sqla.interface import SQLAInterface
|
|
16
16
|
from fastapi_rtk.schemas import ColumnRelationInfo, RelInfo
|
|
@@ -121,7 +121,6 @@ class TestGetDatamodelAndColumn:
|
|
|
121
121
|
@pytest.fixture
|
|
122
122
|
def Country_cls():
|
|
123
123
|
import sqlalchemy
|
|
124
|
-
|
|
125
124
|
from fastapi_rtk.backends.sqla.model import Model
|
|
126
125
|
|
|
127
126
|
if "tier12_country" in Model.metadata.tables:
|
|
@@ -140,7 +139,6 @@ def Country_cls():
|
|
|
140
139
|
@pytest.fixture
|
|
141
140
|
def WidgetWithCountry_cls(Country_cls):
|
|
142
141
|
import sqlalchemy.orm
|
|
143
|
-
|
|
144
142
|
from fastapi_rtk.backends.sqla.model import Model
|
|
145
143
|
|
|
146
144
|
if "tier12_widget_country" in Model.metadata.tables:
|
|
@@ -289,7 +287,6 @@ class TestGenerateJsonformsSchemaRelationReadOnly:
|
|
|
289
287
|
def m2m_api(self):
|
|
290
288
|
import sqlalchemy
|
|
291
289
|
import sqlalchemy.orm
|
|
292
|
-
|
|
293
290
|
from fastapi_rtk.backends.sqla.model import Model
|
|
294
291
|
|
|
295
292
|
assoc = sqlalchemy.Table(
|
|
@@ -362,7 +359,6 @@ class TestGenerateJsonformsSchemaListBranch:
|
|
|
362
359
|
@pytest.fixture(scope="class")
|
|
363
360
|
def list_api(self):
|
|
364
361
|
import sqlalchemy
|
|
365
|
-
|
|
366
362
|
from fastapi_rtk import (
|
|
367
363
|
JSONBListColumn,
|
|
368
364
|
ListColumn,
|
|
@@ -457,3 +453,345 @@ class TestGenerateJsonformsSchemaListBranch:
|
|
|
457
453
|
result = list_api._generate_jsonforms_schema(schema, [], False)
|
|
458
454
|
assert result["properties"]["tags"]["type"] == ["array", "null"]
|
|
459
455
|
assert result["properties"]["tags"]["items"] == {"type": "string"}
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
class TestGenerateJsonformsSchemaFileBranch:
|
|
459
|
+
"""A multi-file / multi-image column's `items` sub-schema must be the
|
|
460
|
+
cleaned leaf schema (no `anyOf` leftover from the pydantic union), not a
|
|
461
|
+
copy taken before that cleanup runs. It also carries a deliberate
|
|
462
|
+
`default: null` so a freshly inserted row fails validation until a file
|
|
463
|
+
is picked."""
|
|
464
|
+
|
|
465
|
+
@pytest.fixture
|
|
466
|
+
def file_api(self, tmp_path, config_setter):
|
|
467
|
+
import sqlalchemy
|
|
468
|
+
from fastapi_rtk.backends.sqla.model import Model
|
|
469
|
+
from fastapi_rtk.types import FileColumn, ImageColumns
|
|
470
|
+
|
|
471
|
+
upload_folder = tmp_path / "uploads"
|
|
472
|
+
img_folder = tmp_path / "img_uploads"
|
|
473
|
+
upload_folder.mkdir()
|
|
474
|
+
img_folder.mkdir()
|
|
475
|
+
config_setter("UPLOAD_FOLDER", str(upload_folder))
|
|
476
|
+
config_setter("IMG_UPLOAD_FOLDER", str(img_folder))
|
|
477
|
+
|
|
478
|
+
class _TierFileModel(Model):
|
|
479
|
+
__tablename__ = "tier12_file_model"
|
|
480
|
+
__table_args__ = {"extend_existing": True}
|
|
481
|
+
id = sqlalchemy.Column(sqlalchemy.Integer, primary_key=True)
|
|
482
|
+
document = sqlalchemy.Column(FileColumn(allowed_extensions=["txt", "pdf"]))
|
|
483
|
+
gallery = sqlalchemy.Column(ImageColumns(allowed_extensions=["png", "jpg"]))
|
|
484
|
+
|
|
485
|
+
class TierFileApi(ModelRestApi):
|
|
486
|
+
datamodel = SQLAInterface(_TierFileModel)
|
|
487
|
+
|
|
488
|
+
return TierFileApi()
|
|
489
|
+
|
|
490
|
+
@pytest.fixture
|
|
491
|
+
def file_schema_result(self, file_api):
|
|
492
|
+
schema = file_api.datamodel.generate_schema(
|
|
493
|
+
columns=["document", "gallery"],
|
|
494
|
+
with_id=False,
|
|
495
|
+
with_name=False,
|
|
496
|
+
with_property=False,
|
|
497
|
+
optional=True,
|
|
498
|
+
hide_sensitive_columns=False,
|
|
499
|
+
is_form_data=True,
|
|
500
|
+
name="TierFileFormData",
|
|
501
|
+
)
|
|
502
|
+
return file_api._generate_jsonforms_schema(schema, [])
|
|
503
|
+
|
|
504
|
+
def test_multi_image_items_is_the_cleaned_leaf_with_null_default(
|
|
505
|
+
self, file_api, file_schema_result
|
|
506
|
+
):
|
|
507
|
+
from fastapi_rtk.setting import Setting
|
|
508
|
+
|
|
509
|
+
gallery = file_schema_result["properties"]["gallery"]
|
|
510
|
+
assert "anyOf" not in gallery["items"]
|
|
511
|
+
assert gallery["items"] == {
|
|
512
|
+
"title": file_api.label_columns["gallery"],
|
|
513
|
+
"type": "string",
|
|
514
|
+
"default": None,
|
|
515
|
+
"x-file-types": [".png", ".jpg"],
|
|
516
|
+
"contentMediaType": ",".join(Setting.IMG_MIME_TYPES),
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
def test_multi_image_does_not_duplicate_file_keys_on_the_array(
|
|
520
|
+
self, file_schema_result
|
|
521
|
+
):
|
|
522
|
+
gallery = file_schema_result["properties"]["gallery"]
|
|
523
|
+
assert "x-file-types" not in gallery
|
|
524
|
+
assert "contentMediaType" not in gallery
|
|
525
|
+
assert "x-file-types" in gallery["items"]
|
|
526
|
+
assert "contentMediaType" in gallery["items"]
|
|
527
|
+
|
|
528
|
+
def test_multi_image_own_type_is_array_or_null(self, file_schema_result):
|
|
529
|
+
gallery = file_schema_result["properties"]["gallery"]
|
|
530
|
+
assert gallery["type"] == ["array", "null"]
|
|
531
|
+
assert "anyOf" not in gallery
|
|
532
|
+
|
|
533
|
+
def test_single_file_has_no_items_or_anyof(self, file_schema_result):
|
|
534
|
+
document = file_schema_result["properties"]["document"]
|
|
535
|
+
assert "anyOf" not in document
|
|
536
|
+
assert "items" not in document
|
|
537
|
+
assert document["type"] == ["string", "null"]
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
class TestGenerateJsonformsSchemaNullableConstrainedValues:
|
|
541
|
+
"""A nullable field with a constrained value set (enum / oneOf) must gain an
|
|
542
|
+
empty member alongside the ``null`` type, or AJV reports an option-mismatch
|
|
543
|
+
error the moment the frontend clears the field.
|
|
544
|
+
"""
|
|
545
|
+
|
|
546
|
+
@pytest.fixture(scope="class")
|
|
547
|
+
def enum_api(self):
|
|
548
|
+
import sqlalchemy
|
|
549
|
+
from fastapi_rtk.backends.sqla.model import Model
|
|
550
|
+
|
|
551
|
+
class _TierStatus(enum.Enum):
|
|
552
|
+
ACTIVE = "active"
|
|
553
|
+
INACTIVE = "inactive"
|
|
554
|
+
|
|
555
|
+
class _TierEnumModel(Model):
|
|
556
|
+
__tablename__ = "tier12_enum_model"
|
|
557
|
+
__table_args__ = {"extend_existing": True}
|
|
558
|
+
id = sqlalchemy.Column(sqlalchemy.Integer, primary_key=True)
|
|
559
|
+
status = sqlalchemy.Column(sqlalchemy.Enum(_TierStatus), nullable=True)
|
|
560
|
+
status_required = sqlalchemy.Column(
|
|
561
|
+
sqlalchemy.Enum(_TierStatus), nullable=False
|
|
562
|
+
)
|
|
563
|
+
|
|
564
|
+
class TierEnumApi(ModelRestApi):
|
|
565
|
+
datamodel = SQLAInterface(_TierEnumModel)
|
|
566
|
+
|
|
567
|
+
return TierEnumApi()
|
|
568
|
+
|
|
569
|
+
def test_nullable_enum_gets_null_type_and_member(self, enum_api):
|
|
570
|
+
schema = enum_api.add_obj_schema
|
|
571
|
+
result = enum_api._generate_jsonforms_schema(schema, [], False)
|
|
572
|
+
status = result["properties"]["status"]
|
|
573
|
+
assert status["type"] == ["string", "null"]
|
|
574
|
+
assert status["enum"][0] is None
|
|
575
|
+
assert set(status["enum"][1:]) == {"active", "inactive"}
|
|
576
|
+
|
|
577
|
+
def test_non_nullable_enum_has_no_null_member(self, enum_api):
|
|
578
|
+
schema = enum_api.add_obj_schema
|
|
579
|
+
result = enum_api._generate_jsonforms_schema(schema, [], False)
|
|
580
|
+
status_required = result["properties"]["status_required"]
|
|
581
|
+
assert None not in status_required["enum"]
|
|
582
|
+
|
|
583
|
+
def test_repeated_calls_do_not_grow_the_enum(self, enum_api):
|
|
584
|
+
"""Regression: ``get_enum_value`` hands back the SQLAlchemy column's own
|
|
585
|
+
``enums`` list, so mutating it in place would corrupt it for every later
|
|
586
|
+
request instead of just the current schema dict.
|
|
587
|
+
"""
|
|
588
|
+
schema = enum_api.add_obj_schema
|
|
589
|
+
enum_api._generate_jsonforms_schema(schema, [], False)
|
|
590
|
+
result = enum_api._generate_jsonforms_schema(schema, [], False)
|
|
591
|
+
status = result["properties"]["status"]
|
|
592
|
+
assert status["enum"].count(None) == 1
|
|
593
|
+
|
|
594
|
+
@pytest.fixture
|
|
595
|
+
def relation_api(self, WidgetWithCountry_cls):
|
|
596
|
+
class TierRelationApi(ModelRestApi):
|
|
597
|
+
datamodel = SQLAInterface(WidgetWithCountry_cls)
|
|
598
|
+
|
|
599
|
+
return TierRelationApi()
|
|
600
|
+
|
|
601
|
+
def test_nullable_relation_with_use_enum_gets_null_member(self, relation_api):
|
|
602
|
+
schema = relation_api.add_obj_schema
|
|
603
|
+
info = [
|
|
604
|
+
ColumnRelationInfo(name="country", values=[RelInfo(id=1, value="Germany")])
|
|
605
|
+
]
|
|
606
|
+
result = relation_api._generate_jsonforms_schema(schema, info, use_enum=True)
|
|
607
|
+
assert result["properties"]["country"]["enum"][0] is None
|
|
608
|
+
|
|
609
|
+
def test_nullable_relation_without_use_enum_gets_null_oneof_member(
|
|
610
|
+
self, relation_api
|
|
611
|
+
):
|
|
612
|
+
schema = relation_api.add_obj_schema
|
|
613
|
+
info = [
|
|
614
|
+
ColumnRelationInfo(name="country", values=[RelInfo(id=1, value="Germany")])
|
|
615
|
+
]
|
|
616
|
+
result = relation_api._generate_jsonforms_schema(schema, info, False)
|
|
617
|
+
assert result["properties"]["country"]["oneOf"][0] == {
|
|
618
|
+
"const": None,
|
|
619
|
+
"title": "",
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
@pytest.fixture(scope="class")
|
|
623
|
+
def m2m_relation_api(self):
|
|
624
|
+
import sqlalchemy
|
|
625
|
+
import sqlalchemy.orm
|
|
626
|
+
from fastapi_rtk.backends.sqla.model import Model
|
|
627
|
+
|
|
628
|
+
assoc = sqlalchemy.Table(
|
|
629
|
+
"tier12_nullable_m2m_assoc",
|
|
630
|
+
Model.metadata,
|
|
631
|
+
sqlalchemy.Column(
|
|
632
|
+
"widget_id",
|
|
633
|
+
sqlalchemy.ForeignKey("tier12_nullable_m2m_widget.id"),
|
|
634
|
+
primary_key=True,
|
|
635
|
+
),
|
|
636
|
+
sqlalchemy.Column(
|
|
637
|
+
"tag_id",
|
|
638
|
+
sqlalchemy.ForeignKey("tier12_nullable_m2m_tag.id"),
|
|
639
|
+
primary_key=True,
|
|
640
|
+
),
|
|
641
|
+
extend_existing=True,
|
|
642
|
+
)
|
|
643
|
+
|
|
644
|
+
class _NullableM2MTag(Model):
|
|
645
|
+
__tablename__ = "tier12_nullable_m2m_tag"
|
|
646
|
+
__table_args__ = {"extend_existing": True}
|
|
647
|
+
id = sqlalchemy.Column(sqlalchemy.Integer, primary_key=True)
|
|
648
|
+
name = sqlalchemy.Column(sqlalchemy.String(50))
|
|
649
|
+
|
|
650
|
+
class _NullableM2MWidget(Model):
|
|
651
|
+
__tablename__ = "tier12_nullable_m2m_widget"
|
|
652
|
+
__table_args__ = {"extend_existing": True}
|
|
653
|
+
id = sqlalchemy.Column(sqlalchemy.Integer, primary_key=True)
|
|
654
|
+
name = sqlalchemy.Column(sqlalchemy.String(50))
|
|
655
|
+
tags = sqlalchemy.orm.relationship(_NullableM2MTag, secondary=assoc)
|
|
656
|
+
|
|
657
|
+
class TierNullableM2MApi(ModelRestApi):
|
|
658
|
+
datamodel = SQLAInterface(_NullableM2MWidget)
|
|
659
|
+
|
|
660
|
+
return TierNullableM2MApi()
|
|
661
|
+
|
|
662
|
+
def test_nullable_to_many_relation_items_have_no_null_member(
|
|
663
|
+
self, m2m_relation_api
|
|
664
|
+
):
|
|
665
|
+
schema = m2m_relation_api.add_obj_schema
|
|
666
|
+
info = [ColumnRelationInfo(name="tags", values=[RelInfo(id=1, value="a")])]
|
|
667
|
+
result = m2m_relation_api._generate_jsonforms_schema(schema, info, False)
|
|
668
|
+
tags = result["properties"]["tags"]
|
|
669
|
+
assert tags["type"] == ["array", "null"]
|
|
670
|
+
assert None not in [item.get("const") for item in tags["items"]["oneOf"]]
|
|
671
|
+
|
|
672
|
+
|
|
673
|
+
class TestGenerateJsonformsSchemaMarkNonNullableRequired:
|
|
674
|
+
"""``mark_non_nullable_required`` lets the EDIT schema report a missing
|
|
675
|
+
non-nullable value itself, instead of the frontend inventing that error.
|
|
676
|
+
"""
|
|
677
|
+
|
|
678
|
+
@pytest.fixture(scope="class")
|
|
679
|
+
def enum_api(self):
|
|
680
|
+
import sqlalchemy
|
|
681
|
+
from fastapi_rtk.backends.sqla.model import Model
|
|
682
|
+
|
|
683
|
+
class _TierRequiredStatus(enum.Enum):
|
|
684
|
+
ACTIVE = "active"
|
|
685
|
+
INACTIVE = "inactive"
|
|
686
|
+
|
|
687
|
+
class _TierRequiredModel(Model):
|
|
688
|
+
__tablename__ = "tier12_required_model"
|
|
689
|
+
__table_args__ = {"extend_existing": True}
|
|
690
|
+
id = sqlalchemy.Column(sqlalchemy.Integer, primary_key=True)
|
|
691
|
+
status = sqlalchemy.Column(
|
|
692
|
+
sqlalchemy.Enum(_TierRequiredStatus), nullable=True
|
|
693
|
+
)
|
|
694
|
+
status_required = sqlalchemy.Column(
|
|
695
|
+
sqlalchemy.Enum(_TierRequiredStatus), nullable=False
|
|
696
|
+
)
|
|
697
|
+
|
|
698
|
+
class TierRequiredApi(ModelRestApi):
|
|
699
|
+
datamodel = SQLAInterface(_TierRequiredModel)
|
|
700
|
+
|
|
701
|
+
return TierRequiredApi()
|
|
702
|
+
|
|
703
|
+
@pytest.fixture
|
|
704
|
+
def excluded_required_api(self, Widget_cls):
|
|
705
|
+
class TierExcludedRequiredApi(ModelRestApi):
|
|
706
|
+
datamodel = SQLAInterface(Widget_cls)
|
|
707
|
+
edit_columns = ["description", "quantity"]
|
|
708
|
+
|
|
709
|
+
return TierExcludedRequiredApi()
|
|
710
|
+
|
|
711
|
+
def test_default_leaves_required_as_pydantic_emitted(self, enum_api):
|
|
712
|
+
schema = enum_api.edit_obj_schema
|
|
713
|
+
result = enum_api._generate_jsonforms_schema(schema, [])
|
|
714
|
+
assert result.get("required", []) == schema.model_json_schema().get(
|
|
715
|
+
"required", []
|
|
716
|
+
)
|
|
717
|
+
|
|
718
|
+
def test_flag_marks_every_non_nullable_column_present_as_required(self, enum_api):
|
|
719
|
+
schema = enum_api.edit_obj_schema
|
|
720
|
+
result = enum_api._generate_jsonforms_schema(
|
|
721
|
+
schema, [], mark_non_nullable_required=True
|
|
722
|
+
)
|
|
723
|
+
assert "status_required" in result["required"]
|
|
724
|
+
assert "status" not in result["required"]
|
|
725
|
+
|
|
726
|
+
def test_flag_does_not_duplicate_an_already_required_column(self, enum_api):
|
|
727
|
+
schema = enum_api.add_obj_schema
|
|
728
|
+
result = enum_api._generate_jsonforms_schema(
|
|
729
|
+
schema, [], mark_non_nullable_required=True
|
|
730
|
+
)
|
|
731
|
+
assert result["required"].count("status_required") == 1
|
|
732
|
+
|
|
733
|
+
def test_flag_skips_non_nullable_column_absent_from_properties(
|
|
734
|
+
self, excluded_required_api
|
|
735
|
+
):
|
|
736
|
+
schema = excluded_required_api.edit_obj_schema
|
|
737
|
+
result = excluded_required_api._generate_jsonforms_schema(
|
|
738
|
+
schema, [], mark_non_nullable_required=True
|
|
739
|
+
)
|
|
740
|
+
assert "name" not in result["properties"]
|
|
741
|
+
assert "name" not in result["required"]
|
|
742
|
+
|
|
743
|
+
def test_repeated_calls_with_flag_yield_identical_required(self, enum_api):
|
|
744
|
+
schema = enum_api.edit_obj_schema
|
|
745
|
+
first = enum_api._generate_jsonforms_schema(
|
|
746
|
+
schema, [], mark_non_nullable_required=True
|
|
747
|
+
)
|
|
748
|
+
second = enum_api._generate_jsonforms_schema(
|
|
749
|
+
schema, [], mark_non_nullable_required=True
|
|
750
|
+
)
|
|
751
|
+
assert first["required"] == second["required"]
|
|
752
|
+
|
|
753
|
+
def test_flag_skips_a_non_nullable_password_column(self, enum_api):
|
|
754
|
+
from fastapi_rtk.globals import g
|
|
755
|
+
|
|
756
|
+
model_name = enum_api.datamodel.obj.__name__
|
|
757
|
+
original = g.sensitive_data
|
|
758
|
+
g.sensitive_data = {**original, model_name: ["status_required"]}
|
|
759
|
+
try:
|
|
760
|
+
schema = enum_api.edit_obj_schema
|
|
761
|
+
result = enum_api._generate_jsonforms_schema(
|
|
762
|
+
schema, [], mark_non_nullable_required=True
|
|
763
|
+
)
|
|
764
|
+
assert result["properties"]["status_required"]["format"] == "password"
|
|
765
|
+
assert "status_required" not in result["required"]
|
|
766
|
+
finally:
|
|
767
|
+
g.sensitive_data = original
|
|
768
|
+
|
|
769
|
+
def test_flag_skips_a_non_nullable_readonly_relation(self, Country_cls):
|
|
770
|
+
import sqlalchemy
|
|
771
|
+
import sqlalchemy.orm
|
|
772
|
+
from fastapi_rtk.backends.sqla.model import Model
|
|
773
|
+
|
|
774
|
+
class TierWidgetRequiredCountry(Model):
|
|
775
|
+
__tablename__ = "tier12_widget_required_country"
|
|
776
|
+
__table_args__ = {"extend_existing": True}
|
|
777
|
+
id = sqlalchemy.Column(sqlalchemy.Integer, primary_key=True)
|
|
778
|
+
name = sqlalchemy.Column(sqlalchemy.String(50))
|
|
779
|
+
country_id = sqlalchemy.Column(
|
|
780
|
+
sqlalchemy.Integer,
|
|
781
|
+
sqlalchemy.ForeignKey("tier12_country.id"),
|
|
782
|
+
nullable=False,
|
|
783
|
+
)
|
|
784
|
+
country = sqlalchemy.orm.relationship(Country_cls)
|
|
785
|
+
|
|
786
|
+
class TierRequiredRelationApi(ModelRestApi):
|
|
787
|
+
datamodel = SQLAInterface(TierWidgetRequiredCountry)
|
|
788
|
+
edit_columns = ["name", "country"]
|
|
789
|
+
|
|
790
|
+
api = TierRequiredRelationApi()
|
|
791
|
+
schema = api.edit_obj_schema
|
|
792
|
+
info = [ColumnRelationInfo(name="country", values=[])]
|
|
793
|
+
result = api._generate_jsonforms_schema(
|
|
794
|
+
schema, info, mark_non_nullable_required=True
|
|
795
|
+
)
|
|
796
|
+
assert result["properties"]["country"]["readOnly"] is True
|
|
797
|
+
assert "country" not in result["required"]
|