fastapi-rtk 2.6.1__tar.gz → 2.7.1__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.6.1 → fastapi_rtk-2.7.1}/PKG-INFO +1 -1
  2. fastapi_rtk-2.7.1/fastapi_rtk/_version.py +1 -0
  3. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/cli/commands/security.py +1 -1
  4. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/cli/decorators.py +24 -1
  5. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/const.py +1 -0
  6. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/globals.py +5 -0
  7. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/security/sqla/apis.py +46 -0
  8. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/security/sqla/security_manager/_import.py +39 -12
  9. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/unit/cli/test_decorators.py +58 -1
  10. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/unit/security/sqla/test_apis.py +56 -0
  11. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/unit/security/sqla/test_security_manager.py +55 -0
  12. fastapi_rtk-2.6.1/fastapi_rtk/_version.py +0 -1
  13. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/.gitignore +0 -0
  14. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/LICENSE +0 -0
  15. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/README.md +0 -0
  16. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/__init__.py +0 -0
  17. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/api/__init__.py +0 -0
  18. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/api/base_api.py +0 -0
  19. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/api/model_rest_api/__init__.py +0 -0
  20. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/api/model_rest_api/_add_mixin.py +0 -0
  21. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/api/model_rest_api/_bulk_mixin.py +0 -0
  22. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/api/model_rest_api/_columns_mixin.py +0 -0
  23. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/api/model_rest_api/_delete_mixin.py +0 -0
  24. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/api/model_rest_api/_download_mixin.py +0 -0
  25. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/api/model_rest_api/_edit_mixin.py +0 -0
  26. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/api/model_rest_api/_file_mixin.py +0 -0
  27. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/api/model_rest_api/_info_mixin.py +0 -0
  28. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/api/model_rest_api/_list_mixin.py +0 -0
  29. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/api/model_rest_api/_params.py +0 -0
  30. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/api/model_rest_api/_show_mixin.py +0 -0
  31. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/api/model_rest_api/_typed_base.py +0 -0
  32. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/api/model_rest_api/_upload_mixin.py +0 -0
  33. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/apis/__init__.py +0 -0
  34. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/apis/info.py +0 -0
  35. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/apis/license.py +0 -0
  36. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/auth/__init__.py +0 -0
  37. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/auth/auth.py +0 -0
  38. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/auth/hashers/__init__.py +0 -0
  39. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/auth/hashers/pbkdf2.py +0 -0
  40. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/auth/hashers/scrypt.py +0 -0
  41. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/auth/ldap.py +0 -0
  42. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/auth/password_helpers/__init__.py +0 -0
  43. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/auth/password_helpers/fab.py +0 -0
  44. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/auth/strategies/__init__.py +0 -0
  45. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/auth/strategies/config.py +0 -0
  46. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/auth/strategies/db.py +0 -0
  47. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/auth/strategies/jwt.py +0 -0
  48. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/backends/__init__.py +0 -0
  49. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/backends/generic/__init__.py +0 -0
  50. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/backends/generic/column.py +0 -0
  51. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/backends/generic/db.py +0 -0
  52. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/backends/generic/exceptions.py +0 -0
  53. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/backends/generic/filters.py +0 -0
  54. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/backends/generic/interface.py +0 -0
  55. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/backends/generic/model.py +0 -0
  56. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/backends/generic/session.py +0 -0
  57. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/backends/sqla/__init__.py +0 -0
  58. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/backends/sqla/column.py +0 -0
  59. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/backends/sqla/db.py +0 -0
  60. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/backends/sqla/exceptions.py +0 -0
  61. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/backends/sqla/extensions/__init__.py +0 -0
  62. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/backends/sqla/extensions/audit/__init__.py +0 -0
  63. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/backends/sqla/extensions/audit/_decorators_mixin.py +0 -0
  64. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/backends/sqla/extensions/audit/_lifecycle_mixin.py +0 -0
  65. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/backends/sqla/extensions/audit/_logger.py +0 -0
  66. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/backends/sqla/extensions/audit/_processing_mixin.py +0 -0
  67. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/backends/sqla/extensions/audit/audit.py +0 -0
  68. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/backends/sqla/extensions/audit/types.py +0 -0
  69. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/backends/sqla/extensions/geoalchemy2/__init__.py +0 -0
  70. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/backends/sqla/extensions/geoalchemy2/filters.py +0 -0
  71. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/backends/sqla/extensions/geoalchemy2/geometry_converter.py +0 -0
  72. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/backends/sqla/filters.py +0 -0
  73. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/backends/sqla/interface.py +0 -0
  74. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/backends/sqla/model.py +0 -0
  75. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/backends/sqla/session.py +0 -0
  76. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/bases/__init__.py +0 -0
  77. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/bases/db.py +0 -0
  78. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/bases/file_manager.py +0 -0
  79. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/bases/filter.py +0 -0
  80. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/bases/interface.py +0 -0
  81. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/bases/model.py +0 -0
  82. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/bases/session.py +0 -0
  83. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/cli/__init__.py +0 -0
  84. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/cli/cli.py +0 -0
  85. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/cli/commands/__init__.py +0 -0
  86. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/cli/commands/_security_crypto.py +0 -0
  87. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/cli/commands/db/__init__.py +0 -0
  88. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/cli/commands/db/templates/fastapi/README +0 -0
  89. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/cli/commands/db/templates/fastapi/alembic.ini.mako +0 -0
  90. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/cli/commands/db/templates/fastapi/env.py +0 -0
  91. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/cli/commands/db/templates/fastapi/script.py.mako +0 -0
  92. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/cli/commands/db/templates/fastapi-multidb/README +0 -0
  93. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/cli/commands/db/templates/fastapi-multidb/alembic.ini.mako +0 -0
  94. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/cli/commands/db/templates/fastapi-multidb/env.py +0 -0
  95. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/cli/commands/db/templates/fastapi-multidb/script.py.mako +0 -0
  96. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/cli/commands/export.py +0 -0
  97. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/cli/commands/translate.py +0 -0
  98. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/cli/const.py +0 -0
  99. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/cli/types.py +0 -0
  100. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/cli/utils.py +0 -0
  101. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/config.py +0 -0
  102. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/db.py +0 -0
  103. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/decorators.py +0 -0
  104. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/dependencies.py +0 -0
  105. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/exceptions.py +0 -0
  106. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/fastapi_react_toolkit.py +0 -0
  107. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/file_managers/__init__.py +0 -0
  108. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/file_managers/file_manager.py +0 -0
  109. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/file_managers/image_manager.py +0 -0
  110. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/file_managers/s3_file_manager.py +0 -0
  111. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/file_managers/s3_image_manager.py +0 -0
  112. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/filters.py +0 -0
  113. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/lang/__init__.py +0 -0
  114. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/lang/babel/__init__.py +0 -0
  115. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/lang/babel/cli.py +0 -0
  116. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/lang/babel/config.py +0 -0
  117. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/lang/babel.cfg +0 -0
  118. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/lang/lazy_text.py +0 -0
  119. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/lang/messages.pot +0 -0
  120. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/lang/translations/de/LC_MESSAGES/messages.mo +0 -0
  121. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/lang/translations/de/LC_MESSAGES/messages.po +0 -0
  122. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/lang/translations/en/LC_MESSAGES/messages.mo +0 -0
  123. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/lang/translations/en/LC_MESSAGES/messages.po +0 -0
  124. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/manager.py +0 -0
  125. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/middlewares.py +0 -0
  126. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/mixins.py +0 -0
  127. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/models.py +0 -0
  128. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/py.typed +0 -0
  129. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/registry.py +0 -0
  130. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/routers.py +0 -0
  131. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/schemas.py +0 -0
  132. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/security/__init__.py +0 -0
  133. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/security/sqla/__init__.py +0 -0
  134. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/security/sqla/models.py +0 -0
  135. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/security/sqla/security_manager/__init__.py +0 -0
  136. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/security/sqla/security_manager/_association_mixin.py +0 -0
  137. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/security/sqla/security_manager/_builtin_role_mixin.py +0 -0
  138. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/security/sqla/security_manager/_cleanup_mixin.py +0 -0
  139. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/security/sqla/security_manager/_exception_filters.py +0 -0
  140. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/security/sqla/security_manager/_export.py +0 -0
  141. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/security/sqla/security_manager/_role_mixin.py +0 -0
  142. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/security/sqla/security_manager/_user_mixin.py +0 -0
  143. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/setting.py +0 -0
  144. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/types.py +0 -0
  145. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/utils/__init__.py +0 -0
  146. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/utils/async_task_runner.py +0 -0
  147. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/utils/class_factory.py +0 -0
  148. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/utils/csv_json_converter.py +0 -0
  149. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/utils/deep_merge.py +0 -0
  150. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/utils/extender_mixin.py +0 -0
  151. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/utils/flask_appbuilder_utils.py +0 -0
  152. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/utils/formatter.py +0 -0
  153. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/utils/hooks.py +0 -0
  154. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/utils/lazy.py +0 -0
  155. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/utils/merge_schema.py +0 -0
  156. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/utils/multiple_async_contexts.py +0 -0
  157. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/utils/pydantic.py +0 -0
  158. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/utils/run_utils.py +0 -0
  159. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/utils/self_dependencies.py +0 -0
  160. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/utils/smartdefaultdict.py +0 -0
  161. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/utils/sqla.py +0 -0
  162. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/utils/timezone.py +0 -0
  163. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/utils/update_signature.py +0 -0
  164. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/utils/use_default_when_none.py +0 -0
  165. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/fastapi_rtk/version.py +0 -0
  166. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/pyproject.toml +0 -0
  167. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/conftest.py +0 -0
  168. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/docker-compose.gis.yml +0 -0
  169. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/integration/__init__.py +0 -0
  170. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/integration/api/__init__.py +0 -0
  171. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/integration/api/test_model_rest_api.py +0 -0
  172. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/integration/apis/__init__.py +0 -0
  173. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/integration/apis/test_info_callable_schema.py +0 -0
  174. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/integration/apis/test_info_contract.py +0 -0
  175. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/integration/auth/__init__.py +0 -0
  176. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/integration/auth/test_auth_flow.py +0 -0
  177. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/integration/auth/test_oauth.py +0 -0
  178. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/integration/conftest.py +0 -0
  179. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/integration/file_managers/__init__.py +0 -0
  180. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/integration/file_managers/test_file_routes.py +0 -0
  181. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/integration/security/__init__.py +0 -0
  182. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/integration/security/sqla/__init__.py +0 -0
  183. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/integration/security/sqla/test_security_manager.py +0 -0
  184. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/integration/test_app_smoke.py +0 -0
  185. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/unit/__init__.py +0 -0
  186. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/unit/api/__init__.py +0 -0
  187. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/unit/api/conftest.py +0 -0
  188. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/unit/api/model_rest_api/__init__.py +0 -0
  189. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/unit/api/model_rest_api/test_columns.py +0 -0
  190. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/unit/api/model_rest_api/test_file_mixin.py +0 -0
  191. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/unit/api/model_rest_api/test_init.py +0 -0
  192. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/unit/api/model_rest_api/test_typed_base_consistency.py +0 -0
  193. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/unit/api/test_base_api.py +0 -0
  194. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/unit/apis/__init__.py +0 -0
  195. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/unit/apis/test_info.py +0 -0
  196. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/unit/apis/test_license.py +0 -0
  197. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/unit/auth/__init__.py +0 -0
  198. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/unit/auth/hashers/__init__.py +0 -0
  199. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/unit/auth/hashers/test_pbkdf2.py +0 -0
  200. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/unit/auth/hashers/test_scrypt.py +0 -0
  201. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/unit/auth/password_helpers/__init__.py +0 -0
  202. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/unit/auth/password_helpers/test_fab.py +0 -0
  203. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/unit/auth/strategies/__init__.py +0 -0
  204. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/unit/auth/strategies/test_config.py +0 -0
  205. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/unit/auth/strategies/test_db.py +0 -0
  206. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/unit/auth/strategies/test_jwt.py +0 -0
  207. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/unit/auth/test_auth.py +0 -0
  208. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/unit/backends/__init__.py +0 -0
  209. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/unit/backends/generic/__init__.py +0 -0
  210. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/unit/backends/generic/test_column.py +0 -0
  211. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/unit/backends/generic/test_db.py +0 -0
  212. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/unit/backends/generic/test_exceptions.py +0 -0
  213. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/unit/backends/generic/test_filters.py +0 -0
  214. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/unit/backends/generic/test_interface.py +0 -0
  215. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/unit/backends/generic/test_model.py +0 -0
  216. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/unit/backends/generic/test_session.py +0 -0
  217. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/unit/backends/sqla/__init__.py +0 -0
  218. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/unit/backends/sqla/conftest.py +0 -0
  219. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/unit/backends/sqla/test_audit.py +0 -0
  220. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/unit/backends/sqla/test_audit_types.py +0 -0
  221. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/unit/backends/sqla/test_column.py +0 -0
  222. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/unit/backends/sqla/test_db.py +0 -0
  223. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/unit/backends/sqla/test_filters.py +0 -0
  224. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/unit/backends/sqla/test_geoalchemy2.py +0 -0
  225. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/unit/backends/sqla/test_interface.py +0 -0
  226. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/unit/backends/sqla/test_model.py +0 -0
  227. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/unit/backends/sqla/test_session.py +0 -0
  228. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/unit/bases/__init__.py +0 -0
  229. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/unit/bases/test_db.py +0 -0
  230. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/unit/bases/test_file_manager.py +0 -0
  231. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/unit/bases/test_filter.py +0 -0
  232. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/unit/bases/test_interface.py +0 -0
  233. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/unit/bases/test_model.py +0 -0
  234. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/unit/bases/test_session.py +0 -0
  235. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/unit/cli/__init__.py +0 -0
  236. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/unit/cli/commands/__init__.py +0 -0
  237. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/unit/cli/commands/conftest.py +0 -0
  238. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/unit/cli/commands/test_db.py +0 -0
  239. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/unit/cli/commands/test_export.py +0 -0
  240. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/unit/cli/commands/test_security.py +0 -0
  241. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/unit/cli/commands/test_security_crypto.py +0 -0
  242. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/unit/cli/commands/test_translate.py +0 -0
  243. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/unit/cli/conftest.py +0 -0
  244. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/unit/cli/test_cli.py +0 -0
  245. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/unit/cli/test_types.py +0 -0
  246. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/unit/cli/test_utils.py +0 -0
  247. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/unit/file_managers/__init__.py +0 -0
  248. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/unit/file_managers/conftest.py +0 -0
  249. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/unit/file_managers/test_file_manager.py +0 -0
  250. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/unit/file_managers/test_image_manager.py +0 -0
  251. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/unit/file_managers/test_s3_file_manager.py +0 -0
  252. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/unit/file_managers/test_s3_image_manager.py +0 -0
  253. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/unit/lang/__init__.py +0 -0
  254. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/unit/lang/test_babel_cli.py +0 -0
  255. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/unit/lang/test_babel_config.py +0 -0
  256. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/unit/lang/test_lazy_text.py +0 -0
  257. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/unit/security/__init__.py +0 -0
  258. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/unit/security/sqla/__init__.py +0 -0
  259. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/unit/security/sqla/conftest.py +0 -0
  260. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/unit/security/sqla/test_models.py +0 -0
  261. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/unit/test_config.py +0 -0
  262. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/unit/test_const.py +0 -0
  263. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/unit/test_db.py +0 -0
  264. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/unit/test_decorators.py +0 -0
  265. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/unit/test_dependencies.py +0 -0
  266. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/unit/test_exceptions.py +0 -0
  267. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/unit/test_fastapi_react_toolkit.py +0 -0
  268. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/unit/test_filters.py +0 -0
  269. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/unit/test_globals.py +0 -0
  270. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/unit/test_manager.py +0 -0
  271. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/unit/test_middlewares.py +0 -0
  272. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/unit/test_mixins.py +0 -0
  273. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/unit/test_models.py +0 -0
  274. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/unit/test_registry.py +0 -0
  275. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/unit/test_routers.py +0 -0
  276. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/unit/test_schemas.py +0 -0
  277. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/unit/test_setting.py +0 -0
  278. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/unit/test_types.py +0 -0
  279. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/unit/test_version.py +0 -0
  280. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/unit/utils/__init__.py +0 -0
  281. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/unit/utils/test_async_task_runner.py +0 -0
  282. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/unit/utils/test_class_factory.py +0 -0
  283. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/unit/utils/test_csv_json_converter.py +0 -0
  284. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/unit/utils/test_deep_merge.py +0 -0
  285. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/unit/utils/test_extender_mixin.py +0 -0
  286. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/unit/utils/test_flask_appbuilder_utils.py +0 -0
  287. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/unit/utils/test_formatter.py +0 -0
  288. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/unit/utils/test_hooks.py +0 -0
  289. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/unit/utils/test_lazy.py +0 -0
  290. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/unit/utils/test_merge_schema.py +0 -0
  291. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/unit/utils/test_multiple_async_contexts.py +0 -0
  292. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/unit/utils/test_pydantic.py +0 -0
  293. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/unit/utils/test_run_utils.py +0 -0
  294. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/unit/utils/test_self_dependencies.py +0 -0
  295. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/unit/utils/test_smartdefaultdict.py +0 -0
  296. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/unit/utils/test_sqla.py +0 -0
  297. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/unit/utils/test_timezone.py +0 -0
  298. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/unit/utils/test_update_signature.py +0 -0
  299. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/tests/unit/utils/test_use_default_when_none.py +0 -0
  300. {fastapi_rtk-2.6.1 → fastapi_rtk-2.7.1}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastapi-rtk
3
- Version: 2.6.1
3
+ Version: 2.7.1
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.7.1"
@@ -129,7 +129,7 @@ async def import_users(
129
129
  ] = False,
130
130
  ):
131
131
  """
132
- Import users from a file produced by export-users. Existing usernames are skipped.
132
+ Import users from a file produced by export-users, skipping case-insensitive username/email matches.
133
133
  """
134
134
  payload = _load_import_payload(
135
135
  file_path,
@@ -1,6 +1,7 @@
1
1
  import functools
2
2
  import inspect
3
3
 
4
+ import click
4
5
  import fastapi_cli.discover
5
6
  import fastapi_cli.exceptions
6
7
  import typer
@@ -58,13 +59,35 @@ def ensure_fastapi_rtk_tables_exist(f):
58
59
  return ensure_app_loaded(wrapper)
59
60
 
60
61
 
62
+ def _current_command_path() -> str | None:
63
+ """Build the full CLI command path from the active Click context.
64
+
65
+ Walks from the invoked command up to (but excluding) the program root and
66
+ joins the names with spaces (group then subcommand).
67
+
68
+ Returns:
69
+ str | None: The space-joined command names, or `None` when there is no active Click context.
70
+ """
71
+ ctx = click.get_current_context(silent=True)
72
+ if ctx is None:
73
+ return None
74
+ names = []
75
+ node = ctx
76
+ while node is not None and node.parent is not None:
77
+ names.append(node.info_name)
78
+ node = node.parent
79
+ return " ".join(reversed(names)) or None
80
+
81
+
61
82
  def _check_existing_app(f, *, silent=False):
62
83
  @functools.wraps(f)
63
84
  def wrapper(*args, **kwargs):
64
85
  # No app loaded -> CLI invocation: flag cli (before import, so the
65
- # toolkit skips the lifespan) and import from path. Else reuse it.
86
+ # toolkit skips the lifespan), record the command, then import from
87
+ # path. Else reuse it.
66
88
  if g.current_app is None:
67
89
  g.is_cli = True
90
+ g.cli_command = _current_command_path()
68
91
  try:
69
92
  fastapi_cli.discover.get_import_data(path=g.path)
70
93
  except fastapi_cli.exceptions.FastAPICLIException as e:
@@ -148,6 +148,7 @@ BEARER_STRATEGY_CONFIG = {
148
148
  class ErrorCode(str, enum.Enum):
149
149
  GET_USER_MISSING_TOKEN_OR_INACTIVE_USER = "GET_USER_MISSING_TOKEN_OR_INACTIVE_USER"
150
150
  GET_USER_NO_ROLES = "GET_USER_NO_ROLES"
151
+ USER_ALREADY_EXISTS = "USER_ALREADY_EXISTS"
151
152
  PERMISSION_DENIED = "PERMISSION_DENIED"
152
153
  FEATURE_NOT_IMPLEMENTED = "FEATURE_NOT_IMPLEMENTED"
153
154
  PAGE_NOT_FOUND = "PAGE_NOT_FOUND"
@@ -87,6 +87,10 @@ class Globals:
87
87
  """
88
88
  A boolean value to indicate if the application is loaded by the CLI. `True` when you are running a command from `fastapi-rtk` CLI.
89
89
  """
90
+ cli_command: str | None
91
+ """
92
+ The full CLI command path currently running (group and subcommand, space-joined), or `None` outside the CLI. Set before the app module is imported.
93
+ """
90
94
  sensitive_data: dict[str, list[str]]
91
95
  """
92
96
  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"]}`.
@@ -283,6 +287,7 @@ g.set_default(
283
287
  )
284
288
  g.set_default("config", Config())
285
289
  g.set_default("is_cli", False)
290
+ g.set_default("cli_command", None)
286
291
  g.set_default("sensitive_data", {"User": ["password", "hashed_password"]})
287
292
  g.set_default(
288
293
  "file_manager",
@@ -238,6 +238,9 @@ class UsersApi(ModelRestApi):
238
238
  if col.name == "password":
239
239
  col.required = False
240
240
 
241
+ async def pre_add(self, item, params):
242
+ await self._check_user_unique(params.session, item.email, item.username)
243
+
241
244
  async def post_add(self, item, params):
242
245
  await g.current_app.sm.update_user(
243
246
  user_update={"password": item.password},
@@ -246,6 +249,14 @@ class UsersApi(ModelRestApi):
246
249
  )
247
250
  await self.datamodel.add(params.session, item)
248
251
 
252
+ async def pre_update_merge(self, item, data, params):
253
+ await self._check_user_unique(
254
+ params.session,
255
+ data.get("email", item.email),
256
+ data.get("username", item.username),
257
+ current_id=item.id,
258
+ )
259
+
249
260
  async def post_update(self, item, params):
250
261
  if params.body.password:
251
262
  await g.current_app.sm.update_user(
@@ -255,6 +266,41 @@ class UsersApi(ModelRestApi):
255
266
  )
256
267
  await self.datamodel.add(params.session, item)
257
268
 
269
+ @classmethod
270
+ async def _check_user_unique(
271
+ cls,
272
+ session: SQLASession,
273
+ email: str,
274
+ username: str,
275
+ current_id: int | None = None,
276
+ ):
277
+ """
278
+ Reject a create/update that collides with an existing user.
279
+
280
+ Matching is case-insensitive (mirrors `get_by_email` /
281
+ `get_by_username`), so `A.b@x.com` and `a.b@x.com` are the same
282
+ user and cannot both exist via the API.
283
+
284
+ Args:
285
+ session (SQLASession): The active database session.
286
+ email (str): The email to check for an existing owner.
287
+ username (str): The username to check for an existing owner.
288
+ current_id (int | None, optional): Id of the row being updated, excluded from the conflict check. Defaults to None.
289
+
290
+ Raises:
291
+ fastapi.HTTPException: 400 when another user already owns the email or username.
292
+ """
293
+ user_db = UserDatabase(session, User)
294
+ for existing in (
295
+ await user_db.get_by_email(email),
296
+ await user_db.get_by_username(username),
297
+ ):
298
+ if existing is not None and existing.id != current_id:
299
+ raise fastapi.HTTPException(
300
+ fastapi.status.HTTP_400_BAD_REQUEST,
301
+ ErrorCode.USER_ALREADY_EXISTS,
302
+ )
303
+
258
304
 
259
305
  class RolesApi(ModelRestApi):
260
306
  resource_name = "roles"
@@ -17,7 +17,10 @@ inserts rather than N reads plus N inserts.
17
17
  import collections.abc
18
18
  import typing
19
19
 
20
+ import sqlalchemy
21
+
20
22
  from ....backends.sqla.session import SQLASession
23
+ from ....utils import smart_run
21
24
 
22
25
  if typing.TYPE_CHECKING:
23
26
  from ..models import Role
@@ -43,12 +46,14 @@ async def import_users(
43
46
 
44
47
  The payload's top-level keys are usernames; each value is the user record
45
48
  dict (email, password hash, first/last name, roles list, optional active
46
- flag). Existing usernames are skipped. Password hashes are written
47
- verbatim: callers are responsible for ensuring the source dump matches
48
- the destination's hashing scheme.
49
+ flag). A record is skipped when its username or email case-insensitively
50
+ matches an existing user or an already-imported record, so a case-variant
51
+ of an existing account is never inserted as a second row. Password hashes
52
+ are written verbatim: callers are responsible for ensuring the source dump
53
+ matches the destination's hashing scheme.
49
54
 
50
- Bulk lookups: one `where_in` query for existing usernames and one for
51
- referenced roles, regardless of payload size.
55
+ Bulk lookups: one case-insensitive query for existing usernames/emails and
56
+ one for referenced roles, regardless of payload size.
52
57
 
53
58
  Args:
54
59
  sm (SecurityManager): The owning security manager (provides
@@ -71,17 +76,28 @@ async def import_users(
71
76
  if not payload:
72
77
  return {"imported": [], "skipped": []}
73
78
 
74
- usernames = list(payload.keys())
75
- existing_users = await sm.user_interface.get_many(
76
- session,
77
- {"list_columns": ["username"], "where_in": ("username", usernames)},
79
+ user_model = sm.user_model
80
+ lowered_usernames = [username.lower() for username in payload]
81
+ lowered_emails = [
82
+ (record.get("email") or "").lower() for record in payload.values()
83
+ ]
84
+ statement = sqlalchemy.select(user_model.username, user_model.email).where(
85
+ sqlalchemy.or_(
86
+ sqlalchemy.func.lower(user_model.username).in_(lowered_usernames),
87
+ sqlalchemy.func.lower(user_model.email).in_(lowered_emails),
88
+ )
78
89
  )
79
- existing_usernames = {u.username for u in existing_users}
90
+ existing = await smart_run(session.execute, statement)
91
+ existing_usernames_lc: set[str] = set()
92
+ existing_emails_lc: set[str] = set()
93
+ for username, email in existing.all():
94
+ existing_usernames_lc.add(username.lower())
95
+ existing_emails_lc.add((email or "").lower())
80
96
 
81
97
  per_user_role_names: dict[str, list[str]] = {
82
98
  username: _normalize_role_names(record.get("roles") or [])
83
99
  for username, record in payload.items()
84
- if username not in existing_usernames
100
+ if username.lower() not in existing_usernames_lc
85
101
  }
86
102
  referenced_role_names: set[str] = set()
87
103
  for names in per_user_role_names.values():
@@ -96,10 +112,21 @@ async def import_users(
96
112
 
97
113
  imported: list[str] = []
98
114
  skipped: list[str] = []
115
+ seen_usernames_lc: set[str] = set()
116
+ seen_emails_lc: set[str] = set()
99
117
  for username, record in payload.items():
100
- if username in existing_usernames:
118
+ username_lc = username.lower()
119
+ email_lc = (record.get("email") or "").lower()
120
+ if (
121
+ username_lc in existing_usernames_lc
122
+ or username_lc in seen_usernames_lc
123
+ or email_lc in existing_emails_lc
124
+ or email_lc in seen_emails_lc
125
+ ):
101
126
  skipped.append(username)
102
127
  continue
128
+ seen_usernames_lc.add(username_lc)
129
+ seen_emails_lc.add(email_lc)
103
130
  role_objs = [role_map[name] for name in per_user_role_names[username]]
104
131
  user = sm.user_model(
105
132
  username=username,
@@ -3,10 +3,10 @@
3
3
  import asyncio
4
4
  import unittest.mock
5
5
 
6
+ import click
6
7
  import fastapi_cli.exceptions
7
8
  import pytest
8
9
  import typer
9
-
10
10
  from fastapi_rtk.cli.decorators import (
11
11
  _check_existing_app,
12
12
  check_existing_app_silent,
@@ -102,6 +102,63 @@ class TestEnsureAppLoaded:
102
102
  g.is_cli = False
103
103
 
104
104
 
105
+ class TestCommandRecording:
106
+ """`g.cli_command` holds the full CLI path and is set before the app import."""
107
+
108
+ def test_records_full_command_path_before_import(self, global_setter):
109
+ global_setter("is_cli", False)
110
+
111
+ from fastapi_rtk.globals import g
112
+
113
+ captured = {}
114
+
115
+ def _capture(*args, **kwargs):
116
+ # Grab g.cli_command at import time: the guard runs during this import.
117
+ captured["cli_command"] = g.cli_command
118
+
119
+ root = click.Context(click.Command("fastapi-rtk"), info_name="fastapi-rtk")
120
+ export = click.Context(click.Command("export"), parent=root, info_name="export")
121
+ leaf = click.Context(
122
+ click.Command("licenses"), parent=export, info_name="licenses"
123
+ )
124
+
125
+ with unittest.mock.patch(
126
+ "fastapi_cli.discover.get_import_data", side_effect=_capture
127
+ ):
128
+
129
+ @ensure_app_loaded
130
+ def fn():
131
+ return "done"
132
+
133
+ try:
134
+ with leaf:
135
+ assert fn() == "done"
136
+ assert captured["cli_command"] == "export licenses"
137
+ assert g.cli_command == "export licenses"
138
+ finally:
139
+ g.is_cli = False
140
+ g.cli_command = None
141
+
142
+ def test_command_is_none_without_click_context(self, global_setter):
143
+ global_setter("is_cli", False)
144
+
145
+ from fastapi_rtk.globals import g
146
+
147
+ g.cli_command = "stale"
148
+ with unittest.mock.patch("fastapi_cli.discover.get_import_data"):
149
+
150
+ @ensure_app_loaded
151
+ def fn():
152
+ return "done"
153
+
154
+ try:
155
+ assert fn() == "done"
156
+ assert g.cli_command is None
157
+ finally:
158
+ g.is_cli = False
159
+ g.cli_command = None
160
+
161
+
105
162
  class TestInProcessReusesLoadedApp:
106
163
  """When `g.current_app` is set (in-process, e.g. the app lifespan) the
107
164
  decorators reuse the loaded app: no import, no CLI flag, no table-init.
@@ -11,6 +11,8 @@ can be tested with a constructed UsersApi without going through
11
11
 
12
12
  import types
13
13
 
14
+ import fastapi
15
+ import pytest
14
16
 
15
17
  from fastapi_rtk.security.sqla.apis import (
16
18
  AuthApi,
@@ -161,3 +163,57 @@ class TestAuthApiSchemas:
161
163
  api = object.__new__(AuthApi)
162
164
  schema = api.update_user_schema
163
165
  assert schema is not None
166
+
167
+
168
+ @pytest.mark.asyncio
169
+ class TestUsersApiUniquenessGuard:
170
+ def _seed(self, session, username, email):
171
+ user = User(username=username, email=email, password="h")
172
+ session.add(user)
173
+ session.commit()
174
+ return user
175
+
176
+ def _api(self):
177
+ return object.__new__(UsersApi)
178
+
179
+ async def test_pre_add_rejects_case_variant_email(self, session):
180
+ self._seed(session, "existing", "Foo@X.com")
181
+ item = User(username="brandnew", email="foo@x.com", password="h")
182
+ with pytest.raises(fastapi.HTTPException) as exc:
183
+ await self._api().pre_add(item, types.SimpleNamespace(session=session))
184
+ assert exc.value.status_code == 400
185
+
186
+ async def test_pre_add_rejects_case_variant_username(self, session):
187
+ self._seed(session, "Admin", "admin@x.com")
188
+ item = User(username="admin", email="fresh@x.com", password="h")
189
+ with pytest.raises(fastapi.HTTPException):
190
+ await self._api().pre_add(item, types.SimpleNamespace(session=session))
191
+
192
+ async def test_pre_add_allows_unique_user(self, session):
193
+ self._seed(session, "existing", "foo@x.com")
194
+ item = User(username="brandnew", email="bar@x.com", password="h")
195
+ assert (
196
+ await self._api().pre_add(item, types.SimpleNamespace(session=session))
197
+ is None
198
+ )
199
+
200
+ async def test_pre_update_merge_rejects_other_users_email(self, session):
201
+ self._seed(session, "aa", "a@x.com")
202
+ target = self._seed(session, "bb", "b@x.com")
203
+ with pytest.raises(fastapi.HTTPException):
204
+ await self._api().pre_update_merge(
205
+ target,
206
+ {"email": "A@X.com"},
207
+ types.SimpleNamespace(session=session),
208
+ )
209
+
210
+ async def test_pre_update_merge_allows_own_email(self, session):
211
+ target = self._seed(session, "aa", "A@x.com")
212
+ assert (
213
+ await self._api().pre_update_merge(
214
+ target,
215
+ {"email": "a@x.com"},
216
+ types.SimpleNamespace(session=session),
217
+ )
218
+ is None
219
+ )
@@ -382,6 +382,61 @@ class TestImportData:
382
382
  result = await sm.import_data("users", payload, session=session)
383
383
  assert result["imported"] == ["admin"]
384
384
 
385
+ async def test_import_users_skips_case_variant_username(self, sm, session):
386
+ base = {
387
+ "password": "$2b$12$x",
388
+ "first_name": "",
389
+ "last_name": "",
390
+ "roles": [],
391
+ }
392
+ await sm.import_data(
393
+ "users",
394
+ {"Admin": {"email": "a@x.com", "username": "Admin", **base}},
395
+ session=session,
396
+ )
397
+ result = await sm.import_data(
398
+ "users",
399
+ {"admin": {"email": "different@x.com", "username": "admin", **base}},
400
+ session=session,
401
+ )
402
+ assert result["imported"] == []
403
+ assert result["skipped"] == ["admin"]
404
+
405
+ async def test_import_users_skips_case_variant_email(self, sm, session):
406
+ base = {
407
+ "password": "$2b$12$x",
408
+ "first_name": "",
409
+ "last_name": "",
410
+ "roles": [],
411
+ }
412
+ await sm.import_data(
413
+ "users",
414
+ {"admin": {"email": "Admin@Example.com", "username": "admin", **base}},
415
+ session=session,
416
+ )
417
+ result = await sm.import_data(
418
+ "users",
419
+ {"other": {"email": "admin@example.com", "username": "other", **base}},
420
+ session=session,
421
+ )
422
+ assert result["imported"] == []
423
+ assert result["skipped"] == ["other"]
424
+
425
+ async def test_import_users_dedups_case_variant_within_payload(self, sm, session):
426
+ base = {
427
+ "password": "$2b$12$x",
428
+ "first_name": "",
429
+ "last_name": "",
430
+ "roles": [],
431
+ }
432
+ payload = {
433
+ "first": {"email": "Dup@x.com", "username": "first", **base},
434
+ "second": {"email": "dup@x.com", "username": "second", **base},
435
+ }
436
+ result = await sm.import_data("users", payload, session=session)
437
+ assert result["imported"] == ["first"]
438
+ assert result["skipped"] == ["second"]
439
+
385
440
 
386
441
  class TestShouldReraise:
387
442
  """Behavior of the `_should_reraise` helper that backs `raise_exception`."""
@@ -1 +0,0 @@
1
- __version__ = "2.6.1"
File without changes
File without changes
File without changes