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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (300) hide show
  1. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/PKG-INFO +1 -1
  2. fastapi_rtk-2.11.2/fastapi_rtk/_version.py +1 -0
  3. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/api/model_rest_api/_columns_mixin.py +32 -4
  4. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/api/model_rest_api/_info_mixin.py +1 -0
  5. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/api/model_rest_api/_typed_base.py +2 -0
  6. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/backends/sqla/filters.py +34 -16
  7. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/security/sqla/models.py +9 -3
  8. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/api/model_rest_api/test_columns.py +343 -5
  9. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/backends/sqla/test_filters.py +52 -1
  10. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/security/sqla/test_models.py +7 -0
  11. fastapi_rtk-2.11.0/fastapi_rtk/_version.py +0 -1
  12. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/.gitignore +0 -0
  13. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/LICENSE +0 -0
  14. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/README.md +0 -0
  15. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/__init__.py +0 -0
  16. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/api/__init__.py +0 -0
  17. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/api/base_api.py +0 -0
  18. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/api/model_rest_api/__init__.py +0 -0
  19. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/api/model_rest_api/_add_mixin.py +0 -0
  20. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/api/model_rest_api/_bulk_mixin.py +0 -0
  21. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/api/model_rest_api/_delete_mixin.py +0 -0
  22. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/api/model_rest_api/_download_mixin.py +0 -0
  23. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/api/model_rest_api/_edit_mixin.py +0 -0
  24. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/api/model_rest_api/_file_mixin.py +0 -0
  25. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/api/model_rest_api/_list_mixin.py +0 -0
  26. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/api/model_rest_api/_params.py +0 -0
  27. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/api/model_rest_api/_show_mixin.py +0 -0
  28. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/api/model_rest_api/_upload_mixin.py +0 -0
  29. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/apis/__init__.py +0 -0
  30. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/apis/info.py +0 -0
  31. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/apis/license.py +0 -0
  32. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/auth/__init__.py +0 -0
  33. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/auth/auth.py +0 -0
  34. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/auth/hashers/__init__.py +0 -0
  35. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/auth/hashers/pbkdf2.py +0 -0
  36. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/auth/hashers/scrypt.py +0 -0
  37. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/auth/ldap.py +0 -0
  38. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/auth/password_helpers/__init__.py +0 -0
  39. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/auth/password_helpers/fab.py +0 -0
  40. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/auth/strategies/__init__.py +0 -0
  41. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/auth/strategies/config.py +0 -0
  42. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/auth/strategies/db.py +0 -0
  43. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/auth/strategies/jwt.py +0 -0
  44. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/backends/__init__.py +0 -0
  45. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/backends/generic/__init__.py +0 -0
  46. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/backends/generic/column.py +0 -0
  47. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/backends/generic/db.py +0 -0
  48. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/backends/generic/exceptions.py +0 -0
  49. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/backends/generic/filters.py +0 -0
  50. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/backends/generic/interface.py +0 -0
  51. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/backends/generic/model.py +0 -0
  52. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/backends/generic/session.py +0 -0
  53. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/backends/sqla/__init__.py +0 -0
  54. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/backends/sqla/column.py +0 -0
  55. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/backends/sqla/db.py +0 -0
  56. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/backends/sqla/exceptions.py +0 -0
  57. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/backends/sqla/extensions/__init__.py +0 -0
  58. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/backends/sqla/extensions/audit/__init__.py +0 -0
  59. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/backends/sqla/extensions/audit/_decorators_mixin.py +0 -0
  60. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/backends/sqla/extensions/audit/_lifecycle_mixin.py +0 -0
  61. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/backends/sqla/extensions/audit/_logger.py +0 -0
  62. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/backends/sqla/extensions/audit/_processing_mixin.py +0 -0
  63. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/backends/sqla/extensions/audit/audit.py +0 -0
  64. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/backends/sqla/extensions/audit/types.py +0 -0
  65. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/backends/sqla/extensions/geoalchemy2/__init__.py +0 -0
  66. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/backends/sqla/extensions/geoalchemy2/filters.py +0 -0
  67. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/backends/sqla/extensions/geoalchemy2/geometry_converter.py +0 -0
  68. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/backends/sqla/interface.py +0 -0
  69. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/backends/sqla/model.py +0 -0
  70. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/backends/sqla/session.py +0 -0
  71. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/bases/__init__.py +0 -0
  72. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/bases/db.py +0 -0
  73. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/bases/file_manager.py +0 -0
  74. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/bases/filter.py +0 -0
  75. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/bases/interface.py +0 -0
  76. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/bases/model.py +0 -0
  77. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/bases/session.py +0 -0
  78. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/cli/__init__.py +0 -0
  79. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/cli/cli.py +0 -0
  80. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/cli/commands/__init__.py +0 -0
  81. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/cli/commands/_security_crypto.py +0 -0
  82. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/cli/commands/db/__init__.py +0 -0
  83. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/cli/commands/db/templates/fastapi/README +0 -0
  84. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/cli/commands/db/templates/fastapi/alembic.ini.mako +0 -0
  85. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/cli/commands/db/templates/fastapi/env.py +0 -0
  86. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/cli/commands/db/templates/fastapi/script.py.mako +0 -0
  87. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/cli/commands/db/templates/fastapi-multidb/README +0 -0
  88. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/cli/commands/db/templates/fastapi-multidb/alembic.ini.mako +0 -0
  89. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/cli/commands/db/templates/fastapi-multidb/env.py +0 -0
  90. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/cli/commands/db/templates/fastapi-multidb/script.py.mako +0 -0
  91. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/cli/commands/export.py +0 -0
  92. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/cli/commands/security.py +0 -0
  93. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/cli/commands/translate.py +0 -0
  94. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/cli/const.py +0 -0
  95. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/cli/decorators.py +0 -0
  96. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/cli/types.py +0 -0
  97. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/cli/utils.py +0 -0
  98. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/config.py +0 -0
  99. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/const.py +0 -0
  100. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/db.py +0 -0
  101. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/decorators.py +0 -0
  102. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/dependencies.py +0 -0
  103. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/exceptions.py +0 -0
  104. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/fastapi_react_toolkit.py +0 -0
  105. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/file_managers/__init__.py +0 -0
  106. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/file_managers/file_manager.py +0 -0
  107. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/file_managers/image_manager.py +0 -0
  108. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/file_managers/s3_file_manager.py +0 -0
  109. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/file_managers/s3_image_manager.py +0 -0
  110. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/filters.py +0 -0
  111. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/globals.py +0 -0
  112. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/lang/__init__.py +0 -0
  113. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/lang/babel/__init__.py +0 -0
  114. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/lang/babel/cli.py +0 -0
  115. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/lang/babel/config.py +0 -0
  116. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/lang/babel.cfg +0 -0
  117. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/lang/lazy_text.py +0 -0
  118. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/lang/messages.pot +0 -0
  119. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/lang/translations/de/LC_MESSAGES/messages.mo +0 -0
  120. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/lang/translations/de/LC_MESSAGES/messages.po +0 -0
  121. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/lang/translations/en/LC_MESSAGES/messages.mo +0 -0
  122. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/lang/translations/en/LC_MESSAGES/messages.po +0 -0
  123. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/manager.py +0 -0
  124. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/middlewares.py +0 -0
  125. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/mixins.py +0 -0
  126. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/models.py +0 -0
  127. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/py.typed +0 -0
  128. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/registry.py +0 -0
  129. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/routers.py +0 -0
  130. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/schemas.py +0 -0
  131. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/security/__init__.py +0 -0
  132. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/security/sqla/__init__.py +0 -0
  133. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/security/sqla/apis.py +0 -0
  134. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/security/sqla/security_manager/__init__.py +0 -0
  135. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/security/sqla/security_manager/_association_mixin.py +0 -0
  136. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/security/sqla/security_manager/_builtin_role_mixin.py +0 -0
  137. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/security/sqla/security_manager/_cleanup_mixin.py +0 -0
  138. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/security/sqla/security_manager/_exception_filters.py +0 -0
  139. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/security/sqla/security_manager/_export.py +0 -0
  140. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/security/sqla/security_manager/_import.py +0 -0
  141. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/security/sqla/security_manager/_role_mixin.py +0 -0
  142. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/security/sqla/security_manager/_user_mixin.py +0 -0
  143. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/setting.py +0 -0
  144. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/types.py +0 -0
  145. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/utils/__init__.py +0 -0
  146. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/utils/async_task_runner.py +0 -0
  147. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/utils/class_factory.py +0 -0
  148. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/utils/csv_json_converter.py +0 -0
  149. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/utils/deep_merge.py +0 -0
  150. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/utils/extender_mixin.py +0 -0
  151. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/utils/flask_appbuilder_utils.py +0 -0
  152. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/utils/formatter.py +0 -0
  153. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/utils/hooks.py +0 -0
  154. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/utils/lazy.py +0 -0
  155. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/utils/merge_schema.py +0 -0
  156. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/utils/multiple_async_contexts.py +0 -0
  157. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/utils/pydantic.py +0 -0
  158. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/utils/run_utils.py +0 -0
  159. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/utils/self_dependencies.py +0 -0
  160. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/utils/smartdefaultdict.py +0 -0
  161. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/utils/sqla.py +0 -0
  162. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/utils/timezone.py +0 -0
  163. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/utils/update_signature.py +0 -0
  164. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/utils/use_default_when_none.py +0 -0
  165. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/fastapi_rtk/version.py +0 -0
  166. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/pyproject.toml +0 -0
  167. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/conftest.py +0 -0
  168. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/docker-compose.gis.yml +0 -0
  169. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/integration/__init__.py +0 -0
  170. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/integration/api/__init__.py +0 -0
  171. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/integration/api/test_model_rest_api.py +0 -0
  172. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/integration/apis/__init__.py +0 -0
  173. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/integration/apis/test_info_callable_schema.py +0 -0
  174. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/integration/apis/test_info_contract.py +0 -0
  175. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/integration/auth/__init__.py +0 -0
  176. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/integration/auth/test_auth_flow.py +0 -0
  177. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/integration/auth/test_oauth.py +0 -0
  178. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/integration/conftest.py +0 -0
  179. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/integration/file_managers/__init__.py +0 -0
  180. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/integration/file_managers/test_file_routes.py +0 -0
  181. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/integration/security/__init__.py +0 -0
  182. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/integration/security/sqla/__init__.py +0 -0
  183. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/integration/security/sqla/test_security_manager.py +0 -0
  184. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/integration/test_app_smoke.py +0 -0
  185. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/__init__.py +0 -0
  186. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/api/__init__.py +0 -0
  187. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/api/conftest.py +0 -0
  188. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/api/model_rest_api/__init__.py +0 -0
  189. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/api/model_rest_api/test_file_mixin.py +0 -0
  190. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/api/model_rest_api/test_init.py +0 -0
  191. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/api/model_rest_api/test_typed_base_consistency.py +0 -0
  192. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/api/test_base_api.py +0 -0
  193. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/apis/__init__.py +0 -0
  194. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/apis/test_info.py +0 -0
  195. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/apis/test_license.py +0 -0
  196. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/auth/__init__.py +0 -0
  197. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/auth/hashers/__init__.py +0 -0
  198. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/auth/hashers/test_pbkdf2.py +0 -0
  199. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/auth/hashers/test_scrypt.py +0 -0
  200. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/auth/password_helpers/__init__.py +0 -0
  201. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/auth/password_helpers/test_fab.py +0 -0
  202. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/auth/strategies/__init__.py +0 -0
  203. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/auth/strategies/test_config.py +0 -0
  204. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/auth/strategies/test_db.py +0 -0
  205. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/auth/strategies/test_jwt.py +0 -0
  206. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/auth/test_auth.py +0 -0
  207. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/backends/__init__.py +0 -0
  208. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/backends/generic/__init__.py +0 -0
  209. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/backends/generic/test_column.py +0 -0
  210. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/backends/generic/test_db.py +0 -0
  211. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/backends/generic/test_exceptions.py +0 -0
  212. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/backends/generic/test_filters.py +0 -0
  213. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/backends/generic/test_interface.py +0 -0
  214. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/backends/generic/test_model.py +0 -0
  215. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/backends/generic/test_session.py +0 -0
  216. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/backends/sqla/__init__.py +0 -0
  217. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/backends/sqla/conftest.py +0 -0
  218. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/backends/sqla/test_audit.py +0 -0
  219. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/backends/sqla/test_audit_types.py +0 -0
  220. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/backends/sqla/test_column.py +0 -0
  221. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/backends/sqla/test_db.py +0 -0
  222. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/backends/sqla/test_geoalchemy2.py +0 -0
  223. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/backends/sqla/test_interface.py +0 -0
  224. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/backends/sqla/test_model.py +0 -0
  225. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/backends/sqla/test_session.py +0 -0
  226. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/bases/__init__.py +0 -0
  227. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/bases/test_db.py +0 -0
  228. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/bases/test_file_manager.py +0 -0
  229. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/bases/test_filter.py +0 -0
  230. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/bases/test_interface.py +0 -0
  231. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/bases/test_model.py +0 -0
  232. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/bases/test_session.py +0 -0
  233. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/cli/__init__.py +0 -0
  234. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/cli/commands/__init__.py +0 -0
  235. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/cli/commands/conftest.py +0 -0
  236. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/cli/commands/test_db.py +0 -0
  237. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/cli/commands/test_export.py +0 -0
  238. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/cli/commands/test_security.py +0 -0
  239. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/cli/commands/test_security_crypto.py +0 -0
  240. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/cli/commands/test_translate.py +0 -0
  241. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/cli/conftest.py +0 -0
  242. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/cli/test_cli.py +0 -0
  243. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/cli/test_decorators.py +0 -0
  244. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/cli/test_types.py +0 -0
  245. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/cli/test_utils.py +0 -0
  246. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/file_managers/__init__.py +0 -0
  247. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/file_managers/conftest.py +0 -0
  248. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/file_managers/test_file_manager.py +0 -0
  249. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/file_managers/test_image_manager.py +0 -0
  250. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/file_managers/test_s3_file_manager.py +0 -0
  251. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/file_managers/test_s3_image_manager.py +0 -0
  252. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/lang/__init__.py +0 -0
  253. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/lang/test_babel_cli.py +0 -0
  254. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/lang/test_babel_config.py +0 -0
  255. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/lang/test_lazy_text.py +0 -0
  256. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/security/__init__.py +0 -0
  257. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/security/sqla/__init__.py +0 -0
  258. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/security/sqla/conftest.py +0 -0
  259. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/security/sqla/test_apis.py +0 -0
  260. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/security/sqla/test_security_manager.py +0 -0
  261. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/test_config.py +0 -0
  262. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/test_const.py +0 -0
  263. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/test_db.py +0 -0
  264. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/test_decorators.py +0 -0
  265. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/test_dependencies.py +0 -0
  266. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/test_exceptions.py +0 -0
  267. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/test_fastapi_react_toolkit.py +0 -0
  268. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/test_filters.py +0 -0
  269. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/test_globals.py +0 -0
  270. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/test_manager.py +0 -0
  271. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/test_middlewares.py +0 -0
  272. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/test_mixins.py +0 -0
  273. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/test_models.py +0 -0
  274. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/test_registry.py +0 -0
  275. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/test_routers.py +0 -0
  276. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/test_schemas.py +0 -0
  277. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/test_setting.py +0 -0
  278. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/test_types.py +0 -0
  279. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/test_version.py +0 -0
  280. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/utils/__init__.py +0 -0
  281. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/utils/test_async_task_runner.py +0 -0
  282. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/utils/test_class_factory.py +0 -0
  283. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/utils/test_csv_json_converter.py +0 -0
  284. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/utils/test_deep_merge.py +0 -0
  285. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/utils/test_extender_mixin.py +0 -0
  286. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/utils/test_flask_appbuilder_utils.py +0 -0
  287. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/utils/test_formatter.py +0 -0
  288. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/utils/test_hooks.py +0 -0
  289. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/utils/test_lazy.py +0 -0
  290. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/utils/test_merge_schema.py +0 -0
  291. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/utils/test_multiple_async_contexts.py +0 -0
  292. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/utils/test_pydantic.py +0 -0
  293. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/utils/test_run_utils.py +0 -0
  294. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/utils/test_self_dependencies.py +0 -0
  295. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/utils/test_smartdefaultdict.py +0 -0
  296. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/utils/test_sqla.py +0 -0
  297. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/utils/test_timezone.py +0 -0
  298. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/utils/test_update_signature.py +0 -0
  299. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/tests/unit/utils/test_use_default_when_none.py +0 -0
  300. {fastapi_rtk-2.11.0 → fastapi_rtk-2.11.2}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastapi-rtk
3
- Version: 2.11.0
3
+ Version: 2.11.2
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.11.2"
@@ -47,6 +47,8 @@ from ...utils import (
47
47
  )
48
48
  from ._typed_base import _TypedModelRestApi
49
49
 
50
+ _UNUSED_SCHEMA_KEYS = ("anyOf", "default", "$ref", "const")
51
+
50
52
 
51
53
  class ColumnsMixin(_TypedModelRestApi):
52
54
  """
@@ -546,6 +548,8 @@ class ColumnsMixin(_TypedModelRestApi):
546
548
  [ColumnInfo | ColumnEnumInfo | ColumnRelationInfo | None], bool
547
549
  ],
548
550
  ] = False,
551
+ *,
552
+ mark_non_nullable_required: bool = False,
549
553
  ):
550
554
  """
551
555
  Generate the JSONForms schema for the API.
@@ -555,6 +559,7 @@ class ColumnsMixin(_TypedModelRestApi):
555
559
  info_columns (list[ColumnInfo | ColumnEnumInfo | ColumnRelationInfo]): The list of columns to include in the JSONForms schema.
556
560
  use_enum (bool | Callable[[ColumnInfo | ColumnEnumInfo | ColumnRelationInfo], bool] | dict[str, bool | Callable[[ColumnInfo | ColumnEnumInfo | ColumnRelationInfo], bool]], optional): A flag or a callable or a dictionary to determine whether to use enum instead of oneOf. Defaults to False.
557
561
  list_as_object (bool | Callable[[ColumnInfo | ColumnEnumInfo | ColumnRelationInfo | None], bool] | dict[str, bool | Callable[[ColumnInfo | ColumnEnumInfo | ColumnRelationInfo | None], bool]], optional): A flag or a callable or a dictionary to determine whether to render a list column as a single `object` control instead of the per-item `array` table. The callable receives the column's `ColumnInfo` when available, else `None`. Defaults to False.
562
+ mark_non_nullable_required (bool, optional): Whether to also mark every non-nullable column present in `properties` as required, in addition to whatever pydantic already marked, excluding `password`-formatted properties, which are never sent back to the client and would force a retype on every edit, and `readOnly` properties, which cannot be filled at all. Defaults to False.
558
563
 
559
564
  Returns:
560
565
  dict[str, typing.Any]: The generated JSONForms schema.
@@ -594,8 +599,15 @@ class ColumnsMixin(_TypedModelRestApi):
594
599
  )
595
600
  if self.datamodel.is_files(key) or self.datamodel.is_images(key):
596
601
  current_value = value.copy()
602
+ for unused_key in _UNUSED_SCHEMA_KEYS:
603
+ current_value.pop(unused_key, None)
604
+ # JsonForms seeds an inserted row from this default, and a null row must fail so an empty slot cannot be submitted.
605
+ current_value["default"] = None
597
606
  value["type"] = "array"
598
607
  value["items"] = current_value
608
+ # The file keys belong on the leaf; nothing reads them off the array wrapper.
609
+ value.pop("x-file-types", None)
610
+ value.pop("contentMediaType", None)
599
611
  elif self.datamodel.is_boolean(key):
600
612
  value["type"] = "boolean"
601
613
  elif self.datamodel.is_integer(key):
@@ -676,12 +688,15 @@ class ColumnsMixin(_TypedModelRestApi):
676
688
  # Check whether it is nullable
677
689
  if self.datamodel.is_nullable(key):
678
690
  value["type"] = [value["type"], "null"]
691
+ # With setNull on, a cleared field writes a real null, which ajv checks against enum / oneOf regardless of the null type member.
692
+ if "enum" in value:
693
+ value["enum"] = [None, *value["enum"]]
694
+ elif "oneOf" in value:
695
+ value["oneOf"] = [{"const": None, "title": ""}, *value["oneOf"]]
679
696
 
680
697
  # Remove unused vars
681
- value.pop("anyOf", None)
682
- value.pop("default", None)
683
- value.pop("$ref", None)
684
- value.pop("const", None)
698
+ for unused_key in _UNUSED_SCHEMA_KEYS:
699
+ value.pop(unused_key, None)
685
700
 
686
701
  # Check whether the value should be a `secret`
687
702
  if key in g.sensitive_data.get(self.datamodel.obj.__name__, []):
@@ -689,4 +704,17 @@ class ColumnsMixin(_TypedModelRestApi):
689
704
 
690
705
  result["properties"][key] = value
691
706
 
707
+ if mark_non_nullable_required:
708
+ pydantic_required = result.get("required", [])
709
+ added_required = [
710
+ key
711
+ for key, value in result["properties"].items()
712
+ # A password is never sent back to the client and a readOnly field cannot be filled, so requiring either would fail the moment the form opens.
713
+ if key not in pydantic_required
714
+ and not self.datamodel.is_nullable(key)
715
+ and value.get("format") != "password"
716
+ and not value.get("readOnly")
717
+ ]
718
+ result["required"] = [*pydantic_required, *added_required]
719
+
692
720
  return result
@@ -194,6 +194,7 @@ class InfoMixin(_TypedModelRestApi):
194
194
  schema.edit_columns,
195
195
  self.edit_jsonforms_use_enum,
196
196
  self.edit_jsonforms_list_as_object,
197
+ mark_non_nullable_required=True,
197
198
  )
198
199
  )
199
200
  schema.edit_uischema = self.edit_jsonforms_uischema
@@ -204,6 +204,8 @@ class _TypedModelRestApi:
204
204
  [ColumnInfo | ColumnEnumInfo | ColumnRelationInfo | None], bool
205
205
  ],
206
206
  ] = False,
207
+ *,
208
+ mark_non_nullable_required: bool = False,
207
209
  ) -> dict[str, typing.Any]: ...
208
210
 
209
211
  def _get_datamodel_and_column(self, col: str) -> tuple[SQLAInterface, str]: ...
@@ -42,6 +42,10 @@ __all__ = [
42
42
  ]
43
43
 
44
44
 
45
+ # Oracle needs a length on the CAST target: CAST(x AS VARCHAR2) is ORA-00906.
46
+ _TEXT_CAST_TYPE = sa_types.String().with_variant(sa_types.String(4000), "oracle")
47
+
48
+
45
49
  class BaseFilter(AbstractBaseFilter[sqlalchemy.Select[Model]]):
46
50
  def _get_column(self, col_name: str) -> sqlalchemy.Column:
47
51
  """
@@ -143,6 +147,28 @@ class BaseFilter(AbstractBaseFilter[sqlalchemy.Select[Model]]):
143
147
 
144
148
 
145
149
  class BaseFilterTextContains(BaseFilter):
150
+ def _build_concat_expr(self, col_objs: list[sqlalchemy.ColumnElement]):
151
+ """
152
+ Concatenate columns as text, separated by a space.
153
+
154
+ Chains ``||`` instead of ``concat(*args)``: Oracle's CONCAT takes exactly two
155
+ arguments and other databases cap function-call args, which wide tables exceed.
156
+ ``coalesce`` keeps concat's NULL-as-empty.
157
+
158
+ Args:
159
+ col_objs (list[sqlalchemy.ColumnElement]): Columns to concatenate.
160
+
161
+ Returns:
162
+ sqlalchemy.ColumnElement | None: The concatenation, or None when col_objs is empty.
163
+ """
164
+ concat_expr = None
165
+ for col_obj in col_objs:
166
+ piece = sqlalchemy.func.coalesce(
167
+ sqlalchemy.cast(col_obj, _TEXT_CAST_TYPE), ""
168
+ )
169
+ concat_expr = piece if concat_expr is None else concat_expr + " " + piece
170
+ return concat_expr
171
+
146
172
  def _handle_relation_contains(self, col: str, value: str):
147
173
  sub_col = ""
148
174
  if "." in col:
@@ -168,16 +194,14 @@ class BaseFilterTextContains(BaseFilter):
168
194
  else:
169
195
  # If there is no sub-column, we filter all columns in the relation
170
196
  rel_cols = rel_interface.list_columns.keys()
171
- concat = []
172
197
  rel_obj = rel_interface.obj
173
198
  rel_pks = rel_interface.get_pk_attrs()
174
- for col_name in rel_cols:
175
- col_obj = getattr(rel_obj, col_name)
176
- concat.append(sqlalchemy.cast(col_obj, sa_types.String))
177
- concat.append(" ")
199
+ concat_expr = self._build_concat_expr(
200
+ [getattr(rel_obj, col_name) for col_name in rel_cols]
201
+ )
178
202
  rel_statements = [
179
203
  sqlalchemy.select(getattr(rel_obj, pk)).filter(
180
- sqlalchemy.func.concat(*concat).ilike("%" + value + "%")
204
+ concat_expr.ilike("%" + value + "%")
181
205
  )
182
206
  for pk in rel_pks
183
207
  ]
@@ -210,7 +234,7 @@ class BaseFilterTextContains(BaseFilter):
210
234
  if any(value in keywords for keywords in true_keywords)
211
235
  else False
212
236
  )
213
- return sqlalchemy.cast(self._cast_column(col), sa_types.String).ilike(
237
+ return sqlalchemy.cast(self._cast_column(col), _TEXT_CAST_TYPE).ilike(
214
238
  "%" + value + "%"
215
239
  )
216
240
 
@@ -491,9 +515,7 @@ class FilterGlobal(BaseFilterTextContains):
491
515
  filters = []
492
516
 
493
517
  for val in value:
494
- # Chain || instead of concat(*args): some databases cap function-call args,
495
- # which wide tables exceed. coalesce keeps concat's NULL-as-empty.
496
- concat_expr = None
518
+ concat_cols = []
497
519
  rel_filter_arr = []
498
520
  for col in cols:
499
521
  if (
@@ -503,13 +525,9 @@ class FilterGlobal(BaseFilterTextContains):
503
525
  ):
504
526
  rel_filter_arr.append(self._handle_text_contains(col, val))
505
527
  elif col in self.datamodel.list_columns:
506
- piece = sqlalchemy.func.coalesce(
507
- sqlalchemy.cast(self._cast_column(col), sa_types.String), ""
508
- )
509
- concat_expr = (
510
- piece if concat_expr is None else concat_expr + " " + piece
511
- )
528
+ concat_cols.append(self._cast_column(col))
512
529
  params = [*rel_filter_arr]
530
+ concat_expr = self._build_concat_expr(concat_cols)
513
531
  if concat_expr is not None:
514
532
  val = val.strip().lower()
515
533
  params.append(concat_expr.ilike("%" + val + "%"))
@@ -122,7 +122,9 @@ class Api(Model):
122
122
  id: Mapped[int] = mapped_column(
123
123
  sqlalchemy.Integer, sqlalchemy.Sequence(API_SEQUENCE), primary_key=True
124
124
  )
125
- name: Mapped[str] = mapped_column(sqlalchemy.String, unique=True, nullable=False)
125
+ name: Mapped[str] = mapped_column(
126
+ sqlalchemy.String(256), unique=True, nullable=False
127
+ )
126
128
 
127
129
  permissions: Mapped[list[PermissionApi]] = relationship(
128
130
  PermissionApi, back_populates="api", cascade="all, delete-orphan"
@@ -143,7 +145,9 @@ class Permission(Model):
143
145
  id: Mapped[int] = mapped_column(
144
146
  sqlalchemy.Integer, sqlalchemy.Sequence(PERMISSION_SEQUENCE), primary_key=True
145
147
  )
146
- name: Mapped[str] = mapped_column(sqlalchemy.String, unique=True, nullable=False)
148
+ name: Mapped[str] = mapped_column(
149
+ sqlalchemy.String(256), unique=True, nullable=False
150
+ )
147
151
 
148
152
  apis: Mapped[list[PermissionApi]] = relationship(
149
153
  PermissionApi, back_populates="permission", cascade="all, delete-orphan"
@@ -158,7 +162,9 @@ class Role(Model):
158
162
  id: Mapped[int] = mapped_column(
159
163
  sqlalchemy.Integer, sqlalchemy.Sequence(ROLE_SEQUENCE), primary_key=True
160
164
  )
161
- name: Mapped[str] = mapped_column(sqlalchemy.String, unique=True, nullable=False)
165
+ name: Mapped[str] = mapped_column(
166
+ sqlalchemy.String(256), unique=True, nullable=False
167
+ )
162
168
 
163
169
  users: Mapped[list["User"]] = relationship(
164
170
  "User", secondary=assoc_user_role, back_populates="roles"
@@ -6,11 +6,11 @@ Covers the prettify statics, label-column auto-generation,
6
6
  ``_get_datamodel_and_column``, and the ``_jsonforms_*`` schema helpers.
7
7
  """
8
8
 
9
+ import enum
9
10
  import typing
10
11
 
11
12
  import pydantic
12
13
  import pytest
13
-
14
14
  from fastapi_rtk.api.model_rest_api import ModelRestApi
15
15
  from fastapi_rtk.backends.sqla.interface import SQLAInterface
16
16
  from fastapi_rtk.schemas import ColumnRelationInfo, RelInfo
@@ -121,7 +121,6 @@ class TestGetDatamodelAndColumn:
121
121
  @pytest.fixture
122
122
  def Country_cls():
123
123
  import sqlalchemy
124
-
125
124
  from fastapi_rtk.backends.sqla.model import Model
126
125
 
127
126
  if "tier12_country" in Model.metadata.tables:
@@ -140,7 +139,6 @@ def Country_cls():
140
139
  @pytest.fixture
141
140
  def WidgetWithCountry_cls(Country_cls):
142
141
  import sqlalchemy.orm
143
-
144
142
  from fastapi_rtk.backends.sqla.model import Model
145
143
 
146
144
  if "tier12_widget_country" in Model.metadata.tables:
@@ -289,7 +287,6 @@ class TestGenerateJsonformsSchemaRelationReadOnly:
289
287
  def m2m_api(self):
290
288
  import sqlalchemy
291
289
  import sqlalchemy.orm
292
-
293
290
  from fastapi_rtk.backends.sqla.model import Model
294
291
 
295
292
  assoc = sqlalchemy.Table(
@@ -362,7 +359,6 @@ class TestGenerateJsonformsSchemaListBranch:
362
359
  @pytest.fixture(scope="class")
363
360
  def list_api(self):
364
361
  import sqlalchemy
365
-
366
362
  from fastapi_rtk import (
367
363
  JSONBListColumn,
368
364
  ListColumn,
@@ -457,3 +453,345 @@ class TestGenerateJsonformsSchemaListBranch:
457
453
  result = list_api._generate_jsonforms_schema(schema, [], False)
458
454
  assert result["properties"]["tags"]["type"] == ["array", "null"]
459
455
  assert result["properties"]["tags"]["items"] == {"type": "string"}
456
+
457
+
458
+ class TestGenerateJsonformsSchemaFileBranch:
459
+ """A multi-file / multi-image column's `items` sub-schema must be the
460
+ cleaned leaf schema (no `anyOf` leftover from the pydantic union), not a
461
+ copy taken before that cleanup runs. It also carries a deliberate
462
+ `default: null` so a freshly inserted row fails validation until a file
463
+ is picked."""
464
+
465
+ @pytest.fixture
466
+ def file_api(self, tmp_path, config_setter):
467
+ import sqlalchemy
468
+ from fastapi_rtk.backends.sqla.model import Model
469
+ from fastapi_rtk.types import FileColumn, ImageColumns
470
+
471
+ upload_folder = tmp_path / "uploads"
472
+ img_folder = tmp_path / "img_uploads"
473
+ upload_folder.mkdir()
474
+ img_folder.mkdir()
475
+ config_setter("UPLOAD_FOLDER", str(upload_folder))
476
+ config_setter("IMG_UPLOAD_FOLDER", str(img_folder))
477
+
478
+ class _TierFileModel(Model):
479
+ __tablename__ = "tier12_file_model"
480
+ __table_args__ = {"extend_existing": True}
481
+ id = sqlalchemy.Column(sqlalchemy.Integer, primary_key=True)
482
+ document = sqlalchemy.Column(FileColumn(allowed_extensions=["txt", "pdf"]))
483
+ gallery = sqlalchemy.Column(ImageColumns(allowed_extensions=["png", "jpg"]))
484
+
485
+ class TierFileApi(ModelRestApi):
486
+ datamodel = SQLAInterface(_TierFileModel)
487
+
488
+ return TierFileApi()
489
+
490
+ @pytest.fixture
491
+ def file_schema_result(self, file_api):
492
+ schema = file_api.datamodel.generate_schema(
493
+ columns=["document", "gallery"],
494
+ with_id=False,
495
+ with_name=False,
496
+ with_property=False,
497
+ optional=True,
498
+ hide_sensitive_columns=False,
499
+ is_form_data=True,
500
+ name="TierFileFormData",
501
+ )
502
+ return file_api._generate_jsonforms_schema(schema, [])
503
+
504
+ def test_multi_image_items_is_the_cleaned_leaf_with_null_default(
505
+ self, file_api, file_schema_result
506
+ ):
507
+ from fastapi_rtk.setting import Setting
508
+
509
+ gallery = file_schema_result["properties"]["gallery"]
510
+ assert "anyOf" not in gallery["items"]
511
+ assert gallery["items"] == {
512
+ "title": file_api.label_columns["gallery"],
513
+ "type": "string",
514
+ "default": None,
515
+ "x-file-types": [".png", ".jpg"],
516
+ "contentMediaType": ",".join(Setting.IMG_MIME_TYPES),
517
+ }
518
+
519
+ def test_multi_image_does_not_duplicate_file_keys_on_the_array(
520
+ self, file_schema_result
521
+ ):
522
+ gallery = file_schema_result["properties"]["gallery"]
523
+ assert "x-file-types" not in gallery
524
+ assert "contentMediaType" not in gallery
525
+ assert "x-file-types" in gallery["items"]
526
+ assert "contentMediaType" in gallery["items"]
527
+
528
+ def test_multi_image_own_type_is_array_or_null(self, file_schema_result):
529
+ gallery = file_schema_result["properties"]["gallery"]
530
+ assert gallery["type"] == ["array", "null"]
531
+ assert "anyOf" not in gallery
532
+
533
+ def test_single_file_has_no_items_or_anyof(self, file_schema_result):
534
+ document = file_schema_result["properties"]["document"]
535
+ assert "anyOf" not in document
536
+ assert "items" not in document
537
+ assert document["type"] == ["string", "null"]
538
+
539
+
540
+ class TestGenerateJsonformsSchemaNullableConstrainedValues:
541
+ """A nullable field with a constrained value set (enum / oneOf) must gain an
542
+ empty member alongside the ``null`` type, or AJV reports an option-mismatch
543
+ error the moment the frontend clears the field.
544
+ """
545
+
546
+ @pytest.fixture(scope="class")
547
+ def enum_api(self):
548
+ import sqlalchemy
549
+ from fastapi_rtk.backends.sqla.model import Model
550
+
551
+ class _TierStatus(enum.Enum):
552
+ ACTIVE = "active"
553
+ INACTIVE = "inactive"
554
+
555
+ class _TierEnumModel(Model):
556
+ __tablename__ = "tier12_enum_model"
557
+ __table_args__ = {"extend_existing": True}
558
+ id = sqlalchemy.Column(sqlalchemy.Integer, primary_key=True)
559
+ status = sqlalchemy.Column(sqlalchemy.Enum(_TierStatus), nullable=True)
560
+ status_required = sqlalchemy.Column(
561
+ sqlalchemy.Enum(_TierStatus), nullable=False
562
+ )
563
+
564
+ class TierEnumApi(ModelRestApi):
565
+ datamodel = SQLAInterface(_TierEnumModel)
566
+
567
+ return TierEnumApi()
568
+
569
+ def test_nullable_enum_gets_null_type_and_member(self, enum_api):
570
+ schema = enum_api.add_obj_schema
571
+ result = enum_api._generate_jsonforms_schema(schema, [], False)
572
+ status = result["properties"]["status"]
573
+ assert status["type"] == ["string", "null"]
574
+ assert status["enum"][0] is None
575
+ assert set(status["enum"][1:]) == {"active", "inactive"}
576
+
577
+ def test_non_nullable_enum_has_no_null_member(self, enum_api):
578
+ schema = enum_api.add_obj_schema
579
+ result = enum_api._generate_jsonforms_schema(schema, [], False)
580
+ status_required = result["properties"]["status_required"]
581
+ assert None not in status_required["enum"]
582
+
583
+ def test_repeated_calls_do_not_grow_the_enum(self, enum_api):
584
+ """Regression: ``get_enum_value`` hands back the SQLAlchemy column's own
585
+ ``enums`` list, so mutating it in place would corrupt it for every later
586
+ request instead of just the current schema dict.
587
+ """
588
+ schema = enum_api.add_obj_schema
589
+ enum_api._generate_jsonforms_schema(schema, [], False)
590
+ result = enum_api._generate_jsonforms_schema(schema, [], False)
591
+ status = result["properties"]["status"]
592
+ assert status["enum"].count(None) == 1
593
+
594
+ @pytest.fixture
595
+ def relation_api(self, WidgetWithCountry_cls):
596
+ class TierRelationApi(ModelRestApi):
597
+ datamodel = SQLAInterface(WidgetWithCountry_cls)
598
+
599
+ return TierRelationApi()
600
+
601
+ def test_nullable_relation_with_use_enum_gets_null_member(self, relation_api):
602
+ schema = relation_api.add_obj_schema
603
+ info = [
604
+ ColumnRelationInfo(name="country", values=[RelInfo(id=1, value="Germany")])
605
+ ]
606
+ result = relation_api._generate_jsonforms_schema(schema, info, use_enum=True)
607
+ assert result["properties"]["country"]["enum"][0] is None
608
+
609
+ def test_nullable_relation_without_use_enum_gets_null_oneof_member(
610
+ self, relation_api
611
+ ):
612
+ schema = relation_api.add_obj_schema
613
+ info = [
614
+ ColumnRelationInfo(name="country", values=[RelInfo(id=1, value="Germany")])
615
+ ]
616
+ result = relation_api._generate_jsonforms_schema(schema, info, False)
617
+ assert result["properties"]["country"]["oneOf"][0] == {
618
+ "const": None,
619
+ "title": "",
620
+ }
621
+
622
+ @pytest.fixture(scope="class")
623
+ def m2m_relation_api(self):
624
+ import sqlalchemy
625
+ import sqlalchemy.orm
626
+ from fastapi_rtk.backends.sqla.model import Model
627
+
628
+ assoc = sqlalchemy.Table(
629
+ "tier12_nullable_m2m_assoc",
630
+ Model.metadata,
631
+ sqlalchemy.Column(
632
+ "widget_id",
633
+ sqlalchemy.ForeignKey("tier12_nullable_m2m_widget.id"),
634
+ primary_key=True,
635
+ ),
636
+ sqlalchemy.Column(
637
+ "tag_id",
638
+ sqlalchemy.ForeignKey("tier12_nullable_m2m_tag.id"),
639
+ primary_key=True,
640
+ ),
641
+ extend_existing=True,
642
+ )
643
+
644
+ class _NullableM2MTag(Model):
645
+ __tablename__ = "tier12_nullable_m2m_tag"
646
+ __table_args__ = {"extend_existing": True}
647
+ id = sqlalchemy.Column(sqlalchemy.Integer, primary_key=True)
648
+ name = sqlalchemy.Column(sqlalchemy.String(50))
649
+
650
+ class _NullableM2MWidget(Model):
651
+ __tablename__ = "tier12_nullable_m2m_widget"
652
+ __table_args__ = {"extend_existing": True}
653
+ id = sqlalchemy.Column(sqlalchemy.Integer, primary_key=True)
654
+ name = sqlalchemy.Column(sqlalchemy.String(50))
655
+ tags = sqlalchemy.orm.relationship(_NullableM2MTag, secondary=assoc)
656
+
657
+ class TierNullableM2MApi(ModelRestApi):
658
+ datamodel = SQLAInterface(_NullableM2MWidget)
659
+
660
+ return TierNullableM2MApi()
661
+
662
+ def test_nullable_to_many_relation_items_have_no_null_member(
663
+ self, m2m_relation_api
664
+ ):
665
+ schema = m2m_relation_api.add_obj_schema
666
+ info = [ColumnRelationInfo(name="tags", values=[RelInfo(id=1, value="a")])]
667
+ result = m2m_relation_api._generate_jsonforms_schema(schema, info, False)
668
+ tags = result["properties"]["tags"]
669
+ assert tags["type"] == ["array", "null"]
670
+ assert None not in [item.get("const") for item in tags["items"]["oneOf"]]
671
+
672
+
673
+ class TestGenerateJsonformsSchemaMarkNonNullableRequired:
674
+ """``mark_non_nullable_required`` lets the EDIT schema report a missing
675
+ non-nullable value itself, instead of the frontend inventing that error.
676
+ """
677
+
678
+ @pytest.fixture(scope="class")
679
+ def enum_api(self):
680
+ import sqlalchemy
681
+ from fastapi_rtk.backends.sqla.model import Model
682
+
683
+ class _TierRequiredStatus(enum.Enum):
684
+ ACTIVE = "active"
685
+ INACTIVE = "inactive"
686
+
687
+ class _TierRequiredModel(Model):
688
+ __tablename__ = "tier12_required_model"
689
+ __table_args__ = {"extend_existing": True}
690
+ id = sqlalchemy.Column(sqlalchemy.Integer, primary_key=True)
691
+ status = sqlalchemy.Column(
692
+ sqlalchemy.Enum(_TierRequiredStatus), nullable=True
693
+ )
694
+ status_required = sqlalchemy.Column(
695
+ sqlalchemy.Enum(_TierRequiredStatus), nullable=False
696
+ )
697
+
698
+ class TierRequiredApi(ModelRestApi):
699
+ datamodel = SQLAInterface(_TierRequiredModel)
700
+
701
+ return TierRequiredApi()
702
+
703
+ @pytest.fixture
704
+ def excluded_required_api(self, Widget_cls):
705
+ class TierExcludedRequiredApi(ModelRestApi):
706
+ datamodel = SQLAInterface(Widget_cls)
707
+ edit_columns = ["description", "quantity"]
708
+
709
+ return TierExcludedRequiredApi()
710
+
711
+ def test_default_leaves_required_as_pydantic_emitted(self, enum_api):
712
+ schema = enum_api.edit_obj_schema
713
+ result = enum_api._generate_jsonforms_schema(schema, [])
714
+ assert result.get("required", []) == schema.model_json_schema().get(
715
+ "required", []
716
+ )
717
+
718
+ def test_flag_marks_every_non_nullable_column_present_as_required(self, enum_api):
719
+ schema = enum_api.edit_obj_schema
720
+ result = enum_api._generate_jsonforms_schema(
721
+ schema, [], mark_non_nullable_required=True
722
+ )
723
+ assert "status_required" in result["required"]
724
+ assert "status" not in result["required"]
725
+
726
+ def test_flag_does_not_duplicate_an_already_required_column(self, enum_api):
727
+ schema = enum_api.add_obj_schema
728
+ result = enum_api._generate_jsonforms_schema(
729
+ schema, [], mark_non_nullable_required=True
730
+ )
731
+ assert result["required"].count("status_required") == 1
732
+
733
+ def test_flag_skips_non_nullable_column_absent_from_properties(
734
+ self, excluded_required_api
735
+ ):
736
+ schema = excluded_required_api.edit_obj_schema
737
+ result = excluded_required_api._generate_jsonforms_schema(
738
+ schema, [], mark_non_nullable_required=True
739
+ )
740
+ assert "name" not in result["properties"]
741
+ assert "name" not in result["required"]
742
+
743
+ def test_repeated_calls_with_flag_yield_identical_required(self, enum_api):
744
+ schema = enum_api.edit_obj_schema
745
+ first = enum_api._generate_jsonforms_schema(
746
+ schema, [], mark_non_nullable_required=True
747
+ )
748
+ second = enum_api._generate_jsonforms_schema(
749
+ schema, [], mark_non_nullable_required=True
750
+ )
751
+ assert first["required"] == second["required"]
752
+
753
+ def test_flag_skips_a_non_nullable_password_column(self, enum_api):
754
+ from fastapi_rtk.globals import g
755
+
756
+ model_name = enum_api.datamodel.obj.__name__
757
+ original = g.sensitive_data
758
+ g.sensitive_data = {**original, model_name: ["status_required"]}
759
+ try:
760
+ schema = enum_api.edit_obj_schema
761
+ result = enum_api._generate_jsonforms_schema(
762
+ schema, [], mark_non_nullable_required=True
763
+ )
764
+ assert result["properties"]["status_required"]["format"] == "password"
765
+ assert "status_required" not in result["required"]
766
+ finally:
767
+ g.sensitive_data = original
768
+
769
+ def test_flag_skips_a_non_nullable_readonly_relation(self, Country_cls):
770
+ import sqlalchemy
771
+ import sqlalchemy.orm
772
+ from fastapi_rtk.backends.sqla.model import Model
773
+
774
+ class TierWidgetRequiredCountry(Model):
775
+ __tablename__ = "tier12_widget_required_country"
776
+ __table_args__ = {"extend_existing": True}
777
+ id = sqlalchemy.Column(sqlalchemy.Integer, primary_key=True)
778
+ name = sqlalchemy.Column(sqlalchemy.String(50))
779
+ country_id = sqlalchemy.Column(
780
+ sqlalchemy.Integer,
781
+ sqlalchemy.ForeignKey("tier12_country.id"),
782
+ nullable=False,
783
+ )
784
+ country = sqlalchemy.orm.relationship(Country_cls)
785
+
786
+ class TierRequiredRelationApi(ModelRestApi):
787
+ datamodel = SQLAInterface(TierWidgetRequiredCountry)
788
+ edit_columns = ["name", "country"]
789
+
790
+ api = TierRequiredRelationApi()
791
+ schema = api.edit_obj_schema
792
+ info = [ColumnRelationInfo(name="country", values=[])]
793
+ result = api._generate_jsonforms_schema(
794
+ schema, info, mark_non_nullable_required=True
795
+ )
796
+ assert result["properties"]["country"]["readOnly"] is True
797
+ assert "country" not in result["required"]