fastapi-rtk 2.1.5__tar.gz → 2.2.0__tar.gz

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