fastapi-rtk 2.18.1__tar.gz → 2.18.3__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.18.1 → fastapi_rtk-2.18.3}/PKG-INFO +12 -2
- fastapi_rtk-2.18.3/fastapi_rtk/_version.py +1 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/agent_docs/guide/backend.md +3 -1
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/api/model_rest_api/services/_download_service.py +47 -1
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/backends/sqla/interface.py +1 -8
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/const.py +1 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/setting.py +8 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/utils/csv_json_converter.py +67 -26
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/pyproject.toml +18 -3
- fastapi_rtk-2.18.3/tests/unit/api/model_rest_api/test_download_service.py +204 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/unit/backends/sqla/test_interface.py +5 -12
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/unit/test_const.py +1 -1
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/unit/test_setting.py +11 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/unit/utils/test_csv_json_converter.py +97 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/uv.lock +99 -82
- fastapi_rtk-2.18.1/fastapi_rtk/_version.py +0 -1
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/.gitignore +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/LICENSE +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/README.md +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/__init__.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/agent_docs/adapters/agents-md/AGENTS.md +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/agent_docs/adapters/claude/skill.frontmatter +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/agent_docs/guide/frontend.md +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/agent_docs/guide/legacy.md +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/agent_docs/guide/overview.md +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/api/__init__.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/api/base_api.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/api/model_rest_api/__init__.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/api/model_rest_api/model_rest_api.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/api/model_rest_api/params.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/api/model_rest_api/services/__init__.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/api/model_rest_api/services/_add_service.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/api/model_rest_api/services/_bulk_service.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/api/model_rest_api/services/_delete_service.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/api/model_rest_api/services/_edit_service.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/api/model_rest_api/services/_file_service.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/api/model_rest_api/services/_info_service.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/api/model_rest_api/services/_list_service.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/api/model_rest_api/services/_schema_service.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/api/model_rest_api/services/_show_service.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/apis/__init__.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/apis/info.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/apis/license.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/auth/__init__.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/auth/auth.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/auth/hashers/__init__.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/auth/hashers/pbkdf2.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/auth/hashers/scrypt.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/auth/ldap.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/auth/password_helpers/__init__.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/auth/password_helpers/fab.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/auth/strategies/__init__.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/auth/strategies/config.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/auth/strategies/db.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/auth/strategies/jwt.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/backends/__init__.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/backends/generic/__init__.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/backends/generic/column.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/backends/generic/db.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/backends/generic/exceptions.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/backends/generic/filters.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/backends/generic/interface.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/backends/generic/model.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/backends/generic/session.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/backends/sqla/__init__.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/backends/sqla/column.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/backends/sqla/db.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/backends/sqla/exceptions.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/backends/sqla/extensions/__init__.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/backends/sqla/extensions/audit/__init__.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/backends/sqla/extensions/audit/_decorators_service.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/backends/sqla/extensions/audit/_lifecycle_service.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/backends/sqla/extensions/audit/_logger.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/backends/sqla/extensions/audit/_processing_service.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/backends/sqla/extensions/audit/audit.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/backends/sqla/extensions/audit/types.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/backends/sqla/extensions/geoalchemy2/__init__.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/backends/sqla/extensions/geoalchemy2/filters.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/backends/sqla/extensions/geoalchemy2/geometry_converter.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/backends/sqla/filters.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/backends/sqla/model.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/backends/sqla/session.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/bases/__init__.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/bases/cache.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/bases/db.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/bases/file_manager.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/bases/filter.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/bases/interface.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/bases/loop_manager.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/bases/model.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/bases/session.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/bases/user_cache.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/caches/__init__.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/caches/cache.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/caches/serializers.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/caches/storages.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/cli/__init__.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/cli/cli.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/cli/commands/__init__.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/cli/commands/_security_crypto.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/cli/commands/agent/__init__.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/cli/commands/agent/adapters/__init__.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/cli/commands/agent/adapters/agents_md.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/cli/commands/agent/adapters/claude.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/cli/commands/agent/base.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/cli/commands/agent/docs.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/cli/commands/db/__init__.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/cli/commands/db/templates/fastapi/README +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/cli/commands/db/templates/fastapi/alembic.ini.mako +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/cli/commands/db/templates/fastapi/env.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/cli/commands/db/templates/fastapi/script.py.mako +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/cli/commands/db/templates/fastapi-multidb/README +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/cli/commands/db/templates/fastapi-multidb/alembic.ini.mako +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/cli/commands/db/templates/fastapi-multidb/env.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/cli/commands/db/templates/fastapi-multidb/script.py.mako +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/cli/commands/export.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/cli/commands/security.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/cli/commands/translate.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/cli/const.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/cli/decorators.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/cli/types.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/cli/utils.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/config.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/db.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/decorators.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/dependencies.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/exceptions.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/fastapi_react_toolkit.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/file_managers/__init__.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/file_managers/file_manager.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/file_managers/image_manager.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/file_managers/s3_file_manager.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/file_managers/s3_image_manager.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/filters.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/globals.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/lang/__init__.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/lang/babel/__init__.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/lang/babel/cli.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/lang/babel/config.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/lang/babel.cfg +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/lang/lazy_text.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/lang/messages.pot +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/lang/translations/de/LC_MESSAGES/messages.mo +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/lang/translations/de/LC_MESSAGES/messages.po +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/lang/translations/en/LC_MESSAGES/messages.mo +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/lang/translations/en/LC_MESSAGES/messages.po +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/loop_managers/__init__.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/loop_managers/loop_manager.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/manager.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/middlewares.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/mixins.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/models.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/py.typed +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/registry.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/routers.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/schemas.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/security/__init__.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/security/sqla/__init__.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/security/sqla/apis.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/security/sqla/models.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/security/sqla/security_manager/__init__.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/security/sqla/security_manager/_builtin_role_service.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/security/sqla/security_manager/_entities.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/security/sqla/security_manager/_exception_filters.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/security/sqla/security_manager/_export.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/security/sqla/security_manager/_import.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/security/sqla/security_manager/_permission_service.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/security/sqla/security_manager/_results.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/security/sqla/security_manager/_role_service.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/security/sqla/security_manager/_user_service.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/security/sqla/security_manager/security_manager.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/security/sqla/user_cache.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/types.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/utils/__init__.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/utils/async_task_runner.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/utils/class_factory.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/utils/deep_merge.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/utils/extender_mixin.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/utils/flask_appbuilder_utils.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/utils/formatter.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/utils/hooks.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/utils/lazy.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/utils/merge_schema.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/utils/multiple_async_contexts.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/utils/pydantic.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/utils/run_utils.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/utils/self_dependencies.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/utils/smartdefaultdict.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/utils/sqla.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/utils/timezone.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/utils/update_signature.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/utils/use_default_when_none.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/fastapi_rtk/version.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/conftest.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/docker-compose.gis.yml +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/integration/__init__.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/integration/api/__init__.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/integration/api/test_add_edit_relation_response.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/integration/api/test_model_rest_api.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/integration/apis/__init__.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/integration/apis/test_info_callable_schema.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/integration/apis/test_info_contract.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/integration/auth/__init__.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/integration/auth/test_auth_flow.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/integration/auth/test_oauth.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/integration/conftest.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/integration/file_managers/__init__.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/integration/file_managers/test_file_routes.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/integration/security/__init__.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/integration/security/sqla/__init__.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/integration/security/sqla/conftest.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/integration/security/sqla/test_permission_service.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/integration/security/sqla/test_role_service.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/integration/security/sqla/test_security_manager.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/integration/security/sqla/test_user_service.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/integration/test_app_smoke.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/unit/__init__.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/unit/api/__init__.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/unit/api/conftest.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/unit/api/model_rest_api/__init__.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/unit/api/model_rest_api/test_columns.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/unit/api/model_rest_api/test_file_mixin.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/unit/api/model_rest_api/test_init.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/unit/api/model_rest_api/test_service_delegators.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/unit/api/test_base_api.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/unit/apis/__init__.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/unit/apis/test_info.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/unit/apis/test_license.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/unit/auth/__init__.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/unit/auth/hashers/__init__.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/unit/auth/hashers/test_pbkdf2.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/unit/auth/hashers/test_scrypt.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/unit/auth/password_helpers/__init__.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/unit/auth/password_helpers/test_fab.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/unit/auth/strategies/__init__.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/unit/auth/strategies/test_config.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/unit/auth/strategies/test_db.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/unit/auth/strategies/test_jwt.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/unit/auth/test_auth.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/unit/backends/__init__.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/unit/backends/generic/__init__.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/unit/backends/generic/test_column.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/unit/backends/generic/test_db.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/unit/backends/generic/test_exceptions.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/unit/backends/generic/test_filters.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/unit/backends/generic/test_interface.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/unit/backends/generic/test_model.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/unit/backends/generic/test_session.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/unit/backends/sqla/__init__.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/unit/backends/sqla/conftest.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/unit/backends/sqla/test_audit.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/unit/backends/sqla/test_audit_service_delegators.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/unit/backends/sqla/test_audit_types.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/unit/backends/sqla/test_column.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/unit/backends/sqla/test_db.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/unit/backends/sqla/test_filters.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/unit/backends/sqla/test_geoalchemy2.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/unit/backends/sqla/test_model.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/unit/backends/sqla/test_session.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/unit/bases/__init__.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/unit/bases/test_db.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/unit/bases/test_file_manager.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/unit/bases/test_filter.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/unit/bases/test_interface.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/unit/bases/test_model.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/unit/bases/test_session.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/unit/cli/__init__.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/unit/cli/commands/__init__.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/unit/cli/commands/conftest.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/unit/cli/commands/test_db.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/unit/cli/commands/test_export.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/unit/cli/commands/test_security.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/unit/cli/commands/test_security_crypto.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/unit/cli/commands/test_translate.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/unit/cli/conftest.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/unit/cli/test_cli.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/unit/cli/test_decorators.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/unit/cli/test_types.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/unit/cli/test_utils.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/unit/file_managers/__init__.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/unit/file_managers/conftest.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/unit/file_managers/test_file_manager.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/unit/file_managers/test_image_manager.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/unit/file_managers/test_s3_file_manager.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/unit/file_managers/test_s3_image_manager.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/unit/lang/__init__.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/unit/lang/test_babel_cli.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/unit/lang/test_babel_config.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/unit/lang/test_lazy_text.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/unit/security/__init__.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/unit/security/sqla/__init__.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/unit/security/sqla/conftest.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/unit/security/sqla/security_manager/__init__.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/unit/security/sqla/security_manager/test_builtin_role_service.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/unit/security/sqla/security_manager/test_entities.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/unit/security/sqla/security_manager/test_permission_service.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/unit/security/sqla/security_manager/test_results.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/unit/security/sqla/security_manager/test_role_service.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/unit/security/sqla/security_manager/test_user_service.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/unit/security/sqla/test_apis.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/unit/security/sqla/test_models.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/unit/security/sqla/test_security_manager.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/unit/test_caches.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/unit/test_config.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/unit/test_db.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/unit/test_decorators.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/unit/test_dependencies.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/unit/test_exceptions.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/unit/test_fastapi_react_toolkit.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/unit/test_filters.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/unit/test_globals.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/unit/test_manager.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/unit/test_middlewares.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/unit/test_mixins.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/unit/test_models.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/unit/test_registry.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/unit/test_routers.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/unit/test_schemas.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/unit/test_types.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/unit/test_user_cache.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/unit/test_version.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/unit/utils/__init__.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/unit/utils/test_async_task_runner.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/unit/utils/test_class_factory.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/unit/utils/test_deep_merge.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/unit/utils/test_extender_mixin.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/unit/utils/test_flask_appbuilder_utils.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/unit/utils/test_formatter.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/unit/utils/test_hooks.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/unit/utils/test_lazy.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/unit/utils/test_merge_schema.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/unit/utils/test_multiple_async_contexts.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/unit/utils/test_pydantic.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/unit/utils/test_run_utils.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/unit/utils/test_self_dependencies.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/unit/utils/test_smartdefaultdict.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/unit/utils/test_sqla.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/unit/utils/test_timezone.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/unit/utils/test_update_signature.py +0 -0
- {fastapi_rtk-2.18.1 → fastapi_rtk-2.18.3}/tests/unit/utils/test_use_default_when_none.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: fastapi-rtk
|
|
3
|
-
Version: 2.18.
|
|
3
|
+
Version: 2.18.3
|
|
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
|
|
@@ -16,16 +16,24 @@ Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
|
|
16
16
|
Requires-Python: >=3.11
|
|
17
17
|
Requires-Dist: alembic>=1.17.0
|
|
18
18
|
Requires-Dist: beautifulsoup4>=4.14.2
|
|
19
|
+
Requires-Dist: cryptography>=50.0.1
|
|
19
20
|
Requires-Dist: fastapi-babel>=1.0.0
|
|
20
21
|
Requires-Dist: fastapi-users[oauth,sqlalchemy]>=15.0.5
|
|
21
22
|
Requires-Dist: fastapi[standard]>=0.119.1
|
|
23
|
+
Requires-Dist: idna>=3.15
|
|
22
24
|
Requires-Dist: jsonschema2md>=1.7.0
|
|
25
|
+
Requires-Dist: mako>=1.3.12
|
|
23
26
|
Requires-Dist: marshmallow-sqlalchemy>=1.4.2
|
|
24
27
|
Requires-Dist: pip-licenses>=5.5.1
|
|
25
28
|
Requires-Dist: polib>=1.2.0
|
|
26
|
-
Requires-Dist: prometheus-fastapi-instrumentator>=
|
|
29
|
+
Requires-Dist: prometheus-fastapi-instrumentator>=8.0.0
|
|
30
|
+
Requires-Dist: pydantic-settings>=2.14.2
|
|
31
|
+
Requires-Dist: pyjwt[crypto]>=2.13.0
|
|
32
|
+
Requires-Dist: python-multipart>=0.0.31
|
|
27
33
|
Requires-Dist: secweb<2,>=1.25.2
|
|
34
|
+
Requires-Dist: soupsieve>=2.8.4
|
|
28
35
|
Requires-Dist: sqlalchemy-utils>=0.42.0
|
|
36
|
+
Requires-Dist: starlette>=1.3.1
|
|
29
37
|
Requires-Dist: werkzeug>=3.1.5
|
|
30
38
|
Provides-Extra: audit-bg
|
|
31
39
|
Requires-Dist: apscheduler>=3.11.2; extra == 'audit-bg'
|
|
@@ -36,10 +44,12 @@ Requires-Dist: geoalchemy2>=0.18.1; extra == 'geometry'
|
|
|
36
44
|
Requires-Dist: shapely>=2.1.2; extra == 'geometry'
|
|
37
45
|
Provides-Extra: ldap
|
|
38
46
|
Requires-Dist: ldap3>=2.9.1; extra == 'ldap'
|
|
47
|
+
Requires-Dist: pyasn1>=0.6.4; extra == 'ldap'
|
|
39
48
|
Provides-Extra: profiler
|
|
40
49
|
Requires-Dist: pyinstrument>=5.1.2; extra == 'profiler'
|
|
41
50
|
Provides-Extra: s3
|
|
42
51
|
Requires-Dist: smart-open[s3]>=7.5.0; extra == 's3'
|
|
52
|
+
Requires-Dist: urllib3>=2.7.0; extra == 's3'
|
|
43
53
|
Provides-Extra: test
|
|
44
54
|
Requires-Dist: aiosqlite>=0.21.0; extra == 'test'
|
|
45
55
|
Requires-Dist: pytest-asyncio>=1.3.0; extra == 'test'
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "2.18.3"
|
|
@@ -135,6 +135,8 @@ class Item(Model):
|
|
|
135
135
|
|
|
136
136
|
`__repr__` is load-bearing: the frontend uses it as the label for related items in dropdowns and grid cells. If it returns nothing meaningful, users see `<Item object at 0x...>`.
|
|
137
137
|
|
|
138
|
+
A bare relation in `list_columns` (`"category"`, no sub-column) loads every column of the related row, so a related model carrying a large column (a JSONB snapshot, a blob) is fetched in full once per row. Declare `__repr_columns__ = ["name"]` on the related model, naming the columns its `__repr__` reads: `/_info` dropdown values and the `/download` CSV then fetch only the primary key plus those columns, for a nested path ending in a relation (`"order.customer"`) as well as a top-level one. The list and show JSON still serialize the full related row, so a heavy column that no response needs is best marked `deferred=True` on its `mapped_column`.
|
|
139
|
+
|
|
138
140
|
### List columns
|
|
139
141
|
|
|
140
142
|
For plain `Mapped[list[T] | None]` columns, use `ListColumn` (JSON-backed) or `JSONBListColumn` (Postgres JSONB-backed) from `fastapi_rtk` instead of `sqlalchemy.dialects.postgresql.JSONB` directly. The `TypeDecorator` enforces list-shape at bind time, optionally coerces each entry to a target Python type, and lets `ModelRestApi` recognize the column as a list (rendered as a tag-array field by the frontend, not a free-form JSON blob).
|
|
@@ -551,7 +553,7 @@ The task runs later, possibly after the loop has advanced or the enclosing funct
|
|
|
551
553
|
|
|
552
554
|
### Sizing the `/download` stream
|
|
553
555
|
|
|
554
|
-
`/download` exports every row matching the query, so it ignores `page` and `page_size` from the request; they reach the handler and are overwritten. It reads the rows in chunks, and the `download_page_size` attribute on the Api decides how large a chunk is (since the first release after v2.15.0). It is either an int (a fixed chunk) or a resolver `(api, session, params) -> int`. It defaults to the `API_DOWNLOAD_PAGE_SIZE` config key, which is itself the built-in resolver: it counts the matching rows and takes a tenth of them, floored at the `API_DOWNLOAD_MIN_PAGE_SIZE` config key (default `100`) and capped at `API_DOWNLOAD_MAX_PAGE_SIZE` (default `1000`) so a large table cannot size the chunk into an out-of-memory read.
|
|
556
|
+
`/download` exports every row matching the query, so it ignores `page` and `page_size` from the request; they reach the handler and are overwritten. It reads the rows from the database in chunks, and the `download_page_size` attribute on the Api decides how large a chunk is (since the first release after v2.15.0); the HTTP response is written separately, in pieces of `API_DOWNLOAD_RESPONSE_CHUNK_SIZE` characters of CSV text (default `8192`; `0` or a negative value sends one chunk per row). It is either an int (a fixed chunk) or a resolver `(api, session, params) -> int`. It defaults to the `API_DOWNLOAD_PAGE_SIZE` config key, which is itself the built-in resolver: it counts the matching rows and takes a tenth of them, floored at the `API_DOWNLOAD_MIN_PAGE_SIZE` config key (default `100`) and capped at `API_DOWNLOAD_MAX_PAGE_SIZE` (default `1000`) so a large table cannot size the chunk into an out-of-memory read.
|
|
555
557
|
|
|
556
558
|
Set it to an int for a fixed chunk, which also skips the count query the default runs:
|
|
557
559
|
|
|
@@ -108,6 +108,7 @@ class DownloadService:
|
|
|
108
108
|
return GeneralResponse(detail=translate("OK"))
|
|
109
109
|
|
|
110
110
|
params = self.api._handle_query_params(q)
|
|
111
|
+
params["list_columns"] = self._repr_projection(params["list_columns"])
|
|
111
112
|
page_size = self.api.download_page_size
|
|
112
113
|
if callable(page_size):
|
|
113
114
|
page_size = await safe_call(page_size(self.api, session, params))
|
|
@@ -169,6 +170,10 @@ class DownloadService:
|
|
|
169
170
|
writer.writerow(labels)
|
|
170
171
|
yield line.read()
|
|
171
172
|
|
|
173
|
+
# Every yielded chunk crosses the globals middleware's memory stream and one ASGI send, so rows are batched.
|
|
174
|
+
chunk_size = Setting.API_DOWNLOAD_RESPONSE_CHUNK_SIZE
|
|
175
|
+
buffer: list[str] = []
|
|
176
|
+
buffered = 0
|
|
172
177
|
async for chunk in data:
|
|
173
178
|
for item in chunk:
|
|
174
179
|
row = await CSVJSONConverter.ajson_to_csv_single(
|
|
@@ -178,7 +183,15 @@ class DownloadService:
|
|
|
178
183
|
export_mode=export_mode,
|
|
179
184
|
)
|
|
180
185
|
writer.writerow(row)
|
|
181
|
-
|
|
186
|
+
text = line.read()
|
|
187
|
+
buffer.append(text)
|
|
188
|
+
buffered += len(text)
|
|
189
|
+
if buffered >= chunk_size:
|
|
190
|
+
yield "".join(buffer)
|
|
191
|
+
buffer.clear()
|
|
192
|
+
buffered = 0
|
|
193
|
+
if buffer:
|
|
194
|
+
yield "".join(buffer)
|
|
182
195
|
|
|
183
196
|
async def import_data( # pragma: no cover
|
|
184
197
|
self,
|
|
@@ -278,6 +291,39 @@ class DownloadService:
|
|
|
278
291
|
|
|
279
292
|
return list_data
|
|
280
293
|
|
|
294
|
+
def _repr_projection(self, columns: list[str]) -> list[str]:
|
|
295
|
+
"""
|
|
296
|
+
Narrow a column ending in a relation to the columns its CSV cell needs, when that model declares them.
|
|
297
|
+
|
|
298
|
+
A CSV cell renders a relation as ``str(item)`` (``name_``) or its primary key (``id_``), so a related model declaring ``__repr_columns__`` is fetched with its primary key plus those columns instead of every column, which is what a path ending in a relation loads otherwise. The path may be nested (``a.b.c``); only its last segment is narrowed.
|
|
299
|
+
|
|
300
|
+
Args:
|
|
301
|
+
columns (list[str]): The columns the export query fetches.
|
|
302
|
+
|
|
303
|
+
Returns:
|
|
304
|
+
list[str]: The same columns, with each qualifying relation path replaced by dotted sub-columns.
|
|
305
|
+
"""
|
|
306
|
+
projected: list[str] = []
|
|
307
|
+
for col in columns:
|
|
308
|
+
interface = self.api.datamodel
|
|
309
|
+
*path, leaf = col.split(".")
|
|
310
|
+
for segment in path:
|
|
311
|
+
if not interface.is_relation(segment):
|
|
312
|
+
interface = None
|
|
313
|
+
break
|
|
314
|
+
interface = interface.get_related_interface(segment)
|
|
315
|
+
repr_columns = None
|
|
316
|
+
if interface is not None and interface.is_relation(leaf):
|
|
317
|
+
related = interface.get_related_interface(leaf)
|
|
318
|
+
repr_columns = getattr(related.obj, "__repr_columns__", None)
|
|
319
|
+
if not repr_columns:
|
|
320
|
+
projected.append(col)
|
|
321
|
+
continue
|
|
322
|
+
projected.extend(
|
|
323
|
+
f"{col}.{sub}" for sub in [*related.get_pk_attrs(), *repr_columns]
|
|
324
|
+
)
|
|
325
|
+
return projected
|
|
326
|
+
|
|
281
327
|
|
|
282
328
|
async def default_download_page_size(
|
|
283
329
|
api: "ModelRestApi", session: SQLASession, params: DBQueryParams
|
|
@@ -152,19 +152,12 @@ class SQLAInterface(
|
|
|
152
152
|
result = result.scalars()
|
|
153
153
|
else:
|
|
154
154
|
result = session.scalars(statement)
|
|
155
|
+
# No expunge: the identity map holds rows weakly, so a consumed chunk is freed once the caller drops it, and expunge_all() would swap the map the streaming result still holds.
|
|
155
156
|
while True:
|
|
156
157
|
chunk = await smart_run(result.fetchmany, page_size)
|
|
157
158
|
if not chunk:
|
|
158
159
|
break
|
|
159
160
|
yield chunk
|
|
160
|
-
|
|
161
|
-
# Expunge consumed rows one by one; expunge_all() swaps the identity
|
|
162
|
-
# map the streaming result still holds, killing mid-stream fetches.
|
|
163
|
-
def expunge_chunk() -> None:
|
|
164
|
-
for item in chunk:
|
|
165
|
-
session.expunge(item)
|
|
166
|
-
|
|
167
|
-
await smart_run(expunge_chunk)
|
|
168
161
|
await safe_call(self.close(session))
|
|
169
162
|
|
|
170
163
|
async def add(self, session, item, *, flush=True, commit=True, refresh=True):
|
|
@@ -74,6 +74,7 @@ DEFAULT_API_PAGE_SIZE = 25
|
|
|
74
74
|
DEFAULT_API_MAX_PAGE_SIZE = 25
|
|
75
75
|
DEFAULT_API_DOWNLOAD_MIN_PAGE_SIZE = 100
|
|
76
76
|
DEFAULT_API_DOWNLOAD_MAX_PAGE_SIZE = 1000
|
|
77
|
+
DEFAULT_API_DOWNLOAD_RESPONSE_CHUNK_SIZE = 8 * 1024
|
|
77
78
|
DEFAULT_TOKEN_URL = "/api/v1/auth/jwt/login"
|
|
78
79
|
DEFAULT_SECRET = "SUPERSECRET"
|
|
79
80
|
DEFAULT_COOKIE_NAME = "dataTactics"
|
|
@@ -4,6 +4,7 @@ from .const import (
|
|
|
4
4
|
DEFAULT_ADMIN_ROLE,
|
|
5
5
|
DEFAULT_API_DOWNLOAD_MAX_PAGE_SIZE,
|
|
6
6
|
DEFAULT_API_DOWNLOAD_MIN_PAGE_SIZE,
|
|
7
|
+
DEFAULT_API_DOWNLOAD_RESPONSE_CHUNK_SIZE,
|
|
7
8
|
DEFAULT_API_MAX_PAGE_SIZE,
|
|
8
9
|
DEFAULT_API_PAGE_SIZE,
|
|
9
10
|
DEFAULT_LANG_FOLDER,
|
|
@@ -100,6 +101,13 @@ class _Setting:
|
|
|
100
101
|
"API_DOWNLOAD_MAX_PAGE_SIZE", DEFAULT_API_DOWNLOAD_MAX_PAGE_SIZE
|
|
101
102
|
)
|
|
102
103
|
|
|
104
|
+
@property
|
|
105
|
+
def API_DOWNLOAD_RESPONSE_CHUNK_SIZE(self) -> int:
|
|
106
|
+
return g.config.get(
|
|
107
|
+
"API_DOWNLOAD_RESPONSE_CHUNK_SIZE",
|
|
108
|
+
DEFAULT_API_DOWNLOAD_RESPONSE_CHUNK_SIZE,
|
|
109
|
+
)
|
|
110
|
+
|
|
103
111
|
@property
|
|
104
112
|
def API_DOWNLOAD_PAGE_SIZE(self) -> "int | DownloadPageSizeResolver":
|
|
105
113
|
from .api.model_rest_api.services._download_service import (
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import abc
|
|
2
2
|
import csv
|
|
3
3
|
import enum
|
|
4
|
+
import functools
|
|
4
5
|
import inspect
|
|
5
6
|
import json
|
|
7
|
+
import types
|
|
6
8
|
import typing
|
|
7
9
|
|
|
8
|
-
from .run_utils import safe_call
|
|
9
|
-
|
|
10
10
|
if typing.TYPE_CHECKING:
|
|
11
11
|
from ..bases import BasicModel
|
|
12
12
|
|
|
@@ -118,7 +118,7 @@ class CSVJSONConverter:
|
|
|
118
118
|
|
|
119
119
|
@classmethod
|
|
120
120
|
def json_to_csv_single(
|
|
121
|
-
|
|
121
|
+
cls,
|
|
122
122
|
data: "dict[str, typing.Any] | BasicModel",
|
|
123
123
|
/,
|
|
124
124
|
*,
|
|
@@ -141,14 +141,12 @@ class CSVJSONConverter:
|
|
|
141
141
|
str: The CSV data as a string.
|
|
142
142
|
"""
|
|
143
143
|
csv_data: list[str] = []
|
|
144
|
-
data_pipeline =
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
144
|
+
data_pipeline = cls.get_data_pipeline(
|
|
145
|
+
async_column=False,
|
|
146
|
+
relation_separator=relation_separator,
|
|
147
|
+
delimiter=delimiter,
|
|
148
|
+
export_mode=export_mode,
|
|
149
149
|
)
|
|
150
|
-
data_pipeline.add_processor(EnumProcessor())
|
|
151
|
-
data_pipeline.add_processor(FallbackProcessor())
|
|
152
150
|
|
|
153
151
|
for col in list_columns:
|
|
154
152
|
value = data_pipeline.process(data, col)
|
|
@@ -181,14 +179,12 @@ class CSVJSONConverter:
|
|
|
181
179
|
str: The CSV data as a string.
|
|
182
180
|
"""
|
|
183
181
|
csv_data: list[str] = []
|
|
184
|
-
data_pipeline =
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
182
|
+
data_pipeline = cls.get_data_pipeline(
|
|
183
|
+
async_column=True,
|
|
184
|
+
relation_separator=relation_separator,
|
|
185
|
+
delimiter=delimiter,
|
|
186
|
+
export_mode=export_mode,
|
|
189
187
|
)
|
|
190
|
-
data_pipeline.add_processor(EnumProcessor())
|
|
191
|
-
data_pipeline.add_processor(FallbackProcessor())
|
|
192
188
|
|
|
193
189
|
for col in list_columns:
|
|
194
190
|
value = await data_pipeline.aprocess(data, col)
|
|
@@ -196,6 +192,44 @@ class CSVJSONConverter:
|
|
|
196
192
|
|
|
197
193
|
return csv_data
|
|
198
194
|
|
|
195
|
+
@classmethod
|
|
196
|
+
@functools.lru_cache(maxsize=64)
|
|
197
|
+
def get_data_pipeline(
|
|
198
|
+
cls,
|
|
199
|
+
*,
|
|
200
|
+
async_column: bool,
|
|
201
|
+
relation_separator: str,
|
|
202
|
+
delimiter: str,
|
|
203
|
+
export_mode: ExportMode,
|
|
204
|
+
) -> "DataPipeline":
|
|
205
|
+
"""
|
|
206
|
+
Build the row-conversion pipeline once per parameter set; its processors hold no per-row state.
|
|
207
|
+
|
|
208
|
+
Override to change the processor chain; the cache is keyed per class, so a subclass gets its own pipelines. The delimiter comes from the request, hence the bounded cache.
|
|
209
|
+
|
|
210
|
+
Args:
|
|
211
|
+
async_column (bool): Whether the column processor awaits coroutine-valued attributes.
|
|
212
|
+
relation_separator (str): The separator between a relation and its sub-column.
|
|
213
|
+
delimiter (str): The CSV delimiter, which picks the list separator.
|
|
214
|
+
export_mode (ExportMode): Export mode (simplified or detailed).
|
|
215
|
+
|
|
216
|
+
Returns:
|
|
217
|
+
DataPipeline: The shared pipeline for these parameters.
|
|
218
|
+
"""
|
|
219
|
+
data_pipeline = DataPipeline()
|
|
220
|
+
data_pipeline.add_processor(
|
|
221
|
+
AsyncColumnProcessor(relation_separator)
|
|
222
|
+
if async_column
|
|
223
|
+
else ColumnProcessor(relation_separator)
|
|
224
|
+
)
|
|
225
|
+
data_pipeline.add_processor(ModelProcessor())
|
|
226
|
+
data_pipeline.add_processor(
|
|
227
|
+
ListProcessor(delimiter=delimiter, export_mode=export_mode)
|
|
228
|
+
)
|
|
229
|
+
data_pipeline.add_processor(EnumProcessor())
|
|
230
|
+
data_pipeline.add_processor(FallbackProcessor())
|
|
231
|
+
return data_pipeline
|
|
232
|
+
|
|
199
233
|
@classmethod
|
|
200
234
|
def _convert_nested_col_into_dict(
|
|
201
235
|
cls,
|
|
@@ -254,15 +288,19 @@ class CSVJSONConverter:
|
|
|
254
288
|
class DataPipeline:
|
|
255
289
|
def __init__(self):
|
|
256
290
|
self.processors = list[DataProcessor]()
|
|
291
|
+
self._steps = list[tuple[DataProcessor, bool]]()
|
|
257
292
|
|
|
258
293
|
def add_processor(self, processor: "DataProcessor"):
|
|
259
294
|
"""
|
|
260
295
|
Adds a data processor to the pipeline.
|
|
261
296
|
|
|
297
|
+
Whether ``process`` is a coroutine function is decided here, once, so ``aprocess`` awaits only the processors that need it.
|
|
298
|
+
|
|
262
299
|
Args:
|
|
263
300
|
processor (DataProcessor): The data processor to add.
|
|
264
301
|
"""
|
|
265
302
|
self.processors.append(processor)
|
|
303
|
+
self._steps.append((processor, inspect.iscoroutinefunction(processor.process)))
|
|
266
304
|
|
|
267
305
|
def process(self, data: typing.Any, col: str):
|
|
268
306
|
"""
|
|
@@ -292,8 +330,11 @@ class DataPipeline:
|
|
|
292
330
|
Returns:
|
|
293
331
|
typing.Any: The processed data.
|
|
294
332
|
"""
|
|
295
|
-
for processor in self.
|
|
296
|
-
|
|
333
|
+
for processor, is_async in self._steps:
|
|
334
|
+
if is_async:
|
|
335
|
+
data, col, should_continue = await processor.process(data, col)
|
|
336
|
+
else:
|
|
337
|
+
data, col, should_continue = processor.process(data, col)
|
|
297
338
|
if not should_continue:
|
|
298
339
|
break
|
|
299
340
|
return data
|
|
@@ -335,22 +376,23 @@ class ColumnProcessor(DataProcessor):
|
|
|
335
376
|
class AsyncColumnProcessor(ColumnProcessor):
|
|
336
377
|
async def process(self, data, col):
|
|
337
378
|
data, col, continue_processing = super().process(data, col)
|
|
338
|
-
if
|
|
379
|
+
if isinstance(data, types.CoroutineType):
|
|
339
380
|
data = await data
|
|
340
381
|
return data, col, continue_processing
|
|
341
382
|
|
|
342
383
|
|
|
343
384
|
class ModelProcessor(DataProcessor):
|
|
344
385
|
def __init__(self, attr="name_"):
|
|
386
|
+
from ..bases import BasicModel
|
|
387
|
+
|
|
345
388
|
super().__init__()
|
|
346
389
|
self.attr = attr
|
|
390
|
+
self.model_class = BasicModel
|
|
347
391
|
|
|
348
392
|
def process(self, data, col):
|
|
349
|
-
from ..bases import BasicModel
|
|
350
|
-
|
|
351
393
|
continue_processing = True
|
|
352
394
|
|
|
353
|
-
if isinstance(data,
|
|
395
|
+
if isinstance(data, self.model_class):
|
|
354
396
|
data = getattr(data, self.attr, "")
|
|
355
397
|
continue_processing = False
|
|
356
398
|
return data, col, continue_processing
|
|
@@ -383,10 +425,9 @@ class ListProcessor(DataProcessor):
|
|
|
383
425
|
self.dict_processor = DictProcessor(
|
|
384
426
|
attr_detailed if export_mode == "detailed" else attr_simplified
|
|
385
427
|
)
|
|
428
|
+
self.model_class = self.model_processor.model_class
|
|
386
429
|
|
|
387
430
|
def process(self, data, col):
|
|
388
|
-
from ..bases import BasicModel
|
|
389
|
-
|
|
390
431
|
continue_processing = True
|
|
391
432
|
|
|
392
433
|
if isinstance(data, list):
|
|
@@ -394,7 +435,7 @@ class ListProcessor(DataProcessor):
|
|
|
394
435
|
for item in data:
|
|
395
436
|
if isinstance(item, dict):
|
|
396
437
|
item_processed, _, _ = self.dict_processor.process(item, col)
|
|
397
|
-
elif isinstance(item,
|
|
438
|
+
elif isinstance(item, self.model_class):
|
|
398
439
|
item_processed, _, _ = self.model_processor.process(item, col)
|
|
399
440
|
else:
|
|
400
441
|
item_processed = str(item)
|
|
@@ -29,6 +29,7 @@ requires-python = ">=3.11"
|
|
|
29
29
|
dependencies = [
|
|
30
30
|
"alembic>=1.17.0",
|
|
31
31
|
"beautifulsoup4>=4.14.2",
|
|
32
|
+
"cryptography>=50.0.1",
|
|
32
33
|
"fastapi-babel>=1.0.0",
|
|
33
34
|
"fastapi-users[oauth,sqlalchemy]>=15.0.5",
|
|
34
35
|
"fastapi[standard]>=0.119.1",
|
|
@@ -36,10 +37,18 @@ dependencies = [
|
|
|
36
37
|
"marshmallow-sqlalchemy>=1.4.2",
|
|
37
38
|
"pip-licenses>=5.5.1",
|
|
38
39
|
"polib>=1.2.0",
|
|
39
|
-
"prometheus-fastapi-instrumentator>=
|
|
40
|
+
"prometheus-fastapi-instrumentator>=8.0.0",
|
|
41
|
+
"pyjwt[crypto]>=2.13.0",
|
|
40
42
|
"secweb>=1.25.2,<2",
|
|
41
43
|
"sqlalchemy-utils>=0.42.0",
|
|
44
|
+
"starlette>=1.3.1",
|
|
42
45
|
"werkzeug>=3.1.5",
|
|
46
|
+
# Security floors: transitive deps pinned above known-vulnerable versions.
|
|
47
|
+
"idna>=3.15", # IDNA decoder DoS on crafted input (CVE-2026-45409)
|
|
48
|
+
"mako>=1.3.12", # TemplateLookup path traversal (CVE-2026-44307)
|
|
49
|
+
"pydantic-settings>=2.14.2", # secrets_dir directory traversal (GHSA-4xgf-cpjx-pc3j)
|
|
50
|
+
"python-multipart>=0.0.31", # multipart/urlencoded parser DoS (CVE-2026-53540)
|
|
51
|
+
"soupsieve>=2.8.4", # CSS selector parser ReDoS (CVE-2026-49477)
|
|
43
52
|
]
|
|
44
53
|
urls = { Homepage = "https://codeberg.org/datatactics/fastapi-rtk", Issues = "https://codeberg.org/datatactics/fastapi-rtk/issues" }
|
|
45
54
|
scripts = { "fastapi-rtk" = "fastapi_rtk.cli:main" }
|
|
@@ -47,10 +56,16 @@ scripts = { "fastapi-rtk" = "fastapi_rtk.cli:main" }
|
|
|
47
56
|
[project.optional-dependencies]
|
|
48
57
|
fab = ["prison>=0.2.1"]
|
|
49
58
|
geometry = ["geoalchemy2>=0.18.1", "shapely>=2.1.2"]
|
|
50
|
-
s3 = [
|
|
59
|
+
s3 = [
|
|
60
|
+
"smart-open[s3]>=7.5.0",
|
|
61
|
+
"urllib3>=2.7.0", # response decompression amplification (CVE-2026-44432)
|
|
62
|
+
]
|
|
51
63
|
audit-bg = ["apscheduler>=3.11.2"]
|
|
52
64
|
profiler = ["pyinstrument>=5.1.2"]
|
|
53
|
-
ldap = [
|
|
65
|
+
ldap = [
|
|
66
|
+
"ldap3>=2.9.1",
|
|
67
|
+
"pyasn1>=0.6.4", # BER/CER/DER decoder DoS (CVE-2026-59884)
|
|
68
|
+
]
|
|
54
69
|
test = [
|
|
55
70
|
"aiosqlite>=0.21.0",
|
|
56
71
|
"pytest>=9.0.2",
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"""`DownloadService`: chunked CSV output and the repr-column projection of relation paths."""
|
|
2
|
+
|
|
3
|
+
import csv
|
|
4
|
+
import io
|
|
5
|
+
import types
|
|
6
|
+
|
|
7
|
+
import pydantic
|
|
8
|
+
import pytest
|
|
9
|
+
import sqlalchemy
|
|
10
|
+
import sqlalchemy.orm
|
|
11
|
+
import sqlalchemy.pool
|
|
12
|
+
|
|
13
|
+
from fastapi_rtk import Mapped, mapped_column, relationship
|
|
14
|
+
from fastapi_rtk.api.model_rest_api.services._download_service import DownloadService
|
|
15
|
+
from fastapi_rtk.backends.sqla.interface import SQLAInterface
|
|
16
|
+
from fastapi_rtk.backends.sqla.model import Model
|
|
17
|
+
|
|
18
|
+
LIST_COLUMNS = ["name", "description"]
|
|
19
|
+
LABEL_COLUMNS = {"name": "Name", "description": "Description"}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class _ExportSite(Model):
|
|
23
|
+
__tablename__ = "download_service_site"
|
|
24
|
+
__repr_columns__ = ["code"]
|
|
25
|
+
id: Mapped[int] = mapped_column(primary_key=True)
|
|
26
|
+
code: Mapped[str]
|
|
27
|
+
secret: Mapped[str | None]
|
|
28
|
+
runs: Mapped[list["_ExportRun"]] = relationship(back_populates="site")
|
|
29
|
+
|
|
30
|
+
def __repr__(self):
|
|
31
|
+
return self.code
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class _ExportRun(Model):
|
|
35
|
+
__tablename__ = "download_service_run"
|
|
36
|
+
__repr_columns__ = ["label"]
|
|
37
|
+
id: Mapped[int] = mapped_column(primary_key=True)
|
|
38
|
+
label: Mapped[str]
|
|
39
|
+
payload: Mapped[str | None]
|
|
40
|
+
site_id: Mapped[int | None] = mapped_column(
|
|
41
|
+
sqlalchemy.ForeignKey("download_service_site.id")
|
|
42
|
+
)
|
|
43
|
+
site: Mapped[_ExportSite | None] = relationship(back_populates="runs")
|
|
44
|
+
rows: Mapped[list["_ExportRow"]] = relationship(back_populates="run")
|
|
45
|
+
|
|
46
|
+
def __repr__(self):
|
|
47
|
+
return self.label
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class _ExportRow(Model):
|
|
51
|
+
__tablename__ = "download_service_row"
|
|
52
|
+
id: Mapped[int] = mapped_column(primary_key=True)
|
|
53
|
+
name: Mapped[str]
|
|
54
|
+
run_id: Mapped[int] = mapped_column(
|
|
55
|
+
sqlalchemy.ForeignKey("download_service_run.id")
|
|
56
|
+
)
|
|
57
|
+
run: Mapped[_ExportRun] = relationship(back_populates="rows")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@pytest.fixture
|
|
61
|
+
def row_service():
|
|
62
|
+
return DownloadService(types.SimpleNamespace(datamodel=SQLAInterface(_ExportRow)))
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@pytest.fixture
|
|
66
|
+
def seeded_session():
|
|
67
|
+
engine = sqlalchemy.create_engine(
|
|
68
|
+
"sqlite://",
|
|
69
|
+
connect_args={"check_same_thread": False},
|
|
70
|
+
poolclass=sqlalchemy.pool.StaticPool,
|
|
71
|
+
)
|
|
72
|
+
_ExportSite.__table__.create(engine)
|
|
73
|
+
_ExportRun.__table__.create(engine)
|
|
74
|
+
_ExportRow.__table__.create(engine)
|
|
75
|
+
with sqlalchemy.orm.Session(engine) as session:
|
|
76
|
+
site = _ExportSite(id=1, code="FRA", secret="s" * 1000)
|
|
77
|
+
run = _ExportRun(id=1, label="run 1", payload="x" * 1000, site=site)
|
|
78
|
+
session.add_all([run, _ExportRow(id=1, name="a", run=run)])
|
|
79
|
+
session.commit()
|
|
80
|
+
session.expunge_all()
|
|
81
|
+
yield session
|
|
82
|
+
engine.dispose()
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class TestReprProjection:
|
|
86
|
+
def test_bare_relation_with_repr_columns_becomes_pk_plus_repr_columns(
|
|
87
|
+
self, row_service
|
|
88
|
+
):
|
|
89
|
+
assert row_service._repr_projection(["name", "run"]) == [
|
|
90
|
+
"name",
|
|
91
|
+
"run.id",
|
|
92
|
+
"run.label",
|
|
93
|
+
]
|
|
94
|
+
|
|
95
|
+
def test_relation_without_repr_columns_is_kept_bare(self, row_service, monkeypatch):
|
|
96
|
+
monkeypatch.setattr(_ExportRun, "__repr_columns__", None)
|
|
97
|
+
assert row_service._repr_projection(["name", "run"]) == ["name", "run"]
|
|
98
|
+
|
|
99
|
+
def test_dotted_and_plain_columns_are_untouched(self, row_service):
|
|
100
|
+
assert row_service._repr_projection(
|
|
101
|
+
["run.payload", "run.site.code", "name", "id"]
|
|
102
|
+
) == ["run.payload", "run.site.code", "name", "id"]
|
|
103
|
+
|
|
104
|
+
def test_nested_path_ending_in_a_relation_is_narrowed(self, row_service):
|
|
105
|
+
assert row_service._repr_projection(["run.site"]) == [
|
|
106
|
+
"run.site.id",
|
|
107
|
+
"run.site.code",
|
|
108
|
+
]
|
|
109
|
+
|
|
110
|
+
def test_unknown_segment_is_kept_as_is(self, row_service):
|
|
111
|
+
assert row_service._repr_projection(["nope.site", "run.nope"]) == [
|
|
112
|
+
"nope.site",
|
|
113
|
+
"run.nope",
|
|
114
|
+
]
|
|
115
|
+
|
|
116
|
+
async def test_nested_projection_leaves_other_related_columns_unloaded(
|
|
117
|
+
self, row_service, seeded_session
|
|
118
|
+
):
|
|
119
|
+
columns = row_service._repr_projection(["run.site"])
|
|
120
|
+
rows = await row_service.api.datamodel.get_many(
|
|
121
|
+
seeded_session, {"list_columns": columns}
|
|
122
|
+
)
|
|
123
|
+
site_state = sqlalchemy.inspect(rows[0].run.site)
|
|
124
|
+
assert "secret" in site_state.unloaded
|
|
125
|
+
assert {"id", "code"}.isdisjoint(site_state.unloaded)
|
|
126
|
+
assert str(rows[0].run.site) == "FRA"
|
|
127
|
+
|
|
128
|
+
async def test_projected_query_leaves_other_related_columns_unloaded(
|
|
129
|
+
self, row_service, seeded_session
|
|
130
|
+
):
|
|
131
|
+
columns = row_service._repr_projection(["name", "run"])
|
|
132
|
+
rows = await row_service.api.datamodel.get_many(
|
|
133
|
+
seeded_session, {"list_columns": columns}
|
|
134
|
+
)
|
|
135
|
+
run_state = sqlalchemy.inspect(rows[0].run)
|
|
136
|
+
assert "payload" in run_state.unloaded
|
|
137
|
+
assert {"id", "label"}.isdisjoint(run_state.unloaded)
|
|
138
|
+
assert str(rows[0].run) == "run 1"
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
class _RowSchema(pydantic.BaseModel):
|
|
142
|
+
name: str
|
|
143
|
+
description: str
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _rows(count: int, description_length: int = 4):
|
|
147
|
+
return [
|
|
148
|
+
{"name": f"row_{i}", "description": "x" * description_length}
|
|
149
|
+
for i in range(count)
|
|
150
|
+
]
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
async def _chunks(rows, chunk_size: int):
|
|
154
|
+
for start in range(0, len(rows), chunk_size):
|
|
155
|
+
yield rows[start : start + chunk_size]
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _expected_csv(rows):
|
|
159
|
+
buffer = io.StringIO()
|
|
160
|
+
writer = csv.writer(buffer)
|
|
161
|
+
writer.writerow([LABEL_COLUMNS[col] for col in LIST_COLUMNS])
|
|
162
|
+
for row in rows:
|
|
163
|
+
writer.writerow([row[col] for col in LIST_COLUMNS])
|
|
164
|
+
return buffer.getvalue()
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
async def _export(rows, chunk_size: int = 2):
|
|
168
|
+
service = DownloadService(api=None)
|
|
169
|
+
pieces = []
|
|
170
|
+
async for piece in service.export_data(
|
|
171
|
+
_chunks(rows, chunk_size), LIST_COLUMNS, LABEL_COLUMNS, _RowSchema
|
|
172
|
+
):
|
|
173
|
+
pieces.append(piece)
|
|
174
|
+
return pieces
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
async def test_header_then_one_chunk_for_a_small_export():
|
|
178
|
+
rows = _rows(5)
|
|
179
|
+
pieces = await _export(rows)
|
|
180
|
+
assert len(pieces) == 2
|
|
181
|
+
assert "".join(pieces) == _expected_csv(rows)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
async def test_large_export_is_batched_but_byte_identical(config_setter):
|
|
185
|
+
config_setter("API_DOWNLOAD_RESPONSE_CHUNK_SIZE", 200)
|
|
186
|
+
rows = _rows(50, description_length=30)
|
|
187
|
+
pieces = await _export(rows, chunk_size=7)
|
|
188
|
+
body = pieces[1:]
|
|
189
|
+
assert len(body) > 1
|
|
190
|
+
assert all(len(piece) >= 200 for piece in body[:-1])
|
|
191
|
+
assert "".join(pieces) == _expected_csv(rows)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
async def test_negative_chunk_size_sends_one_chunk_per_row(config_setter):
|
|
195
|
+
config_setter("API_DOWNLOAD_RESPONSE_CHUNK_SIZE", -1)
|
|
196
|
+
rows = _rows(5)
|
|
197
|
+
pieces = await _export(rows)
|
|
198
|
+
assert len(pieces) == 1 + len(rows)
|
|
199
|
+
assert "".join(pieces) == _expected_csv(rows)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
async def test_empty_export_yields_only_the_header():
|
|
203
|
+
pieces = await _export([])
|
|
204
|
+
assert pieces == [_expected_csv([])]
|
|
@@ -379,18 +379,11 @@ class TestCRUD:
|
|
|
379
379
|
total = sum(len(c) for c in chunks)
|
|
380
380
|
assert total == 3
|
|
381
381
|
|
|
382
|
-
async def
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
expunged.append(item)
|
|
388
|
-
original(item)
|
|
389
|
-
|
|
390
|
-
monkeypatch.setattr(session, "expunge", expunge)
|
|
391
|
-
async for _chunk in iface.yield_per(session, {"page_size": 2}):
|
|
392
|
-
pass
|
|
393
|
-
assert len(expunged) == 3
|
|
382
|
+
async def test_yield_per_does_not_retain_consumed_rows(self, session, iface):
|
|
383
|
+
async for chunk in iface.yield_per(session, {"page_size": 2}):
|
|
384
|
+
assert len(session.identity_map) >= len(chunk)
|
|
385
|
+
del chunk
|
|
386
|
+
assert len(session.identity_map) == 0
|
|
394
387
|
|
|
395
388
|
async def test_yield_per_async_survives_chunk_boundary(self):
|
|
396
389
|
engine = sqlalchemy.ext.asyncio.create_async_engine("sqlite+aiosqlite://")
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
|
|
3
3
|
import pytest
|
|
4
|
-
|
|
5
4
|
from fastapi_rtk import const
|
|
6
5
|
|
|
7
6
|
|
|
@@ -60,6 +59,7 @@ class TestDefaults:
|
|
|
60
59
|
assert const.DEFAULT_API_MAX_PAGE_SIZE == 25
|
|
61
60
|
assert const.DEFAULT_API_DOWNLOAD_MIN_PAGE_SIZE == 100
|
|
62
61
|
assert const.DEFAULT_API_DOWNLOAD_MAX_PAGE_SIZE == 1000
|
|
62
|
+
assert const.DEFAULT_API_DOWNLOAD_RESPONSE_CHUNK_SIZE == 8192
|
|
63
63
|
|
|
64
64
|
def test_default_token_url(self):
|
|
65
65
|
assert const.DEFAULT_TOKEN_URL.startswith("/")
|