fastapi-rtk 2.7.0__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.7.0 → 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.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/cli/commands/security.py +1 -1
  4. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/const.py +1 -0
  5. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/security/sqla/apis.py +46 -0
  6. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/security/sqla/security_manager/_import.py +39 -12
  7. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/unit/security/sqla/test_apis.py +56 -0
  8. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/unit/security/sqla/test_security_manager.py +55 -0
  9. fastapi_rtk-2.7.0/fastapi_rtk/_version.py +0 -1
  10. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/.gitignore +0 -0
  11. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/LICENSE +0 -0
  12. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/README.md +0 -0
  13. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/__init__.py +0 -0
  14. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/api/__init__.py +0 -0
  15. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/api/base_api.py +0 -0
  16. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/api/model_rest_api/__init__.py +0 -0
  17. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/api/model_rest_api/_add_mixin.py +0 -0
  18. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/api/model_rest_api/_bulk_mixin.py +0 -0
  19. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/api/model_rest_api/_columns_mixin.py +0 -0
  20. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/api/model_rest_api/_delete_mixin.py +0 -0
  21. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/api/model_rest_api/_download_mixin.py +0 -0
  22. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/api/model_rest_api/_edit_mixin.py +0 -0
  23. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/api/model_rest_api/_file_mixin.py +0 -0
  24. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/api/model_rest_api/_info_mixin.py +0 -0
  25. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/api/model_rest_api/_list_mixin.py +0 -0
  26. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/api/model_rest_api/_params.py +0 -0
  27. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/api/model_rest_api/_show_mixin.py +0 -0
  28. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/api/model_rest_api/_typed_base.py +0 -0
  29. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/api/model_rest_api/_upload_mixin.py +0 -0
  30. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/apis/__init__.py +0 -0
  31. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/apis/info.py +0 -0
  32. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/apis/license.py +0 -0
  33. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/auth/__init__.py +0 -0
  34. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/auth/auth.py +0 -0
  35. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/auth/hashers/__init__.py +0 -0
  36. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/auth/hashers/pbkdf2.py +0 -0
  37. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/auth/hashers/scrypt.py +0 -0
  38. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/auth/ldap.py +0 -0
  39. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/auth/password_helpers/__init__.py +0 -0
  40. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/auth/password_helpers/fab.py +0 -0
  41. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/auth/strategies/__init__.py +0 -0
  42. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/auth/strategies/config.py +0 -0
  43. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/auth/strategies/db.py +0 -0
  44. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/auth/strategies/jwt.py +0 -0
  45. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/backends/__init__.py +0 -0
  46. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/backends/generic/__init__.py +0 -0
  47. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/backends/generic/column.py +0 -0
  48. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/backends/generic/db.py +0 -0
  49. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/backends/generic/exceptions.py +0 -0
  50. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/backends/generic/filters.py +0 -0
  51. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/backends/generic/interface.py +0 -0
  52. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/backends/generic/model.py +0 -0
  53. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/backends/generic/session.py +0 -0
  54. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/backends/sqla/__init__.py +0 -0
  55. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/backends/sqla/column.py +0 -0
  56. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/backends/sqla/db.py +0 -0
  57. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/backends/sqla/exceptions.py +0 -0
  58. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/backends/sqla/extensions/__init__.py +0 -0
  59. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/backends/sqla/extensions/audit/__init__.py +0 -0
  60. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/backends/sqla/extensions/audit/_decorators_mixin.py +0 -0
  61. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/backends/sqla/extensions/audit/_lifecycle_mixin.py +0 -0
  62. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/backends/sqla/extensions/audit/_logger.py +0 -0
  63. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/backends/sqla/extensions/audit/_processing_mixin.py +0 -0
  64. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/backends/sqla/extensions/audit/audit.py +0 -0
  65. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/backends/sqla/extensions/audit/types.py +0 -0
  66. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/backends/sqla/extensions/geoalchemy2/__init__.py +0 -0
  67. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/backends/sqla/extensions/geoalchemy2/filters.py +0 -0
  68. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/backends/sqla/extensions/geoalchemy2/geometry_converter.py +0 -0
  69. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/backends/sqla/filters.py +0 -0
  70. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/backends/sqla/interface.py +0 -0
  71. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/backends/sqla/model.py +0 -0
  72. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/backends/sqla/session.py +0 -0
  73. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/bases/__init__.py +0 -0
  74. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/bases/db.py +0 -0
  75. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/bases/file_manager.py +0 -0
  76. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/bases/filter.py +0 -0
  77. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/bases/interface.py +0 -0
  78. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/bases/model.py +0 -0
  79. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/bases/session.py +0 -0
  80. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/cli/__init__.py +0 -0
  81. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/cli/cli.py +0 -0
  82. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/cli/commands/__init__.py +0 -0
  83. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/cli/commands/_security_crypto.py +0 -0
  84. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/cli/commands/db/__init__.py +0 -0
  85. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/cli/commands/db/templates/fastapi/README +0 -0
  86. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/cli/commands/db/templates/fastapi/alembic.ini.mako +0 -0
  87. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/cli/commands/db/templates/fastapi/env.py +0 -0
  88. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/cli/commands/db/templates/fastapi/script.py.mako +0 -0
  89. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/cli/commands/db/templates/fastapi-multidb/README +0 -0
  90. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/cli/commands/db/templates/fastapi-multidb/alembic.ini.mako +0 -0
  91. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/cli/commands/db/templates/fastapi-multidb/env.py +0 -0
  92. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/cli/commands/db/templates/fastapi-multidb/script.py.mako +0 -0
  93. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/cli/commands/export.py +0 -0
  94. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/cli/commands/translate.py +0 -0
  95. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/cli/const.py +0 -0
  96. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/cli/decorators.py +0 -0
  97. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/cli/types.py +0 -0
  98. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/cli/utils.py +0 -0
  99. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/config.py +0 -0
  100. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/db.py +0 -0
  101. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/decorators.py +0 -0
  102. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/dependencies.py +0 -0
  103. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/exceptions.py +0 -0
  104. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/fastapi_react_toolkit.py +0 -0
  105. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/file_managers/__init__.py +0 -0
  106. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/file_managers/file_manager.py +0 -0
  107. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/file_managers/image_manager.py +0 -0
  108. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/file_managers/s3_file_manager.py +0 -0
  109. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/file_managers/s3_image_manager.py +0 -0
  110. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/filters.py +0 -0
  111. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/globals.py +0 -0
  112. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/lang/__init__.py +0 -0
  113. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/lang/babel/__init__.py +0 -0
  114. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/lang/babel/cli.py +0 -0
  115. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/lang/babel/config.py +0 -0
  116. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/lang/babel.cfg +0 -0
  117. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/lang/lazy_text.py +0 -0
  118. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/lang/messages.pot +0 -0
  119. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/lang/translations/de/LC_MESSAGES/messages.mo +0 -0
  120. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/lang/translations/de/LC_MESSAGES/messages.po +0 -0
  121. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/lang/translations/en/LC_MESSAGES/messages.mo +0 -0
  122. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/lang/translations/en/LC_MESSAGES/messages.po +0 -0
  123. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/manager.py +0 -0
  124. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/middlewares.py +0 -0
  125. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/mixins.py +0 -0
  126. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/models.py +0 -0
  127. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/py.typed +0 -0
  128. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/registry.py +0 -0
  129. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/routers.py +0 -0
  130. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/schemas.py +0 -0
  131. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/security/__init__.py +0 -0
  132. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/security/sqla/__init__.py +0 -0
  133. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/security/sqla/models.py +0 -0
  134. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/security/sqla/security_manager/__init__.py +0 -0
  135. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/security/sqla/security_manager/_association_mixin.py +0 -0
  136. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/security/sqla/security_manager/_builtin_role_mixin.py +0 -0
  137. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/security/sqla/security_manager/_cleanup_mixin.py +0 -0
  138. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/security/sqla/security_manager/_exception_filters.py +0 -0
  139. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/security/sqla/security_manager/_export.py +0 -0
  140. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/security/sqla/security_manager/_role_mixin.py +0 -0
  141. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/security/sqla/security_manager/_user_mixin.py +0 -0
  142. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/setting.py +0 -0
  143. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/types.py +0 -0
  144. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/utils/__init__.py +0 -0
  145. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/utils/async_task_runner.py +0 -0
  146. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/utils/class_factory.py +0 -0
  147. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/utils/csv_json_converter.py +0 -0
  148. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/utils/deep_merge.py +0 -0
  149. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/utils/extender_mixin.py +0 -0
  150. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/utils/flask_appbuilder_utils.py +0 -0
  151. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/utils/formatter.py +0 -0
  152. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/utils/hooks.py +0 -0
  153. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/utils/lazy.py +0 -0
  154. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/utils/merge_schema.py +0 -0
  155. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/utils/multiple_async_contexts.py +0 -0
  156. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/utils/pydantic.py +0 -0
  157. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/utils/run_utils.py +0 -0
  158. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/utils/self_dependencies.py +0 -0
  159. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/utils/smartdefaultdict.py +0 -0
  160. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/utils/sqla.py +0 -0
  161. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/utils/timezone.py +0 -0
  162. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/utils/update_signature.py +0 -0
  163. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/utils/use_default_when_none.py +0 -0
  164. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/fastapi_rtk/version.py +0 -0
  165. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/pyproject.toml +0 -0
  166. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/conftest.py +0 -0
  167. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/docker-compose.gis.yml +0 -0
  168. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/integration/__init__.py +0 -0
  169. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/integration/api/__init__.py +0 -0
  170. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/integration/api/test_model_rest_api.py +0 -0
  171. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/integration/apis/__init__.py +0 -0
  172. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/integration/apis/test_info_callable_schema.py +0 -0
  173. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/integration/apis/test_info_contract.py +0 -0
  174. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/integration/auth/__init__.py +0 -0
  175. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/integration/auth/test_auth_flow.py +0 -0
  176. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/integration/auth/test_oauth.py +0 -0
  177. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/integration/conftest.py +0 -0
  178. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/integration/file_managers/__init__.py +0 -0
  179. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/integration/file_managers/test_file_routes.py +0 -0
  180. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/integration/security/__init__.py +0 -0
  181. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/integration/security/sqla/__init__.py +0 -0
  182. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/integration/security/sqla/test_security_manager.py +0 -0
  183. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/integration/test_app_smoke.py +0 -0
  184. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/unit/__init__.py +0 -0
  185. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/unit/api/__init__.py +0 -0
  186. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/unit/api/conftest.py +0 -0
  187. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/unit/api/model_rest_api/__init__.py +0 -0
  188. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/unit/api/model_rest_api/test_columns.py +0 -0
  189. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/unit/api/model_rest_api/test_file_mixin.py +0 -0
  190. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/unit/api/model_rest_api/test_init.py +0 -0
  191. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/unit/api/model_rest_api/test_typed_base_consistency.py +0 -0
  192. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/unit/api/test_base_api.py +0 -0
  193. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/unit/apis/__init__.py +0 -0
  194. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/unit/apis/test_info.py +0 -0
  195. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/unit/apis/test_license.py +0 -0
  196. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/unit/auth/__init__.py +0 -0
  197. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/unit/auth/hashers/__init__.py +0 -0
  198. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/unit/auth/hashers/test_pbkdf2.py +0 -0
  199. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/unit/auth/hashers/test_scrypt.py +0 -0
  200. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/unit/auth/password_helpers/__init__.py +0 -0
  201. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/unit/auth/password_helpers/test_fab.py +0 -0
  202. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/unit/auth/strategies/__init__.py +0 -0
  203. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/unit/auth/strategies/test_config.py +0 -0
  204. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/unit/auth/strategies/test_db.py +0 -0
  205. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/unit/auth/strategies/test_jwt.py +0 -0
  206. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/unit/auth/test_auth.py +0 -0
  207. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/unit/backends/__init__.py +0 -0
  208. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/unit/backends/generic/__init__.py +0 -0
  209. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/unit/backends/generic/test_column.py +0 -0
  210. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/unit/backends/generic/test_db.py +0 -0
  211. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/unit/backends/generic/test_exceptions.py +0 -0
  212. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/unit/backends/generic/test_filters.py +0 -0
  213. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/unit/backends/generic/test_interface.py +0 -0
  214. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/unit/backends/generic/test_model.py +0 -0
  215. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/unit/backends/generic/test_session.py +0 -0
  216. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/unit/backends/sqla/__init__.py +0 -0
  217. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/unit/backends/sqla/conftest.py +0 -0
  218. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/unit/backends/sqla/test_audit.py +0 -0
  219. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/unit/backends/sqla/test_audit_types.py +0 -0
  220. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/unit/backends/sqla/test_column.py +0 -0
  221. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/unit/backends/sqla/test_db.py +0 -0
  222. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/unit/backends/sqla/test_filters.py +0 -0
  223. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/unit/backends/sqla/test_geoalchemy2.py +0 -0
  224. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/unit/backends/sqla/test_interface.py +0 -0
  225. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/unit/backends/sqla/test_model.py +0 -0
  226. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/unit/backends/sqla/test_session.py +0 -0
  227. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/unit/bases/__init__.py +0 -0
  228. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/unit/bases/test_db.py +0 -0
  229. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/unit/bases/test_file_manager.py +0 -0
  230. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/unit/bases/test_filter.py +0 -0
  231. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/unit/bases/test_interface.py +0 -0
  232. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/unit/bases/test_model.py +0 -0
  233. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/unit/bases/test_session.py +0 -0
  234. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/unit/cli/__init__.py +0 -0
  235. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/unit/cli/commands/__init__.py +0 -0
  236. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/unit/cli/commands/conftest.py +0 -0
  237. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/unit/cli/commands/test_db.py +0 -0
  238. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/unit/cli/commands/test_export.py +0 -0
  239. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/unit/cli/commands/test_security.py +0 -0
  240. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/unit/cli/commands/test_security_crypto.py +0 -0
  241. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/unit/cli/commands/test_translate.py +0 -0
  242. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/unit/cli/conftest.py +0 -0
  243. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/unit/cli/test_cli.py +0 -0
  244. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/unit/cli/test_decorators.py +0 -0
  245. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/unit/cli/test_types.py +0 -0
  246. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/unit/cli/test_utils.py +0 -0
  247. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/unit/file_managers/__init__.py +0 -0
  248. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/unit/file_managers/conftest.py +0 -0
  249. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/unit/file_managers/test_file_manager.py +0 -0
  250. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/unit/file_managers/test_image_manager.py +0 -0
  251. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/unit/file_managers/test_s3_file_manager.py +0 -0
  252. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/unit/file_managers/test_s3_image_manager.py +0 -0
  253. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/unit/lang/__init__.py +0 -0
  254. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/unit/lang/test_babel_cli.py +0 -0
  255. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/unit/lang/test_babel_config.py +0 -0
  256. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/unit/lang/test_lazy_text.py +0 -0
  257. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/unit/security/__init__.py +0 -0
  258. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/unit/security/sqla/__init__.py +0 -0
  259. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/unit/security/sqla/conftest.py +0 -0
  260. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/unit/security/sqla/test_models.py +0 -0
  261. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/unit/test_config.py +0 -0
  262. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/unit/test_const.py +0 -0
  263. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/unit/test_db.py +0 -0
  264. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/unit/test_decorators.py +0 -0
  265. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/unit/test_dependencies.py +0 -0
  266. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/unit/test_exceptions.py +0 -0
  267. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/unit/test_fastapi_react_toolkit.py +0 -0
  268. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/unit/test_filters.py +0 -0
  269. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/unit/test_globals.py +0 -0
  270. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/unit/test_manager.py +0 -0
  271. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/unit/test_middlewares.py +0 -0
  272. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/unit/test_mixins.py +0 -0
  273. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/unit/test_models.py +0 -0
  274. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/unit/test_registry.py +0 -0
  275. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/unit/test_routers.py +0 -0
  276. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/unit/test_schemas.py +0 -0
  277. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/unit/test_setting.py +0 -0
  278. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/unit/test_types.py +0 -0
  279. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/unit/test_version.py +0 -0
  280. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/unit/utils/__init__.py +0 -0
  281. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/unit/utils/test_async_task_runner.py +0 -0
  282. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/unit/utils/test_class_factory.py +0 -0
  283. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/unit/utils/test_csv_json_converter.py +0 -0
  284. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/unit/utils/test_deep_merge.py +0 -0
  285. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/unit/utils/test_extender_mixin.py +0 -0
  286. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/unit/utils/test_flask_appbuilder_utils.py +0 -0
  287. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/unit/utils/test_formatter.py +0 -0
  288. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/unit/utils/test_hooks.py +0 -0
  289. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/unit/utils/test_lazy.py +0 -0
  290. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/unit/utils/test_merge_schema.py +0 -0
  291. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/unit/utils/test_multiple_async_contexts.py +0 -0
  292. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/unit/utils/test_pydantic.py +0 -0
  293. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/unit/utils/test_run_utils.py +0 -0
  294. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/unit/utils/test_self_dependencies.py +0 -0
  295. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/unit/utils/test_smartdefaultdict.py +0 -0
  296. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/unit/utils/test_sqla.py +0 -0
  297. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/unit/utils/test_timezone.py +0 -0
  298. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/unit/utils/test_update_signature.py +0 -0
  299. {fastapi_rtk-2.7.0 → fastapi_rtk-2.7.1}/tests/unit/utils/test_use_default_when_none.py +0 -0
  300. {fastapi_rtk-2.7.0 → 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.7.0
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,
@@ -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"
@@ -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,
@@ -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.7.0"
File without changes
File without changes
File without changes