fastapi-rtk 2.1.5__tar.gz → 2.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/.gitignore +3 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/PKG-INFO +1 -1
- fastapi_rtk-2.2.0/fastapi_rtk/_version.py +1 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/backends/sqla/db.py +11 -10
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/bases/interface.py +16 -1
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/fastapi_react_toolkit.py +4 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/globals.py +0 -6
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/utils/run_utils.py +27 -9
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/tests/unit/backends/sqla/test_db.py +73 -7
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/tests/unit/bases/test_interface.py +105 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/tests/unit/test_fastapi_react_toolkit.py +23 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/tests/unit/test_globals.py +0 -18
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/tests/unit/utils/test_run_utils.py +51 -0
- fastapi_rtk-2.1.5/fastapi_rtk/_version.py +0 -1
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/LICENSE +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/README.md +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/__init__.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/api/__init__.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/api/base_api.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/api/model_rest_api/__init__.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/api/model_rest_api/_add_mixin.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/api/model_rest_api/_bulk_mixin.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/api/model_rest_api/_columns.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/api/model_rest_api/_delete_mixin.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/api/model_rest_api/_download_mixin.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/api/model_rest_api/_edit_mixin.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/api/model_rest_api/_file_mixin.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/api/model_rest_api/_info_mixin.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/api/model_rest_api/_list_mixin.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/api/model_rest_api/_params.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/api/model_rest_api/_show_mixin.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/api/model_rest_api/_upload_mixin.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/apis/__init__.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/apis/info.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/apis/license.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/auth/__init__.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/auth/auth.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/auth/hashers/__init__.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/auth/hashers/pbkdf2.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/auth/hashers/scrypt.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/auth/ldap.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/auth/password_helpers/__init__.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/auth/password_helpers/fab.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/auth/strategies/__init__.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/auth/strategies/config.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/auth/strategies/db.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/auth/strategies/jwt.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/backends/__init__.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/backends/generic/__init__.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/backends/generic/column.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/backends/generic/db.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/backends/generic/exceptions.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/backends/generic/filters.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/backends/generic/interface.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/backends/generic/model.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/backends/generic/session.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/backends/sqla/__init__.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/backends/sqla/column.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/backends/sqla/exceptions.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/backends/sqla/extensions/__init__.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/backends/sqla/extensions/audit/__init__.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/backends/sqla/extensions/audit/_decorators_mixin.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/backends/sqla/extensions/audit/_lifecycle_mixin.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/backends/sqla/extensions/audit/_logger.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/backends/sqla/extensions/audit/_processing_mixin.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/backends/sqla/extensions/audit/audit.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/backends/sqla/extensions/audit/types.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/backends/sqla/extensions/geoalchemy2/__init__.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/backends/sqla/extensions/geoalchemy2/filters.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/backends/sqla/extensions/geoalchemy2/geometry_converter.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/backends/sqla/filters.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/backends/sqla/interface.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/backends/sqla/model.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/backends/sqla/session.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/bases/__init__.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/bases/db.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/bases/file_manager.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/bases/filter.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/bases/model.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/bases/session.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/cli/__init__.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/cli/cli.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/cli/commands/__init__.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/cli/commands/_security_crypto.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/cli/commands/db/__init__.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/cli/commands/db/templates/fastapi/README +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/cli/commands/db/templates/fastapi/alembic.ini.mako +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/cli/commands/db/templates/fastapi/env.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/cli/commands/db/templates/fastapi/script.py.mako +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/cli/commands/db/templates/fastapi-multidb/README +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/cli/commands/db/templates/fastapi-multidb/alembic.ini.mako +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/cli/commands/db/templates/fastapi-multidb/env.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/cli/commands/db/templates/fastapi-multidb/script.py.mako +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/cli/commands/export.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/cli/commands/security.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/cli/commands/translate.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/cli/const.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/cli/decorators.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/cli/types.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/cli/utils.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/config.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/const.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/db.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/decorators.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/dependencies.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/exceptions.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/file_managers/__init__.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/file_managers/file_manager.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/file_managers/image_manager.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/file_managers/s3_file_manager.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/file_managers/s3_image_manager.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/filters.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/lang/__init__.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/lang/babel/__init__.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/lang/babel/cli.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/lang/babel/config.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/lang/babel.cfg +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/lang/lazy_text.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/lang/messages.pot +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/lang/translations/de/LC_MESSAGES/messages.mo +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/lang/translations/de/LC_MESSAGES/messages.po +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/lang/translations/en/LC_MESSAGES/messages.mo +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/lang/translations/en/LC_MESSAGES/messages.po +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/manager.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/middlewares.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/mixins.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/models.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/py.typed +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/registry.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/routers.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/schemas.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/security/__init__.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/security/sqla/__init__.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/security/sqla/apis.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/security/sqla/models.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/security/sqla/security_manager/__init__.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/security/sqla/security_manager/_association_mixin.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/security/sqla/security_manager/_builtin_role_mixin.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/security/sqla/security_manager/_cleanup_mixin.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/security/sqla/security_manager/_exception_filters.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/security/sqla/security_manager/_export.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/security/sqla/security_manager/_import.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/security/sqla/security_manager/_role_mixin.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/security/sqla/security_manager/_user_mixin.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/setting.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/types.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/utils/__init__.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/utils/async_task_runner.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/utils/class_factory.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/utils/csv_json_converter.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/utils/deep_merge.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/utils/extender_mixin.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/utils/flask_appbuilder_utils.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/utils/formatter.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/utils/hooks.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/utils/lazy.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/utils/merge_schema.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/utils/multiple_async_contexts.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/utils/pydantic.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/utils/self_dependencies.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/utils/smartdefaultdict.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/utils/sqla.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/utils/timezone.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/utils/update_signature.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/utils/use_default_when_none.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/fastapi_rtk/version.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/pyproject.toml +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/tests/conftest.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/tests/docker-compose.gis.yml +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/tests/integration/__init__.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/tests/integration/api/__init__.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/tests/integration/api/test_model_rest_api.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/tests/integration/apis/__init__.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/tests/integration/apis/test_info_contract.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/tests/integration/auth/__init__.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/tests/integration/auth/test_auth_flow.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/tests/integration/auth/test_oauth.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/tests/integration/conftest.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/tests/integration/file_managers/__init__.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/tests/integration/file_managers/test_file_routes.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/tests/integration/security/__init__.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/tests/integration/security/sqla/__init__.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/tests/integration/security/sqla/test_security_manager.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/tests/integration/test_app_smoke.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/tests/unit/__init__.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/tests/unit/api/__init__.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/tests/unit/api/conftest.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/tests/unit/api/model_rest_api/__init__.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/tests/unit/api/model_rest_api/test_columns.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/tests/unit/api/model_rest_api/test_file_mixin.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/tests/unit/api/model_rest_api/test_init.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/tests/unit/api/test_base_api.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/tests/unit/apis/__init__.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/tests/unit/apis/test_info.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/tests/unit/apis/test_license.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/tests/unit/auth/__init__.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/tests/unit/auth/hashers/__init__.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/tests/unit/auth/hashers/test_pbkdf2.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/tests/unit/auth/hashers/test_scrypt.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/tests/unit/auth/password_helpers/__init__.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/tests/unit/auth/password_helpers/test_fab.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/tests/unit/auth/strategies/__init__.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/tests/unit/auth/strategies/test_config.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/tests/unit/auth/strategies/test_db.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/tests/unit/auth/strategies/test_jwt.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/tests/unit/auth/test_auth.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/tests/unit/backends/__init__.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/tests/unit/backends/generic/__init__.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/tests/unit/backends/generic/test_column.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/tests/unit/backends/generic/test_db.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/tests/unit/backends/generic/test_exceptions.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/tests/unit/backends/generic/test_filters.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/tests/unit/backends/generic/test_interface.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/tests/unit/backends/generic/test_model.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/tests/unit/backends/generic/test_session.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/tests/unit/backends/sqla/__init__.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/tests/unit/backends/sqla/conftest.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/tests/unit/backends/sqla/test_audit.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/tests/unit/backends/sqla/test_audit_types.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/tests/unit/backends/sqla/test_column.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/tests/unit/backends/sqla/test_filters.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/tests/unit/backends/sqla/test_geoalchemy2.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/tests/unit/backends/sqla/test_interface.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/tests/unit/backends/sqla/test_model.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/tests/unit/backends/sqla/test_session.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/tests/unit/bases/__init__.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/tests/unit/bases/test_db.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/tests/unit/bases/test_file_manager.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/tests/unit/bases/test_filter.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/tests/unit/bases/test_model.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/tests/unit/bases/test_session.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/tests/unit/cli/__init__.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/tests/unit/cli/commands/__init__.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/tests/unit/cli/commands/conftest.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/tests/unit/cli/commands/test_db.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/tests/unit/cli/commands/test_export.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/tests/unit/cli/commands/test_security.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/tests/unit/cli/commands/test_security_crypto.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/tests/unit/cli/commands/test_translate.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/tests/unit/cli/conftest.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/tests/unit/cli/test_cli.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/tests/unit/cli/test_decorators.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/tests/unit/cli/test_types.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/tests/unit/cli/test_utils.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/tests/unit/file_managers/__init__.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/tests/unit/file_managers/conftest.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/tests/unit/file_managers/test_file_manager.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/tests/unit/file_managers/test_image_manager.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/tests/unit/file_managers/test_s3_file_manager.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/tests/unit/file_managers/test_s3_image_manager.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/tests/unit/lang/__init__.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/tests/unit/lang/test_babel_cli.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/tests/unit/lang/test_babel_config.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/tests/unit/lang/test_lazy_text.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/tests/unit/security/__init__.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/tests/unit/security/sqla/__init__.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/tests/unit/security/sqla/conftest.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/tests/unit/security/sqla/test_apis.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/tests/unit/security/sqla/test_models.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/tests/unit/security/sqla/test_security_manager.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/tests/unit/test_config.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/tests/unit/test_const.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/tests/unit/test_db.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/tests/unit/test_decorators.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/tests/unit/test_dependencies.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/tests/unit/test_exceptions.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/tests/unit/test_filters.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/tests/unit/test_manager.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/tests/unit/test_middlewares.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/tests/unit/test_mixins.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/tests/unit/test_models.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/tests/unit/test_registry.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/tests/unit/test_routers.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/tests/unit/test_schemas.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/tests/unit/test_setting.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/tests/unit/test_types.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/tests/unit/test_version.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/tests/unit/utils/__init__.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/tests/unit/utils/test_async_task_runner.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/tests/unit/utils/test_class_factory.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/tests/unit/utils/test_csv_json_converter.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/tests/unit/utils/test_deep_merge.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/tests/unit/utils/test_extender_mixin.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/tests/unit/utils/test_flask_appbuilder_utils.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/tests/unit/utils/test_formatter.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/tests/unit/utils/test_hooks.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/tests/unit/utils/test_lazy.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/tests/unit/utils/test_merge_schema.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/tests/unit/utils/test_multiple_async_contexts.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/tests/unit/utils/test_pydantic.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/tests/unit/utils/test_self_dependencies.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/tests/unit/utils/test_smartdefaultdict.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/tests/unit/utils/test_sqla.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/tests/unit/utils/test_timezone.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/tests/unit/utils/test_update_signature.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/tests/unit/utils/test_use_default_when_none.py +0 -0
- {fastapi_rtk-2.1.5 → fastapi_rtk-2.2.0}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fastapi-rtk
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.2.0
|
|
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.2.0"
|
|
@@ -31,6 +31,9 @@ LOAD_TYPE_MAPPING = {
|
|
|
31
31
|
"all": 2,
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
# Cap for _load_options_cache; keys include a client-reachable column subset, so an unbounded cache is a memory-growth vector.
|
|
35
|
+
_LOAD_OPTIONS_CACHE_MAXSIZE = 1024
|
|
36
|
+
|
|
34
37
|
|
|
35
38
|
class LoadColumn(typing.TypedDict):
|
|
36
39
|
statement: Select[tuple[T]] | _AbstractLoad
|
|
@@ -68,7 +71,9 @@ class SQLAQueryBuilder(AbstractQueryBuilder[Select[tuple[T]]]):
|
|
|
68
71
|
```
|
|
69
72
|
"""
|
|
70
73
|
|
|
71
|
-
_load_options_cache:
|
|
74
|
+
_load_options_cache: collections.OrderedDict[
|
|
75
|
+
str, list[list[_AbstractLoad] | _AbstractLoad]
|
|
76
|
+
] = collections.OrderedDict()
|
|
72
77
|
|
|
73
78
|
def __init__(self, datamodel, statement=None):
|
|
74
79
|
if self.statement is None:
|
|
@@ -97,6 +102,7 @@ class SQLAQueryBuilder(AbstractQueryBuilder[Select[tuple[T]]]):
|
|
|
97
102
|
# Check if the columns have been loaded before
|
|
98
103
|
cache_key = f"{self.datamodel.obj.__name__}-{list_columns}"
|
|
99
104
|
if cache_key in self._load_options_cache:
|
|
105
|
+
self._load_options_cache.move_to_end(cache_key)
|
|
100
106
|
logger.debug(f"Loading columns from cache: {cache_key}")
|
|
101
107
|
for cache_option in self._load_options_cache[cache_key]:
|
|
102
108
|
if isinstance(cache_option, list):
|
|
@@ -113,15 +119,8 @@ class SQLAQueryBuilder(AbstractQueryBuilder[Select[tuple[T]]]):
|
|
|
113
119
|
return statement.offset(page * page_size).limit(page_size)
|
|
114
120
|
|
|
115
121
|
def apply_order_by(self, statement, order_column, order_direction):
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
#! If the order column comes from a request, it will be in the format ClassName.column_name
|
|
119
|
-
if col.startswith(
|
|
120
|
-
self.datamodel.obj.__class__.__name__
|
|
121
|
-
): # pragma: no cover - obj.__class__.__name__ is metaclass name (DeclarativeMeta), almost never matches
|
|
122
|
-
col = col.split(".", 1)[1]
|
|
123
|
-
|
|
124
|
-
statement, col = self._retrieve_column(statement, col)
|
|
122
|
+
# A request may send the order column as ``ClassName.column``; _retrieve_column resolves it by landing on the final path segment.
|
|
123
|
+
statement, col = self._retrieve_column(statement, order_column)
|
|
125
124
|
if order_direction == "asc":
|
|
126
125
|
statement = statement.order_by(col)
|
|
127
126
|
else:
|
|
@@ -336,6 +335,8 @@ class SQLAQueryBuilder(AbstractQueryBuilder[Select[tuple[T]]]):
|
|
|
336
335
|
if columns:
|
|
337
336
|
cache_key = f"{self.datamodel.obj.__name__}-{columns}"
|
|
338
337
|
self._load_options_cache[cache_key] = []
|
|
338
|
+
if len(self._load_options_cache) > _LOAD_OPTIONS_CACHE_MAXSIZE:
|
|
339
|
+
self._load_options_cache.popitem(last=False)
|
|
339
340
|
if load_column["type"] == "defer":
|
|
340
341
|
defers = [
|
|
341
342
|
defer(getattr(self.datamodel.obj, col))
|
|
@@ -207,6 +207,12 @@ class AbstractInterface(abc.ABC, typing.Generic[T, C, CT]):
|
|
|
207
207
|
logger = lazy(
|
|
208
208
|
lambda self: logger.getChild(f"{self.__class__.__name__}[{self.obj.__name__}]")
|
|
209
209
|
)
|
|
210
|
+
_related_interfaces = lazy(
|
|
211
|
+
lambda: dict[tuple[typing.Any, bool | None], "AbstractInterface"]()
|
|
212
|
+
)
|
|
213
|
+
"""
|
|
214
|
+
Cache of related interfaces keyed by `(related model, with_fk)`, so repeated lookups reuse the lazily computed attributes.
|
|
215
|
+
"""
|
|
210
216
|
|
|
211
217
|
_cache_schema: dict[str, typing.Type[pydantic.BaseModel]] = {}
|
|
212
218
|
|
|
@@ -867,6 +873,8 @@ class AbstractInterface(abc.ABC, typing.Generic[T, C, CT]):
|
|
|
867
873
|
"""
|
|
868
874
|
Gets the related interface for a given column name.
|
|
869
875
|
|
|
876
|
+
Interfaces are cached per `(related model, with_fk)`, so repeated lookups reuse the same instance and its lazily computed attributes.
|
|
877
|
+
|
|
870
878
|
Args:
|
|
871
879
|
col_name (str): The name of the column for which to get the related interface.
|
|
872
880
|
with_fk (bool | None, optional): Whether to include foreign keys in the related interface. Defaults to None.
|
|
@@ -874,7 +882,12 @@ class AbstractInterface(abc.ABC, typing.Generic[T, C, CT]):
|
|
|
874
882
|
Returns:
|
|
875
883
|
typing.Self: The related interface for the specified column name.
|
|
876
884
|
"""
|
|
877
|
-
|
|
885
|
+
key = (self.get_related_model(col_name), with_fk)
|
|
886
|
+
interface = self._related_interfaces.get(key)
|
|
887
|
+
if interface is None:
|
|
888
|
+
interface = type(self)(key[0], with_fk)
|
|
889
|
+
self._related_interfaces[key] = interface
|
|
890
|
+
return interface
|
|
878
891
|
|
|
879
892
|
"""
|
|
880
893
|
--------------------------------------------------------------------------------------------------------
|
|
@@ -1084,6 +1097,8 @@ class AbstractInterface(abc.ABC, typing.Generic[T, C, CT]):
|
|
|
1084
1097
|
if value is None or isinstance(value, dict):
|
|
1085
1098
|
return value
|
|
1086
1099
|
for field_name in cls.model_fields:
|
|
1100
|
+
if field_name in async_columns:
|
|
1101
|
+
continue
|
|
1087
1102
|
try:
|
|
1088
1103
|
getattr(value, field_name)
|
|
1089
1104
|
except sqlalchemy.exc.MissingGreenlet as e:
|
|
@@ -642,6 +642,10 @@ class FastAPIReactToolkit:
|
|
|
642
642
|
# Add the endpoint for the metrics
|
|
643
643
|
self.instrumentator.expose(app, **Setting.INSTRUMENTATOR_EXPOSE_CONFIG)
|
|
644
644
|
|
|
645
|
+
# The engine is created once in initialize(); a prior lifespan shutdown disposes it, so re-init on re-entry.
|
|
646
|
+
if db._engine is None:
|
|
647
|
+
self.connect_to_database()
|
|
648
|
+
|
|
645
649
|
await db.init_fastapi_rtk_tables()
|
|
646
650
|
|
|
647
651
|
if self.upgrade_db:
|
|
@@ -88,10 +88,6 @@ class Globals:
|
|
|
88
88
|
"""
|
|
89
89
|
A boolean value to indicate if the application is loaded by the CLI. `True` when you are running a command from `fastapi-rtk` CLI.
|
|
90
90
|
"""
|
|
91
|
-
is_migrate: bool
|
|
92
|
-
"""
|
|
93
|
-
Deprecated alias of `is_cli`. TODO(next-minor): remove.
|
|
94
|
-
"""
|
|
95
91
|
sensitive_data: dict[str, list[str]]
|
|
96
92
|
"""
|
|
97
93
|
A dictionary used to store list of sensitive columns for each model that should not be returned in the list and get endpoints. Default is `{"User": ["password", "hashed_password"]}`.
|
|
@@ -286,8 +282,6 @@ g.set_default(
|
|
|
286
282
|
)
|
|
287
283
|
g.set_default("config", Config())
|
|
288
284
|
g.set_default("is_cli", False)
|
|
289
|
-
# TODO(next-minor): remove. Deprecated alias; resolves to `is_cli`.
|
|
290
|
-
g.set_default("is_migrate", lazy(lambda: g.is_cli))
|
|
291
285
|
g.set_default("sensitive_data", {"User": ["password", "hashed_password"]})
|
|
292
286
|
g.set_default(
|
|
293
287
|
"file_manager",
|
|
@@ -45,23 +45,27 @@ async def smart_run(
|
|
|
45
45
|
|
|
46
46
|
|
|
47
47
|
def smart_run_sync(
|
|
48
|
-
func: typing.Callable[
|
|
49
|
-
*args:
|
|
50
|
-
|
|
48
|
+
func: typing.Callable[..., typing.Union[T, typing.Awaitable[T]]],
|
|
49
|
+
*args: typing.Any,
|
|
50
|
+
loop: asyncio.AbstractEventLoop | None = None,
|
|
51
|
+
**kwargs: typing.Any,
|
|
51
52
|
) -> T:
|
|
52
53
|
"""
|
|
53
54
|
Run a function synchronously, or run an async function in a thread pool so it can be called from a synchronous context.
|
|
54
55
|
|
|
56
|
+
``loop`` is reserved by this helper and is not forwarded to ``func``.
|
|
57
|
+
|
|
55
58
|
Args:
|
|
56
|
-
func (typing.Callable[
|
|
57
|
-
*args
|
|
58
|
-
|
|
59
|
+
func (typing.Callable[..., typing.Union[T, typing.Awaitable[T]]]): The function to be executed.
|
|
60
|
+
*args: Positional arguments to be passed to the function.
|
|
61
|
+
loop (asyncio.AbstractEventLoop | None, optional): A running event loop to submit an async ``func`` to. Defaults to None, which runs it on a fresh loop in a thread pool.
|
|
62
|
+
**kwargs: Keyword arguments to be passed to the function.
|
|
59
63
|
|
|
60
64
|
Returns:
|
|
61
65
|
T: The result of the function execution.
|
|
62
66
|
"""
|
|
63
67
|
if inspect.iscoroutinefunction(func):
|
|
64
|
-
return run_coroutine_in_threadpool(func(*args, **kwargs))
|
|
68
|
+
return run_coroutine_in_threadpool(func(*args, **kwargs), loop=loop)
|
|
65
69
|
return func(*args, **kwargs)
|
|
66
70
|
|
|
67
71
|
|
|
@@ -83,6 +87,7 @@ async def safe_call(coro: typing.Coroutine[typing.Any, typing.Any, T] | T) -> T:
|
|
|
83
87
|
def safe_call_sync(
|
|
84
88
|
result_or_coro: typing.Union[T, typing.Coroutine[typing.Any, typing.Any, T]],
|
|
85
89
|
max_workers: int = 1,
|
|
90
|
+
loop: asyncio.AbstractEventLoop | None = None,
|
|
86
91
|
**kwargs: typing.Any,
|
|
87
92
|
) -> T:
|
|
88
93
|
"""
|
|
@@ -91,6 +96,7 @@ def safe_call_sync(
|
|
|
91
96
|
Args:
|
|
92
97
|
result_or_coro (typing.Union[T, typing.Coroutine[typing.Any, typing.Any, T]]): The result or coroutine to be executed.
|
|
93
98
|
max_workers (int, optional): The maximum number of workers in the thread pool. Defaults to 1.
|
|
99
|
+
loop (asyncio.AbstractEventLoop | None, optional): A running event loop to submit the coroutine to. Defaults to None, which runs the coroutine on a fresh loop in a thread pool.
|
|
94
100
|
**kwargs: Additional keyword arguments to pass to the `ThreadPoolExecutor`.
|
|
95
101
|
|
|
96
102
|
Returns:
|
|
@@ -98,25 +104,37 @@ def safe_call_sync(
|
|
|
98
104
|
"""
|
|
99
105
|
if isinstance(result_or_coro, typing.Coroutine):
|
|
100
106
|
return run_coroutine_in_threadpool(
|
|
101
|
-
result_or_coro, max_workers=max_workers, **kwargs
|
|
107
|
+
result_or_coro, max_workers=max_workers, loop=loop, **kwargs
|
|
102
108
|
)
|
|
103
109
|
return result_or_coro
|
|
104
110
|
|
|
105
111
|
|
|
106
112
|
def run_coroutine_in_threadpool(
|
|
107
|
-
coro: typing.Coroutine[typing.Any, typing.Any, T],
|
|
113
|
+
coro: typing.Coroutine[typing.Any, typing.Any, T],
|
|
114
|
+
max_workers=1,
|
|
115
|
+
loop: asyncio.AbstractEventLoop | None = None,
|
|
116
|
+
**kwargs,
|
|
108
117
|
):
|
|
109
118
|
"""
|
|
110
119
|
Run a coroutine in a thread pool executor.
|
|
111
120
|
|
|
121
|
+
When ``loop`` is given, the coroutine is submitted to that already-running
|
|
122
|
+
loop via ``asyncio.run_coroutine_threadsafe`` instead of a throwaway loop.
|
|
123
|
+
This keeps loop-bound resources (asyncpg pools, redis asyncio clients,
|
|
124
|
+
``asyncio.Lock``) valid across calls; ``max_workers`` and ``**kwargs`` are
|
|
125
|
+
then unused. The caller owns the loop and must run it in another thread.
|
|
126
|
+
|
|
112
127
|
Args:
|
|
113
128
|
coro (typing.Coroutine[typing.Any, typing.Any, T]): The coroutine to run.
|
|
114
129
|
max_workers (int, optional): The maximum number of workers in the thread pool. Defaults to 1.
|
|
130
|
+
loop (asyncio.AbstractEventLoop | None, optional): A running event loop to submit the coroutine to. Defaults to None, which runs the coroutine on a fresh loop in a thread pool.
|
|
115
131
|
**kwargs: Additional keyword arguments to pass to the `ThreadPoolExecutor`.
|
|
116
132
|
|
|
117
133
|
Returns:
|
|
118
134
|
T: The result of the coroutine.
|
|
119
135
|
"""
|
|
136
|
+
if loop is not None:
|
|
137
|
+
return asyncio.run_coroutine_threadsafe(coro, loop).result()
|
|
120
138
|
with ThreadPoolExecutor(max_workers=max_workers, **kwargs) as executor:
|
|
121
139
|
ctx = contextvars.copy_context()
|
|
122
140
|
future = executor.submit(ctx.run, asyncio.run, coro)
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import pytest
|
|
2
|
+
|
|
2
3
|
from fastapi_rtk.backends.sqla.db import (
|
|
3
4
|
SQLAQueryBuilder,
|
|
4
5
|
create_load_column,
|
|
@@ -30,12 +31,20 @@ class TestQueryBuilderBasic:
|
|
|
30
31
|
result = await iface.get_many(session, {"page": 0, "page_size": 2})
|
|
31
32
|
assert len(result) == 2
|
|
32
33
|
|
|
33
|
-
async def
|
|
34
|
+
async def test_order_by_class_prefixed_column(self, session, iface):
|
|
35
|
+
# A request may send the order column as ``ClassName.column``; it must resolve to the scalar column.
|
|
34
36
|
result = await iface.get_many(
|
|
35
37
|
session, {"order_column": "Person.age", "order_direction": "asc"}
|
|
36
38
|
)
|
|
37
39
|
assert [p.age for p in result] == [20, 30, 40]
|
|
38
40
|
|
|
41
|
+
async def test_order_by_class_prefixed_relation_column(self, session, iface):
|
|
42
|
+
# ``ClassName.relation.column`` must join the relation and order by its column.
|
|
43
|
+
result = await iface.get_many(
|
|
44
|
+
session, {"order_column": "Person.country.name", "order_direction": "asc"}
|
|
45
|
+
)
|
|
46
|
+
assert len(result) == 3
|
|
47
|
+
|
|
39
48
|
async def test_apply_where(self, session, iface):
|
|
40
49
|
result = await iface.get_many(session, {"where": ("name", "Alice")})
|
|
41
50
|
assert [p.name for p in result] == ["Alice"]
|
|
@@ -173,9 +182,10 @@ class TestRelationFilterDispatch:
|
|
|
173
182
|
"""Cover line 179 - apply_filter_class with `.` in col routes through _filter_relation."""
|
|
174
183
|
|
|
175
184
|
async def test_filter_class_with_dotted_col(self, session, iface):
|
|
176
|
-
from fastapi_rtk.backends.sqla.filters import FilterEqual
|
|
177
185
|
from sqlalchemy import select
|
|
178
186
|
|
|
187
|
+
from fastapi_rtk.backends.sqla.filters import FilterEqual
|
|
188
|
+
|
|
179
189
|
qb = SQLAQueryBuilder(iface)
|
|
180
190
|
stmt = select(iface.obj)
|
|
181
191
|
# _filter_relation expects a filter instance bound to the related interface
|
|
@@ -212,14 +222,15 @@ class TestApplyListColumnsCache:
|
|
|
212
222
|
|
|
213
223
|
async def test_apply_list_columns_cache_hit_via_repeated_call(self):
|
|
214
224
|
import sqlalchemy as _sa
|
|
215
|
-
from fastapi_rtk.backends.sqla.interface import SQLAInterface
|
|
216
|
-
from fastapi_rtk.backends.sqla.model import Model
|
|
217
225
|
from sqlalchemy.ext.asyncio import (
|
|
218
226
|
AsyncSession,
|
|
219
227
|
async_sessionmaker,
|
|
220
228
|
create_async_engine,
|
|
221
229
|
)
|
|
222
230
|
|
|
231
|
+
from fastapi_rtk.backends.sqla.interface import SQLAInterface
|
|
232
|
+
from fastapi_rtk.backends.sqla.model import Model
|
|
233
|
+
|
|
223
234
|
class CacheWidgetDb(Model):
|
|
224
235
|
__tablename__ = "cache_widget_db"
|
|
225
236
|
__table_args__ = {"extend_existing": True}
|
|
@@ -243,21 +254,76 @@ class TestApplyListColumnsCache:
|
|
|
243
254
|
await engine.dispose()
|
|
244
255
|
|
|
245
256
|
|
|
257
|
+
class TestLoadOptionsCacheEviction:
|
|
258
|
+
"""LRU bound on the class-level _load_options_cache (subset-enumeration guard)."""
|
|
259
|
+
|
|
260
|
+
@staticmethod
|
|
261
|
+
def _key(model, cols):
|
|
262
|
+
return f"{model.__name__}-{sorted(cols)}"
|
|
263
|
+
|
|
264
|
+
def test_lru_evicts_oldest_over_maxsize(self, monkeypatch, iface):
|
|
265
|
+
import collections
|
|
266
|
+
|
|
267
|
+
import sqlalchemy as _sa
|
|
268
|
+
|
|
269
|
+
from fastapi_rtk.backends.sqla import db as _db
|
|
270
|
+
|
|
271
|
+
monkeypatch.setattr(_db, "_LOAD_OPTIONS_CACHE_MAXSIZE", 2)
|
|
272
|
+
monkeypatch.setattr(
|
|
273
|
+
SQLAQueryBuilder, "_load_options_cache", collections.OrderedDict()
|
|
274
|
+
)
|
|
275
|
+
qb = iface.query
|
|
276
|
+
model = iface.obj
|
|
277
|
+
for cols in (["id"], ["name"], ["age"]):
|
|
278
|
+
qb.apply_list_columns(_sa.select(model), cols)
|
|
279
|
+
|
|
280
|
+
cache = SQLAQueryBuilder._load_options_cache
|
|
281
|
+
assert len(cache) == 2
|
|
282
|
+
assert self._key(model, ["id"]) not in cache # oldest evicted
|
|
283
|
+
assert self._key(model, ["name"]) in cache
|
|
284
|
+
assert self._key(model, ["age"]) in cache
|
|
285
|
+
|
|
286
|
+
def test_lru_hit_keeps_key_alive(self, monkeypatch, iface):
|
|
287
|
+
import collections
|
|
288
|
+
|
|
289
|
+
import sqlalchemy as _sa
|
|
290
|
+
|
|
291
|
+
from fastapi_rtk.backends.sqla import db as _db
|
|
292
|
+
|
|
293
|
+
monkeypatch.setattr(_db, "_LOAD_OPTIONS_CACHE_MAXSIZE", 2)
|
|
294
|
+
monkeypatch.setattr(
|
|
295
|
+
SQLAQueryBuilder, "_load_options_cache", collections.OrderedDict()
|
|
296
|
+
)
|
|
297
|
+
qb = iface.query
|
|
298
|
+
model = iface.obj
|
|
299
|
+
qb.apply_list_columns(_sa.select(model), ["id"])
|
|
300
|
+
qb.apply_list_columns(_sa.select(model), ["name"])
|
|
301
|
+
qb.apply_list_columns(_sa.select(model), ["id"]) # hit -> moves "id" to MRU
|
|
302
|
+
qb.apply_list_columns(_sa.select(model), ["age"]) # evicts oldest ("name")
|
|
303
|
+
|
|
304
|
+
cache = SQLAQueryBuilder._load_options_cache
|
|
305
|
+
assert len(cache) == 2
|
|
306
|
+
assert self._key(model, ["name"]) not in cache
|
|
307
|
+
assert self._key(model, ["id"]) in cache
|
|
308
|
+
assert self._key(model, ["age"]) in cache
|
|
309
|
+
|
|
310
|
+
|
|
246
311
|
@pytest.mark.asyncio
|
|
247
312
|
class TestApplyFilterClassHeavy:
|
|
248
313
|
"""Lines 312-313: `is_heavy` filter dispatched via smart_run."""
|
|
249
314
|
|
|
250
315
|
async def test_heavy_filter_uses_smart_run(self):
|
|
251
316
|
import sqlalchemy as _sa
|
|
252
|
-
from fastapi_rtk.backends.sqla.filters import BaseFilter
|
|
253
|
-
from fastapi_rtk.backends.sqla.interface import SQLAInterface
|
|
254
|
-
from fastapi_rtk.backends.sqla.model import Model
|
|
255
317
|
from sqlalchemy.ext.asyncio import (
|
|
256
318
|
AsyncSession,
|
|
257
319
|
async_sessionmaker,
|
|
258
320
|
create_async_engine,
|
|
259
321
|
)
|
|
260
322
|
|
|
323
|
+
from fastapi_rtk.backends.sqla.filters import BaseFilter
|
|
324
|
+
from fastapi_rtk.backends.sqla.interface import SQLAInterface
|
|
325
|
+
from fastapi_rtk.backends.sqla.model import Model
|
|
326
|
+
|
|
261
327
|
class HeavyWidgetDb(Model):
|
|
262
328
|
__tablename__ = "heavy_widget_db"
|
|
263
329
|
__table_args__ = {"extend_existing": True}
|
|
@@ -1,6 +1,11 @@
|
|
|
1
|
+
import typing
|
|
2
|
+
import warnings
|
|
3
|
+
|
|
1
4
|
import pydantic
|
|
2
5
|
import pytest
|
|
3
6
|
import sqlalchemy.exc
|
|
7
|
+
|
|
8
|
+
from fastapi_rtk import AsyncTaskRunner
|
|
4
9
|
from fastapi_rtk.bases.interface import (
|
|
5
10
|
AbstractInterface,
|
|
6
11
|
MissingGreenletError,
|
|
@@ -277,6 +282,7 @@ class TestSqlaCompositePKReportsTrue:
|
|
|
277
282
|
|
|
278
283
|
def test_composite_pk(self):
|
|
279
284
|
import sqlalchemy
|
|
285
|
+
|
|
280
286
|
from fastapi_rtk.backends.sqla.interface import SQLAInterface
|
|
281
287
|
from fastapi_rtk.backends.sqla.model import Model
|
|
282
288
|
|
|
@@ -399,3 +405,102 @@ def test_before_validator_raises_on_greenlet_for_regular_attribute():
|
|
|
399
405
|
assert "'relation'" in msg
|
|
400
406
|
assert "MissingGreenlet" in msg
|
|
401
407
|
assert isinstance(excinfo.value.__cause__, sqlalchemy.exc.MissingGreenlet)
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
@pytest.mark.asyncio
|
|
411
|
+
async def test_before_validator_skips_async_columns_no_unawaited_coroutine():
|
|
412
|
+
"""The greenlet probe must skip async-property columns. Probing them via
|
|
413
|
+
getattr creates a coroutine the probe discards unawaited, emitting
|
|
414
|
+
`RuntimeWarning: coroutine ... was never awaited`. The async columns are
|
|
415
|
+
resolved by the after-validator instead."""
|
|
416
|
+
|
|
417
|
+
class _AsyncPropOrm:
|
|
418
|
+
@property
|
|
419
|
+
async def async_prop(self):
|
|
420
|
+
return "async value"
|
|
421
|
+
|
|
422
|
+
interface = _make_concrete_subclass()(obj=type("M", (), {}))
|
|
423
|
+
schema_dict = PydanticGenerationSchema(
|
|
424
|
+
__config__=pydantic.ConfigDict(from_attributes=True),
|
|
425
|
+
__async_columns__=["async_prop"],
|
|
426
|
+
__columns__=None,
|
|
427
|
+
__with_id__=False,
|
|
428
|
+
__with_name__=False,
|
|
429
|
+
__with_property__=True,
|
|
430
|
+
__optional__=False,
|
|
431
|
+
__hide_sensitive_columns__=False,
|
|
432
|
+
__with_fk__=True,
|
|
433
|
+
__name__="AsyncColRegressionSchema",
|
|
434
|
+
)
|
|
435
|
+
schema_dict["async_prop"] = (typing.Any, pydantic.Field(default=None))
|
|
436
|
+
Schema = interface._generate_schema_from_dict(schema_dict)
|
|
437
|
+
|
|
438
|
+
with warnings.catch_warnings(record=True) as caught:
|
|
439
|
+
warnings.simplefilter("always")
|
|
440
|
+
async with AsyncTaskRunner():
|
|
441
|
+
model = Schema.model_validate(_AsyncPropOrm())
|
|
442
|
+
|
|
443
|
+
unawaited = [
|
|
444
|
+
w
|
|
445
|
+
for w in caught
|
|
446
|
+
if issubclass(w.category, RuntimeWarning) and "never awaited" in str(w.message)
|
|
447
|
+
]
|
|
448
|
+
assert not unawaited, [str(w.message) for w in unawaited]
|
|
449
|
+
assert model.async_prop == "async value"
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
class TestGetRelatedInterfaceMemo:
|
|
453
|
+
"""get_related_interface caches instances per (related model, with_fk)."""
|
|
454
|
+
|
|
455
|
+
@classmethod
|
|
456
|
+
def _make_sub_with_relations(cls):
|
|
457
|
+
Sub = _make_concrete_subclass()
|
|
458
|
+
RelA = type("RelA", (), {})
|
|
459
|
+
RelB = type("RelB", (), {})
|
|
460
|
+
|
|
461
|
+
class RelSub(Sub):
|
|
462
|
+
def get_related_model(self, col_name):
|
|
463
|
+
return {"a": RelA, "a_again": RelA, "b": RelB}[col_name]
|
|
464
|
+
|
|
465
|
+
return RelSub, RelA, RelB
|
|
466
|
+
|
|
467
|
+
def test_same_column_returns_cached_instance(self):
|
|
468
|
+
RelSub, RelA, _ = self._make_sub_with_relations()
|
|
469
|
+
parent = RelSub(obj=type("M", (), {}))
|
|
470
|
+
first = parent.get_related_interface("a")
|
|
471
|
+
second = parent.get_related_interface("a")
|
|
472
|
+
assert first is second
|
|
473
|
+
assert first.obj is RelA
|
|
474
|
+
|
|
475
|
+
def test_with_fk_variants_cached_separately(self):
|
|
476
|
+
RelSub, _, _ = self._make_sub_with_relations()
|
|
477
|
+
parent = RelSub(obj=type("M", (), {}))
|
|
478
|
+
default = parent.get_related_interface("a")
|
|
479
|
+
with_fk = parent.get_related_interface("a", True)
|
|
480
|
+
without_fk = parent.get_related_interface("a", False)
|
|
481
|
+
assert default is not with_fk
|
|
482
|
+
assert default is not without_fk
|
|
483
|
+
assert with_fk is not without_fk
|
|
484
|
+
assert default.with_fk is None
|
|
485
|
+
assert with_fk.with_fk is True
|
|
486
|
+
assert without_fk.with_fk is False
|
|
487
|
+
assert parent.get_related_interface("a", True) is with_fk
|
|
488
|
+
assert parent.get_related_interface("a", False) is without_fk
|
|
489
|
+
|
|
490
|
+
def test_columns_sharing_model_share_interface(self):
|
|
491
|
+
RelSub, RelA, RelB = self._make_sub_with_relations()
|
|
492
|
+
parent = RelSub(obj=type("M", (), {}))
|
|
493
|
+
assert parent.get_related_interface("a") is parent.get_related_interface(
|
|
494
|
+
"a_again"
|
|
495
|
+
)
|
|
496
|
+
assert parent.get_related_interface("a") is not parent.get_related_interface(
|
|
497
|
+
"b"
|
|
498
|
+
)
|
|
499
|
+
|
|
500
|
+
def test_cache_is_per_parent_instance(self):
|
|
501
|
+
RelSub, _, _ = self._make_sub_with_relations()
|
|
502
|
+
parent_one = RelSub(obj=type("M", (), {}))
|
|
503
|
+
parent_two = RelSub(obj=type("M", (), {}))
|
|
504
|
+
assert parent_one.get_related_interface(
|
|
505
|
+
"a"
|
|
506
|
+
) is not parent_two.get_related_interface("a")
|
|
@@ -15,6 +15,7 @@ from unittest.mock import MagicMock, patch
|
|
|
15
15
|
|
|
16
16
|
import pytest
|
|
17
17
|
from fastapi import FastAPI
|
|
18
|
+
|
|
18
19
|
from fastapi_rtk.fastapi_react_toolkit import FastAPIReactToolkit
|
|
19
20
|
|
|
20
21
|
|
|
@@ -614,6 +615,28 @@ class TestToolkitLifespanSmoke:
|
|
|
614
615
|
assert app is not None
|
|
615
616
|
|
|
616
617
|
|
|
618
|
+
class TestToolkitLifespanReentry:
|
|
619
|
+
"""Regression: the lifespan must be re-enterable.
|
|
620
|
+
|
|
621
|
+
`db.close()` on shutdown disposes the engine, but the engine is created
|
|
622
|
+
once in `initialize()` (outside the lifespan). A second entry - the common
|
|
623
|
+
per-module `TestClient` / `lifespan_context` test pattern - must re-init the
|
|
624
|
+
engine instead of crashing with "DatabaseSessionManager is not initialized".
|
|
625
|
+
"""
|
|
626
|
+
|
|
627
|
+
@pytest.mark.asyncio
|
|
628
|
+
async def test_lifespan_context_reentrant(self, make_toolkit_app):
|
|
629
|
+
app = make_toolkit_app()
|
|
630
|
+
from fastapi_rtk.db import db
|
|
631
|
+
|
|
632
|
+
async with app.router.lifespan_context(app):
|
|
633
|
+
assert db._engine is not None
|
|
634
|
+
assert db._engine is None # shutdown disposed the engine
|
|
635
|
+
|
|
636
|
+
async with app.router.lifespan_context(app):
|
|
637
|
+
assert db._engine is not None
|
|
638
|
+
|
|
639
|
+
|
|
617
640
|
class TestToolkitLifespanUpgradeDb:
|
|
618
641
|
"""Regression: `upgrade_db=True` must not crash on async drivers.
|
|
619
642
|
|
|
@@ -123,24 +123,6 @@ class TestGlobalSingletonState:
|
|
|
123
123
|
def test_is_cli_default_false(self):
|
|
124
124
|
assert g.is_cli is False
|
|
125
125
|
|
|
126
|
-
def test_is_migrate_alias_resolves_to_is_cli(self):
|
|
127
|
-
# TODO(next-minor): remove with the deprecated `is_migrate` alias.
|
|
128
|
-
# The lazy default resolves to `is_cli` at first read, so drop any
|
|
129
|
-
# cached value before each read.
|
|
130
|
-
def _fresh_is_migrate():
|
|
131
|
-
g._bootstrap_values.pop("is_migrate", None)
|
|
132
|
-
g._vars.pop("is_migrate", None)
|
|
133
|
-
return g.is_migrate
|
|
134
|
-
|
|
135
|
-
try:
|
|
136
|
-
assert _fresh_is_migrate() == g.is_cli
|
|
137
|
-
g.is_cli = True
|
|
138
|
-
assert _fresh_is_migrate() is True
|
|
139
|
-
finally:
|
|
140
|
-
g.is_cli = False
|
|
141
|
-
g._bootstrap_values.pop("is_migrate", None)
|
|
142
|
-
g._vars.pop("is_migrate", None)
|
|
143
|
-
|
|
144
126
|
def test_sensitive_data_default(self):
|
|
145
127
|
assert "User" in g.sensitive_data
|
|
146
128
|
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import contextvars
|
|
3
|
+
import threading
|
|
3
4
|
|
|
4
5
|
import pytest
|
|
6
|
+
|
|
5
7
|
from fastapi_rtk.utils.run_utils import (
|
|
6
8
|
call_with_valid_kwargs,
|
|
7
9
|
parse_from_values_or_func,
|
|
@@ -194,6 +196,55 @@ class TestRunCoroutineInThreadpool:
|
|
|
194
196
|
assert result == "new value"
|
|
195
197
|
|
|
196
198
|
|
|
199
|
+
@pytest.fixture
|
|
200
|
+
def running_loop():
|
|
201
|
+
loop = asyncio.new_event_loop()
|
|
202
|
+
thread = threading.Thread(target=loop.run_forever, daemon=True)
|
|
203
|
+
thread.start()
|
|
204
|
+
try:
|
|
205
|
+
yield loop
|
|
206
|
+
finally:
|
|
207
|
+
loop.call_soon_threadsafe(loop.stop)
|
|
208
|
+
thread.join()
|
|
209
|
+
loop.close()
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
class TestSuppliedLoop:
|
|
213
|
+
def test_run_coroutine_in_threadpool_on_supplied_loop(self, running_loop):
|
|
214
|
+
async def async_func(x, y):
|
|
215
|
+
await asyncio.sleep(0.01)
|
|
216
|
+
return x + y
|
|
217
|
+
|
|
218
|
+
result = run_coroutine_in_threadpool(async_func(2, 3), loop=running_loop)
|
|
219
|
+
assert result == 5
|
|
220
|
+
|
|
221
|
+
def test_run_coroutine_in_threadpool_reuses_loop_bound_resource(self, running_loop):
|
|
222
|
+
async def make_lock():
|
|
223
|
+
return asyncio.Lock()
|
|
224
|
+
|
|
225
|
+
lock = run_coroutine_in_threadpool(make_lock(), loop=running_loop)
|
|
226
|
+
|
|
227
|
+
async def use_lock():
|
|
228
|
+
async with lock:
|
|
229
|
+
return "ok"
|
|
230
|
+
|
|
231
|
+
assert run_coroutine_in_threadpool(use_lock(), loop=running_loop) == "ok"
|
|
232
|
+
assert run_coroutine_in_threadpool(use_lock(), loop=running_loop) == "ok"
|
|
233
|
+
|
|
234
|
+
def test_safe_call_sync_on_supplied_loop(self, running_loop):
|
|
235
|
+
async def async_func():
|
|
236
|
+
return "looped"
|
|
237
|
+
|
|
238
|
+
assert safe_call_sync(async_func(), loop=running_loop) == "looped"
|
|
239
|
+
|
|
240
|
+
def test_smart_run_sync_on_supplied_loop(self, running_loop):
|
|
241
|
+
async def async_func(x):
|
|
242
|
+
await asyncio.sleep(0.01)
|
|
243
|
+
return x * 2
|
|
244
|
+
|
|
245
|
+
assert smart_run_sync(async_func, 5, loop=running_loop) == 10
|
|
246
|
+
|
|
247
|
+
|
|
197
248
|
class TestRunFunctionInThreadpool:
|
|
198
249
|
def test_run_function_in_threadpool(self):
|
|
199
250
|
def sync_func(x):
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "2.1.5"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|