fastapi-rtk 2.11.2__tar.gz → 2.11.4__tar.gz

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