bazis 2.3.0__tar.gz → 2.3.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 (242) hide show
  1. {bazis-2.3.0 → bazis-2.3.2}/.github/workflows/release.yml +5 -2
  2. {bazis-2.3.0 → bazis-2.3.2}/CLAUDE.md +27 -0
  3. {bazis-2.3.0 → bazis-2.3.2}/PKG-INFO +2 -2
  4. {bazis-2.3.0 → bazis-2.3.2}/bazis/core/_version.py +3 -3
  5. {bazis-2.3.0 → bazis-2.3.2}/bazis/core/routes_abstract/context.py +1 -1
  6. bazis-2.3.2/bazis/core/routes_abstract/jsonapi/cache.py +25 -0
  7. {bazis-2.3.0 → bazis-2.3.2}/bazis/core/routes_abstract/jsonapi/route_base.py +127 -21
  8. {bazis-2.3.0 → bazis-2.3.2}/bazis/core/schemas/cache.py +2 -33
  9. {bazis-2.3.0 → bazis-2.3.2}/bazis/core/schemas/schemas.py +1 -1
  10. {bazis-2.3.0 → bazis-2.3.2}/bazis/core/schemas/validators.py +0 -5
  11. {bazis-2.3.0 → bazis-2.3.2}/bazis.egg-info/PKG-INFO +2 -2
  12. {bazis-2.3.0 → bazis-2.3.2}/bazis.egg-info/SOURCES.txt +3 -0
  13. {bazis-2.3.0 → bazis-2.3.2}/bazis.egg-info/requires.txt +1 -1
  14. {bazis-2.3.0 → bazis-2.3.2}/bazis.egg-info/scm_file_list.json +3 -0
  15. bazis-2.3.2/bazis.egg-info/scm_version.json +8 -0
  16. bazis-2.3.2/docs/releases/2.3.1.md +29 -0
  17. bazis-2.3.2/docs/releases/2.3.2.md +27 -0
  18. {bazis-2.3.0 → bazis-2.3.2}/pyproject.toml +1 -1
  19. {bazis-2.3.0 → bazis-2.3.2}/sample/entity/routes.py +8 -0
  20. bazis-2.3.2/tests/test_relationships_access.py +72 -0
  21. {bazis-2.3.0 → bazis-2.3.2}/tests/test_schema_cache.py +3 -4
  22. bazis-2.3.0/bazis/core/routes_abstract/jsonapi/cache.py +0 -44
  23. bazis-2.3.0/bazis.egg-info/scm_version.json +0 -8
  24. {bazis-2.3.0 → bazis-2.3.2}/.dockerignore +0 -0
  25. {bazis-2.3.0 → bazis-2.3.2}/.github/workflows/build-publish.yml +0 -0
  26. {bazis-2.3.0 → bazis-2.3.2}/.github/workflows/tests.yml +0 -0
  27. {bazis-2.3.0 → bazis-2.3.2}/.github/workflows/update-licenses.yml +0 -0
  28. {bazis-2.3.0 → bazis-2.3.2}/.gitignore +0 -0
  29. {bazis-2.3.0 → bazis-2.3.2}/LICENSE +0 -0
  30. {bazis-2.3.0 → bazis-2.3.2}/NOTICE +0 -0
  31. {bazis-2.3.0 → bazis-2.3.2}/README.md +0 -0
  32. {bazis-2.3.0 → bazis-2.3.2}/bazis/core/__init__.py +0 -0
  33. {bazis-2.3.0 → bazis-2.3.2}/bazis/core/admin_abstract.py +0 -0
  34. {bazis-2.3.0 → bazis-2.3.2}/bazis/core/app.py +0 -0
  35. {bazis-2.3.0 → bazis-2.3.2}/bazis/core/app_factory.py +0 -0
  36. {bazis-2.3.0 → bazis-2.3.2}/bazis/core/apps.py +0 -0
  37. {bazis-2.3.0 → bazis-2.3.2}/bazis/core/checks.py +0 -0
  38. {bazis-2.3.0 → bazis-2.3.2}/bazis/core/conf.py +0 -0
  39. {bazis-2.3.0 → bazis-2.3.2}/bazis/core/configure.py +0 -0
  40. {bazis-2.3.0 → bazis-2.3.2}/bazis/core/constance_conf/__init__.py +0 -0
  41. {bazis-2.3.0 → bazis-2.3.2}/bazis/core/constance_conf/base.py +0 -0
  42. {bazis-2.3.0 → bazis-2.3.2}/bazis/core/errors.py +0 -0
  43. {bazis-2.3.0 → bazis-2.3.2}/bazis/core/fields.py +0 -0
  44. {bazis-2.3.0 → bazis-2.3.2}/bazis/core/i18n.py +0 -0
  45. {bazis-2.3.0 → bazis-2.3.2}/bazis/core/locale/en/LC_MESSAGES/django.mo +0 -0
  46. {bazis-2.3.0 → bazis-2.3.2}/bazis/core/locale/en/LC_MESSAGES/django.po +0 -0
  47. {bazis-2.3.0 → bazis-2.3.2}/bazis/core/locale/messages.pot +0 -0
  48. {bazis-2.3.0 → bazis-2.3.2}/bazis/core/locale/ru/LC_MESSAGES/django.mo +0 -0
  49. {bazis-2.3.0 → bazis-2.3.2}/bazis/core/locale/ru/LC_MESSAGES/django.po +0 -0
  50. {bazis-2.3.0 → bazis-2.3.2}/bazis/core/mail.py +0 -0
  51. {bazis-2.3.0 → bazis-2.3.2}/bazis/core/management/__init__.py +0 -0
  52. {bazis-2.3.0 → bazis-2.3.2}/bazis/core/management/commands/__init__.py +0 -0
  53. {bazis-2.3.0 → bazis-2.3.2}/bazis/core/management/commands/admin_create.py +0 -0
  54. {bazis-2.3.0 → bazis-2.3.2}/bazis/core/management/commands/schemas_build.py +0 -0
  55. {bazis-2.3.0 → bazis-2.3.2}/bazis/core/management/commands/schemas_build_lang.py +0 -0
  56. {bazis-2.3.0 → bazis-2.3.2}/bazis/core/migrations/0001_initial.py +0 -0
  57. {bazis-2.3.0 → bazis-2.3.2}/bazis/core/migrations/0002_auto_20260114_1222.py +0 -0
  58. {bazis-2.3.0 → bazis-2.3.2}/bazis/core/migrations/0003_rename_email_settings.py +0 -0
  59. {bazis-2.3.0 → bazis-2.3.2}/bazis/core/migrations/__init__.py +0 -0
  60. {bazis-2.3.0 → bazis-2.3.2}/bazis/core/models_abstract.py +0 -0
  61. {bazis-2.3.0 → bazis-2.3.2}/bazis/core/router.py +0 -0
  62. {bazis-2.3.0 → bazis-2.3.2}/bazis/core/routes_abstract/__init__.py +0 -0
  63. {bazis-2.3.0 → bazis-2.3.2}/bazis/core/routes_abstract/initial.py +0 -0
  64. {bazis-2.3.0 → bazis-2.3.2}/bazis/core/routes_abstract/jsonapi/__init__.py +0 -0
  65. {bazis-2.3.0 → bazis-2.3.2}/bazis/core/routes_abstract/jsonapi/callbacks.py +0 -0
  66. {bazis-2.3.0 → bazis-2.3.2}/bazis/core/routes_abstract/jsonapi/mixins.py +0 -0
  67. {bazis-2.3.0 → bazis-2.3.2}/bazis/core/routes_abstract/jsonapi/schemas.py +0 -0
  68. {bazis-2.3.0 → bazis-2.3.2}/bazis/core/routes_abstract/jsonapi/schemas_dict.py +0 -0
  69. {bazis-2.3.0 → bazis-2.3.2}/bazis/core/routes_abstract/jsonapi/services.py +0 -0
  70. {bazis-2.3.0 → bazis-2.3.2}/bazis/core/routing.py +0 -0
  71. {bazis-2.3.0 → bazis-2.3.2}/bazis/core/schemas/__init__.py +0 -0
  72. {bazis-2.3.0 → bazis-2.3.2}/bazis/core/schemas/builders.py +0 -0
  73. {bazis-2.3.0 → bazis-2.3.2}/bazis/core/schemas/enums.py +0 -0
  74. {bazis-2.3.0 → bazis-2.3.2}/bazis/core/schemas/factory.py +0 -0
  75. {bazis-2.3.0 → bazis-2.3.2}/bazis/core/schemas/fields.py +0 -0
  76. {bazis-2.3.0 → bazis-2.3.2}/bazis/core/schemas/meta.py +0 -0
  77. {bazis-2.3.0 → bazis-2.3.2}/bazis/core/schemas/utils.py +0 -0
  78. {bazis-2.3.0 → bazis-2.3.2}/bazis/core/services/__init__.py +0 -0
  79. {bazis-2.3.0 → bazis-2.3.2}/bazis/core/services/filtering.py +0 -0
  80. {bazis-2.3.0 → bazis-2.3.2}/bazis/core/services/includes.py +0 -0
  81. {bazis-2.3.0 → bazis-2.3.2}/bazis/core/services/meta_fields.py +0 -0
  82. {bazis-2.3.0 → bazis-2.3.2}/bazis/core/services/pagination.py +0 -0
  83. {bazis-2.3.0 → bazis-2.3.2}/bazis/core/services/relationships.py +0 -0
  84. {bazis-2.3.0 → bazis-2.3.2}/bazis/core/services/route_ctx.py +0 -0
  85. {bazis-2.3.0 → bazis-2.3.2}/bazis/core/services/searching.py +0 -0
  86. {bazis-2.3.0 → bazis-2.3.2}/bazis/core/services/sorting.py +0 -0
  87. {bazis-2.3.0 → bazis-2.3.2}/bazis/core/services/sparse_fieldsets.py +0 -0
  88. {bazis-2.3.0 → bazis-2.3.2}/bazis/core/triggers.py +0 -0
  89. {bazis-2.3.0 → bazis-2.3.2}/bazis/core/urls.py +0 -0
  90. {bazis-2.3.0 → bazis-2.3.2}/bazis/core/utils/__init__.py +0 -0
  91. {bazis-2.3.0 → bazis-2.3.2}/bazis/core/utils/apps.py +0 -0
  92. {bazis-2.3.0 → bazis-2.3.2}/bazis/core/utils/django_types.py +0 -0
  93. {bazis-2.3.0 → bazis-2.3.2}/bazis/core/utils/fastapi_monkey_patch.py +0 -0
  94. {bazis-2.3.0 → bazis-2.3.2}/bazis/core/utils/files.py +0 -0
  95. {bazis-2.3.0 → bazis-2.3.2}/bazis/core/utils/functools.py +0 -0
  96. {bazis-2.3.0 → bazis-2.3.2}/bazis/core/utils/imp.py +0 -0
  97. {bazis-2.3.0 → bazis-2.3.2}/bazis/core/utils/locale.py +0 -0
  98. {bazis-2.3.0 → bazis-2.3.2}/bazis/core/utils/logging_level.py +0 -0
  99. {bazis-2.3.0 → bazis-2.3.2}/bazis/core/utils/model_meta.py +0 -0
  100. {bazis-2.3.0 → bazis-2.3.2}/bazis/core/utils/network.py +0 -0
  101. {bazis-2.3.0 → bazis-2.3.2}/bazis/core/utils/orm.py +0 -0
  102. {bazis-2.3.0 → bazis-2.3.2}/bazis/core/utils/pghistory_monkey_patch.py +0 -0
  103. {bazis-2.3.0 → bazis-2.3.2}/bazis/core/utils/query_complex.py +0 -0
  104. {bazis-2.3.0 → bazis-2.3.2}/bazis/core/utils/schemas.py +0 -0
  105. {bazis-2.3.0 → bazis-2.3.2}/bazis/core/utils/sets_order.py +0 -0
  106. {bazis-2.3.0 → bazis-2.3.2}/bazis/core/utils/translated_field_utils.py +0 -0
  107. {bazis-2.3.0 → bazis-2.3.2}/bazis/core/utils/triggers.py +0 -0
  108. {bazis-2.3.0 → bazis-2.3.2}/bazis.egg-info/dependency_links.txt +0 -0
  109. {bazis-2.3.0 → bazis-2.3.2}/bazis.egg-info/top_level.txt +0 -0
  110. {bazis-2.3.0 → bazis-2.3.2}/docs/Makefile +0 -0
  111. {bazis-2.3.0 → bazis-2.3.2}/docs/README.md +0 -0
  112. {bazis-2.3.0 → bazis-2.3.2}/docs/_static/pydata-custom.css +0 -0
  113. {bazis-2.3.0 → bazis-2.3.2}/docs/_static/readthedocs-custom.css +0 -0
  114. {bazis-2.3.0 → bazis-2.3.2}/docs/_templates/custom-class-template.rst +0 -0
  115. {bazis-2.3.0 → bazis-2.3.2}/docs/_templates/custom-module-template.rst +0 -0
  116. {bazis-2.3.0 → bazis-2.3.2}/docs/conf.py +0 -0
  117. {bazis-2.3.0 → bazis-2.3.2}/docs/dynamic_fields.rst +0 -0
  118. {bazis-2.3.0 → bazis-2.3.2}/docs/endpoint_callbacks.rst +0 -0
  119. {bazis-2.3.0 → bazis-2.3.2}/docs/generate_dynamic_rst.py +0 -0
  120. {bazis-2.3.0 → bazis-2.3.2}/docs/index.rst +0 -0
  121. {bazis-2.3.0 → bazis-2.3.2}/docs/make.bat +0 -0
  122. {bazis-2.3.0 → bazis-2.3.2}/docs/manage.py +0 -0
  123. {bazis-2.3.0 → bazis-2.3.2}/docs/openapi.rst +0 -0
  124. {bazis-2.3.0 → bazis-2.3.2}/docs/relationships.rst +0 -0
  125. {bazis-2.3.0 → bazis-2.3.2}/docs/releases/2.2.9.md +0 -0
  126. {bazis-2.3.0 → bazis-2.3.2}/docs/releases/2.3.0.md +0 -0
  127. {bazis-2.3.0 → bazis-2.3.2}/docs/requirements.txt +0 -0
  128. {bazis-2.3.0 → bazis-2.3.2}/docs/response_models.rst +0 -0
  129. {bazis-2.3.0 → bazis-2.3.2}/docs/routes.rst +0 -0
  130. {bazis-2.3.0 → bazis-2.3.2}/docs/services.rst +0 -0
  131. {bazis-2.3.0 → bazis-2.3.2}/docs/setting.rst +0 -0
  132. {bazis-2.3.0 → bazis-2.3.2}/docs/settings.py +0 -0
  133. {bazis-2.3.0 → bazis-2.3.2}/docs/sparse_fieldsets.rst +0 -0
  134. {bazis-2.3.0 → bazis-2.3.2}/docs/structure.rst +0 -0
  135. {bazis-2.3.0 → bazis-2.3.2}/pytest.ini +0 -0
  136. {bazis-2.3.0 → bazis-2.3.2}/sample/deploy/admin.sh +0 -0
  137. {bazis-2.3.0 → bazis-2.3.2}/sample/deploy/app.sh +0 -0
  138. {bazis-2.3.0 → bazis-2.3.2}/sample/deploy/config/admin.py +0 -0
  139. {bazis-2.3.0 → bazis-2.3.2}/sample/deploy/config/app.py +0 -0
  140. {bazis-2.3.0 → bazis-2.3.2}/sample/deploy/db.sh +0 -0
  141. {bazis-2.3.0 → bazis-2.3.2}/sample/deploy/init.sh +0 -0
  142. {bazis-2.3.0 → bazis-2.3.2}/sample/dynamic/__init__.py +0 -0
  143. {bazis-2.3.0 → bazis-2.3.2}/sample/dynamic/migrations/0001_initial.py +0 -0
  144. {bazis-2.3.0 → bazis-2.3.2}/sample/dynamic/migrations/__init__.py +0 -0
  145. {bazis-2.3.0 → bazis-2.3.2}/sample/dynamic/models.py +0 -0
  146. {bazis-2.3.0 → bazis-2.3.2}/sample/dynamic/router.py +0 -0
  147. {bazis-2.3.0 → bazis-2.3.2}/sample/dynamic/routes.py +0 -0
  148. {bazis-2.3.0 → bazis-2.3.2}/sample/entity/__init__.py +0 -0
  149. {bazis-2.3.0 → bazis-2.3.2}/sample/entity/admin.py +0 -0
  150. {bazis-2.3.0 → bazis-2.3.2}/sample/entity/migrations/0001_initial.py +0 -0
  151. {bazis-2.3.0 → bazis-2.3.2}/sample/entity/migrations/0002_parententity_field_parententity_state.py +0 -0
  152. {bazis-2.3.0 → bazis-2.3.2}/sample/entity/migrations/0003_country_driver_vehiclebrand_vehicle_carriertask_and_more.py +0 -0
  153. {bazis-2.3.0 → bazis-2.3.2}/sample/entity/migrations/0004_division_organization_driver_divisions_and_more.py +0 -0
  154. {bazis-2.3.0 → bazis-2.3.2}/sample/entity/migrations/0005_extendedentitynull.py +0 -0
  155. {bazis-2.3.0 → bazis-2.3.2}/sample/entity/migrations/0006_dependententitynull.py +0 -0
  156. {bazis-2.3.0 → bazis-2.3.2}/sample/entity/migrations/0007_withprotectedentity.py +0 -0
  157. {bazis-2.3.0 → bazis-2.3.2}/sample/entity/migrations/0008_withprotectedentitysystem.py +0 -0
  158. {bazis-2.3.0 → bazis-2.3.2}/sample/entity/migrations/__init__.py +0 -0
  159. {bazis-2.3.0 → bazis-2.3.2}/sample/entity/models.py +0 -0
  160. {bazis-2.3.0 → bazis-2.3.2}/sample/entity/router.py +0 -0
  161. {bazis-2.3.0 → bazis-2.3.2}/sample/generate_local_test_data.py +0 -0
  162. {bazis-2.3.0 → bazis-2.3.2}/sample/main.py +0 -0
  163. {bazis-2.3.0 → bazis-2.3.2}/sample/manage.py +0 -0
  164. {bazis-2.3.0 → bazis-2.3.2}/sample/project.env +0 -0
  165. {bazis-2.3.0 → bazis-2.3.2}/sample/pyproject.toml +0 -0
  166. {bazis-2.3.0 → bazis-2.3.2}/sample/route_injection/__init__.py +0 -0
  167. {bazis-2.3.0 → bazis-2.3.2}/sample/route_injection/migrations/0001_initial.py +0 -0
  168. {bazis-2.3.0 → bazis-2.3.2}/sample/route_injection/migrations/__init__.py +0 -0
  169. {bazis-2.3.0 → bazis-2.3.2}/sample/route_injection/models.py +0 -0
  170. {bazis-2.3.0 → bazis-2.3.2}/sample/route_injection/route_bases.py +0 -0
  171. {bazis-2.3.0 → bazis-2.3.2}/sample/route_injection/router.py +0 -0
  172. {bazis-2.3.0 → bazis-2.3.2}/sample/route_injection/routes.py +0 -0
  173. {bazis-2.3.0 → bazis-2.3.2}/sample/route_injection/schemas.py +0 -0
  174. {bazis-2.3.0 → bazis-2.3.2}/sample/route_injection/services.py +0 -0
  175. {bazis-2.3.0 → bazis-2.3.2}/sample/sample/__init__.py +0 -0
  176. {bazis-2.3.0 → bazis-2.3.2}/sample/sample/main.py +0 -0
  177. {bazis-2.3.0 → bazis-2.3.2}/sample/sample/route_mixins.py +0 -0
  178. {bazis-2.3.0 → bazis-2.3.2}/sample/sample/router.py +0 -0
  179. {bazis-2.3.0 → bazis-2.3.2}/sample/sample/settings.py +0 -0
  180. {bazis-2.3.0 → bazis-2.3.2}/sample/sample/urls.py +0 -0
  181. {bazis-2.3.0 → bazis-2.3.2}/sample/sample/wsgi.py +0 -0
  182. {bazis-2.3.0 → bazis-2.3.2}/sample/sparse_fieldsets/__init__.py +0 -0
  183. {bazis-2.3.0 → bazis-2.3.2}/sample/sparse_fieldsets/migrations/0001_initial.py +0 -0
  184. {bazis-2.3.0 → bazis-2.3.2}/sample/sparse_fieldsets/migrations/0002_category_article_category.py +0 -0
  185. {bazis-2.3.0 → bazis-2.3.2}/sample/sparse_fieldsets/migrations/__init__.py +0 -0
  186. {bazis-2.3.0 → bazis-2.3.2}/sample/sparse_fieldsets/models.py +0 -0
  187. {bazis-2.3.0 → bazis-2.3.2}/sample/sparse_fieldsets/router.py +0 -0
  188. {bazis-2.3.0 → bazis-2.3.2}/sample/sparse_fieldsets/routes.py +0 -0
  189. {bazis-2.3.0 → bazis-2.3.2}/sample/uv.lock +0 -0
  190. {bazis-2.3.0 → bazis-2.3.2}/setup.cfg +0 -0
  191. {bazis-2.3.0 → bazis-2.3.2}/tests/__init__.py +0 -0
  192. {bazis-2.3.0 → bazis-2.3.2}/tests/conftest.py +0 -0
  193. {bazis-2.3.0 → bazis-2.3.2}/tests/factories.py +0 -0
  194. {bazis-2.3.0 → bazis-2.3.2}/tests/test_core_config.py +0 -0
  195. {bazis-2.3.0 → bazis-2.3.2}/tests/test_models.py +0 -0
  196. {bazis-2.3.0 → bazis-2.3.2}/tests/test_routes_add.py +0 -0
  197. {bazis-2.3.0 → bazis-2.3.2}/tests/test_routes_change.py +0 -0
  198. {bazis-2.3.0 → bazis-2.3.2}/tests/test_routes_core.py +0 -0
  199. {bazis-2.3.0 → bazis-2.3.2}/tests/test_routes_delete.py +0 -0
  200. {bazis-2.3.0 → bazis-2.3.2}/tests/test_routes_errors.py +0 -0
  201. {bazis-2.3.0 → bazis-2.3.2}/tests/test_routes_view/__init__.py +0 -0
  202. {bazis-2.3.0 → bazis-2.3.2}/tests/test_routes_view/dynamic/__init__.py +0 -0
  203. {bazis-2.3.0 → bazis-2.3.2}/tests/test_routes_view/dynamic/field_types/__init__.py +0 -0
  204. {bazis-2.3.0 → bazis-2.3.2}/tests/test_routes_view/dynamic/field_types/test_json__one_to_many.py +0 -0
  205. {bazis-2.3.0 → bazis-2.3.2}/tests/test_routes_view/dynamic/field_types/test_json_many_to_many.py +0 -0
  206. {bazis-2.3.0 → bazis-2.3.2}/tests/test_routes_view/dynamic/field_types/test_related__many_to_one.py +0 -0
  207. {bazis-2.3.0 → bazis-2.3.2}/tests/test_routes_view/dynamic/field_types/test_related__one_to_one.py +0 -0
  208. {bazis-2.3.0 → bazis-2.3.2}/tests/test_routes_view/dynamic/field_types/test_related_bool.py +0 -0
  209. {bazis-2.3.0 → bazis-2.3.2}/tests/test_routes_view/dynamic/field_types/test_subaggr.py +0 -0
  210. {bazis-2.3.0 → bazis-2.3.2}/tests/test_routes_view/dynamic/test_context.py +0 -0
  211. {bazis-2.3.0 → bazis-2.3.2}/tests/test_routes_view/dynamic/test_errors.py +0 -0
  212. {bazis-2.3.0 → bazis-2.3.2}/tests/test_routes_view/dynamic/test_route_filter_fields.py +0 -0
  213. {bazis-2.3.0 → bazis-2.3.2}/tests/test_routes_view/entity/__init__.py +0 -0
  214. {bazis-2.3.0 → bazis-2.3.2}/tests/test_routes_view/entity/field_types/__init__.py +0 -0
  215. {bazis-2.3.0 → bazis-2.3.2}/tests/test_routes_view/entity/field_types/test_is_exists.py +0 -0
  216. {bazis-2.3.0 → bazis-2.3.2}/tests/test_routes_view/entity/field_types/test_json.py +0 -0
  217. {bazis-2.3.0 → bazis-2.3.2}/tests/test_routes_view/entity/field_types/test_related.py +0 -0
  218. {bazis-2.3.0 → bazis-2.3.2}/tests/test_routes_view/entity/field_types/test_subaggr_aggr.py +0 -0
  219. {bazis-2.3.0 → bazis-2.3.2}/tests/test_routes_view/entity/relationships/__init__.py +0 -0
  220. {bazis-2.3.0 → bazis-2.3.2}/tests/test_routes_view/entity/relationships/test_fk.py +0 -0
  221. {bazis-2.3.0 → bazis-2.3.2}/tests/test_routes_view/entity/relationships/test_m2m.py +0 -0
  222. {bazis-2.3.0 → bazis-2.3.2}/tests/test_routes_view/entity/relationships/test_o2o.py +0 -0
  223. {bazis-2.3.0 → bazis-2.3.2}/tests/test_routes_view/entity/test_calc_property.py +0 -0
  224. {bazis-2.3.0 → bazis-2.3.2}/tests/test_routes_view/entity/test_context.py +0 -0
  225. {bazis-2.3.0 → bazis-2.3.2}/tests/test_routes_view/entity/test_errors.py +0 -0
  226. {bazis-2.3.0 → bazis-2.3.2}/tests/test_routes_view/entity/test_m2m.py +0 -0
  227. {bazis-2.3.0 → bazis-2.3.2}/tests/test_routes_view/entity/test_reverse.py +0 -0
  228. {bazis-2.3.0 → bazis-2.3.2}/tests/test_routes_view/entity/test_route_filter_fields.py +0 -0
  229. {bazis-2.3.0 → bazis-2.3.2}/tests/test_routes_view/entity/test_routes.py +0 -0
  230. {bazis-2.3.0 → bazis-2.3.2}/tests/test_routes_view/entity/test_routes_filter.py +0 -0
  231. {bazis-2.3.0 → bazis-2.3.2}/tests/test_routes_view/entity/test_routes_included.py +0 -0
  232. {bazis-2.3.0 → bazis-2.3.2}/tests/test_routes_view/entity/test_routes_pagination.py +0 -0
  233. {bazis-2.3.0 → bazis-2.3.2}/tests/test_routes_view/entity/test_routes_search.py +0 -0
  234. {bazis-2.3.0 → bazis-2.3.2}/tests/test_routes_view/entity/test_routes_sort.py +0 -0
  235. {bazis-2.3.0 → bazis-2.3.2}/tests/test_routes_view/entity/test_slice_order_by.py +0 -0
  236. {bazis-2.3.0 → bazis-2.3.2}/tests/test_routes_view/route_injection/__init__.py +0 -0
  237. {bazis-2.3.0 → bazis-2.3.2}/tests/test_routes_view/route_injection/test_custom_meta.py +0 -0
  238. {bazis-2.3.0 → bazis-2.3.2}/tests/test_routes_view/route_injection/test_custom_response_model.py +0 -0
  239. {bazis-2.3.0 → bazis-2.3.2}/tests/test_settings.py +0 -0
  240. {bazis-2.3.0 → bazis-2.3.2}/tests/test_sparse_fieldsets.py +0 -0
  241. {bazis-2.3.0 → bazis-2.3.2}/tests/utils/__init__.py +0 -0
  242. {bazis-2.3.0 → bazis-2.3.2}/tests/utils/assert_sql.py +0 -0
@@ -42,9 +42,12 @@ jobs:
42
42
  echo "::error::Tag v$VERSION already exists"
43
43
  exit 1
44
44
  fi
45
- LAST_TAG=$(git tag --list 'v*' --sort=-v:refname | head -n 1)
45
+ # versionsort.suffix: a pre-release (v2.4.0-rc1) sorts before its release (v2.4.0)
46
+ LAST_TAG=$(git -c versionsort.suffix=- tag --list 'v*' --sort=-v:refname | head -n 1)
46
47
  echo "Last release: ${LAST_TAG:-none}, new release: v$VERSION"
47
- if [ -n "$LAST_TAG" ] && [ "$(printf '%s\n%s\n' "${LAST_TAG#v}" "$VERSION" | sort -V | tail -n 1)" != "$VERSION" ]; then
48
+ # sort -V orders "~" before anything, as versionsort.suffix does with "-"
49
+ NEWEST=$(printf '%s\n%s\n' "${LAST_TAG#v}" "$VERSION" | sed 's/-/~/' | sort -V | tail -n 1)
50
+ if [ -n "$LAST_TAG" ] && [ "$NEWEST" != "${VERSION/-/\~}" ]; then
48
51
  echo "::error::Version $VERSION is not newer than $LAST_TAG"
49
52
  exit 1
50
53
  fi
@@ -43,3 +43,30 @@ Claude Code sessions cannot push tags. Release through the **Release** workflow
43
43
 
44
44
  Pick the version by semver: breaking changes (settings renamed or required, dependency
45
45
  removed, behavior changed) bump the minor version while the project is below 3.0.
46
+
47
+ ## The Bazis packages
48
+
49
+ The framework is split into packages in separate repositories of `ecofuture-tech`, each with
50
+ its own `CLAUDE.md`, Tests and Release workflows. Release them in dependency order (CI of a
51
+ package installs its Bazis dependencies from PyPI):
52
+
53
+ 1. `bazis` → `bazis-test-utils`
54
+ 2. `bazis-users`, `bazis-ws`, `bazis-bulk`, `bazis-uploadable`
55
+ 3. `bazis-author`, `bazis-authing` (users) → `bazis-permit` (users, author for its tests)
56
+ 4. `bazis-statusy` (permit), `bazis-bg` (author)
57
+ 5. `bazis-async-background` (ws, Kafka) → `bazis-async-request`
58
+
59
+ A package requires the versions of Bazis and of the sibling packages whose API or behavior it
60
+ relies on (for example a security fix it builds on); new releases of the other packages do
61
+ not raise the floors.
62
+
63
+ Security boundaries that span packages:
64
+
65
+ - Every way of changing an object must go through the checks of an update: the
66
+ relationships endpoints (`JsonapiRouteBase.relationships_change`) accept only the
67
+ relations of the update schema and call `hook_before/after_relationships_change`, which
68
+ bazis-permit uses.
69
+ - Session JWTs (bazis-users) require `exp` and `sub`; on HTTP a token without `exp` (the
70
+ store token of bazis-authing) is anonymous, bazis-ws rejects it.
71
+ - Anonymous WebSocket channels (bazis-ws) live under `user_ws:anon:`; bazis-async-background
72
+ resolves channels with the same functions.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bazis
3
- Version: 2.3.0
3
+ Version: 2.3.2
4
4
  Summary: A JsonAPI framework based on Django + FastAPI + Pydantic.
5
5
  Author-email: Ilya Kharyn <ilya.tt07@gmail.com>
6
6
  Maintainer-email: Ilya Kharyn <ilya.tt07@gmail.com>
@@ -47,7 +47,7 @@ Requires-Dist: gunicorn>=23
47
47
  Requires-Dist: itsdangerous>=2.2
48
48
  Requires-Dist: jsonref>=1.1
49
49
  Provides-Extra: test
50
- Requires-Dist: bazis-test-utils>=2.2.0; extra == "test"
50
+ Requires-Dist: bazis-test-utils>=2.3.0; extra == "test"
51
51
  Provides-Extra: dev
52
52
  Requires-Dist: ruff; extra == "dev"
53
53
  Dynamic: license-file
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '2.3.0'
22
- __version_tuple__ = version_tuple = (2, 3, 0)
21
+ __version__ = version = '2.3.2'
22
+ __version_tuple__ = version_tuple = (2, 3, 2)
23
23
 
24
- __commit_id__ = commit_id = 'ga081a2388'
24
+ __commit_id__ = commit_id = 'g4710cec49'
@@ -88,7 +88,7 @@ class RouteParams:
88
88
  class RouteContext:
89
89
  """
90
90
  An object of this class is a proxy wrapper for the route function.
91
- After a method becomes a route through one of the http\_... decorators,
91
+ After a method becomes a route through one of the http_... decorators,
92
92
  accessing it returns a RouteContext object.
93
93
  This object stores references to the original method, routing parameters,
94
94
  the endpoint function, and the route's local storage.
@@ -0,0 +1,25 @@
1
+ # Copyright 2026 EcoFuture Technology Services LLC and contributors
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from pydantic import BaseModel
16
+
17
+
18
+ def with_cache_openapi_schema(schema: type[BaseModel], lang: str = None) -> dict:
19
+ """
20
+ Returns the OpenAPI (JSON) schema of a Pydantic model. Not cached: the schemas
21
+ themselves are cached (see `bazis.core.schemas.cache`).
22
+
23
+ Tags: RAG
24
+ """
25
+ return schema.model_json_schema()
@@ -32,6 +32,7 @@ from fastapi.responses import Response
32
32
  from pydantic import BaseModel, ValidationError
33
33
 
34
34
  from bazis.core.errors import (
35
+ JsonApi403Exception,
35
36
  JsonApiBazisError,
36
37
  JsonApiBazisException,
37
38
  JsonApiHttpException,
@@ -503,13 +504,7 @@ class JsonapiRouteBase(InitialRouteBase):
503
504
  Handles the HTTP POST request to add relationships.
504
505
  """
505
506
  if relationships_data.data:
506
- self.relationships_service.apply_relationship_action(
507
- action='add',
508
- model=self.model,
509
- item_id=item_id,
510
- related_field_name=related_field_name,
511
- relationships_data=relationships_data,
512
- )
507
+ self.relationships_change('add', item_id, related_field_name, relationships_data)
513
508
  return Response(status_code=204)
514
509
 
515
510
  @http_patch(
@@ -528,13 +523,7 @@ class JsonapiRouteBase(InitialRouteBase):
528
523
  """
529
524
  Handles the HTTP PATCH request to update relationships.
530
525
  """
531
- self.relationships_service.apply_relationship_action(
532
- action='set',
533
- model=self.model,
534
- item_id=item_id,
535
- related_field_name=related_field_name,
536
- relationships_data=relationships_data,
537
- )
526
+ self.relationships_change('set', item_id, related_field_name, relationships_data)
538
527
  return Response(status_code=204)
539
528
 
540
529
  @http_delete(
@@ -554,13 +543,7 @@ class JsonapiRouteBase(InitialRouteBase):
554
543
  Handles the HTTP DELETE request to remove relationships.
555
544
  """
556
545
  if relationships_data.data:
557
- self.relationships_service.apply_relationship_action(
558
- action='remove',
559
- model=self.model,
560
- item_id=item_id,
561
- related_field_name=related_field_name,
562
- relationships_data=relationships_data,
563
- )
546
+ self.relationships_change('remove', item_id, related_field_name, relationships_data)
564
547
  return Response(status_code=204)
565
548
 
566
549
  @http_delete('/{item_id}/', inject_tags=[CrudApiAction.DESTROY], status_code=204)
@@ -717,6 +700,129 @@ class JsonapiRouteBase(InitialRouteBase):
717
700
  self.item.only_fields = only_fields
718
701
  return self.item
719
702
 
703
+ def relationships_change(
704
+ self,
705
+ action: str,
706
+ item_id: str,
707
+ related_field_name: str,
708
+ relationships_data: RelationshipData,
709
+ ):
710
+ """
711
+ Changes a relationship of an item (the relationships endpoints) with the same
712
+ checks as an update of the item: the item is taken through the route, the
713
+ relationship must be writable in the update schema of the route (for this user),
714
+ the `filter:` restrictions of the field apply, and the hooks
715
+ `hook_before_relationships_change` / `hook_after_relationships_change` run inside
716
+ the transaction.
717
+ """
718
+ with transaction.atomic(savepoint=False):
719
+ self.set_api_action(CrudApiAction.UPDATE)
720
+ item = self.set_item(str(item_id).strip())
721
+
722
+ data = self.relationships_validate(item, related_field_name, relationships_data)
723
+ self.relationships_restricts_check(item, data, related_field_name, action)
724
+
725
+ self.hook_before_relationships_change(item, data, related_field_name, action)
726
+ self.relationships_service.apply_relationship_action(
727
+ action=action,
728
+ model=self.model,
729
+ item_id=str(item.pk),
730
+ related_field_name=related_field_name,
731
+ relationships_data=relationships_data,
732
+ )
733
+ item.refresh_from_db()
734
+ self.hook_after_relationships_change(item, data, related_field_name, action)
735
+
736
+ def relationships_validate(
737
+ self, item: JsonApiMixin, related_field_name: str, relationships_data: RelationshipData
738
+ ) -> JsonApiDataSchema:
739
+ """
740
+ Validates the relationship data with the update schema of the route and returns
741
+ the validated item data. Fails with 403 if the relationship is not writable.
742
+ """
743
+ schema = self.schemas.get(CrudApiAction.UPDATE)
744
+ if schema is None:
745
+ raise JsonApi403Exception()
746
+
747
+ item_raw = {
748
+ 'data': {
749
+ 'id': str(item.pk),
750
+ 'type': self.model.get_resource_label(),
751
+ 'bs:action': CrudAccessAction.CHANGE.value,
752
+ 'relationships': {
753
+ related_field_name: relationships_data.model_dump(mode='json'),
754
+ },
755
+ }
756
+ }
757
+ try:
758
+ item_data = schema.model_validate(item_raw)
759
+ except ValidationError as e:
760
+ raise RequestValidationError(e.errors(), body=item_raw) from e
761
+
762
+ relationships = item_data.data.relationships
763
+ if relationships is None or related_field_name not in type(relationships).model_fields:
764
+ raise JsonApiBazisException(
765
+ JsonApiBazisError(
766
+ detail=f'The relationship {related_field_name} cannot be changed',
767
+ loc=('path', 'related_field_name'),
768
+ code='ERR_RELATIONSHIP_READONLY',
769
+ title='Relationship is read-only',
770
+ status=403,
771
+ ),
772
+ status=403,
773
+ )
774
+ return item_data.data
775
+
776
+ def relationships_restricts_check(
777
+ self, item: JsonApiMixin, data: JsonApiDataSchema, related_field_name: str, action: str
778
+ ):
779
+ """
780
+ Applies the `filter:` restrictions of the field: the objects the action links and
781
+ unlinks must match them (an update keeps the linked objects that do not match).
782
+ """
783
+ field_info = item.get_fields_info().relations[related_field_name]
784
+ value = data.relationships.model_dump(exclude_unset=True)[related_field_name]
785
+ rel_model = field_info.related_model
786
+
787
+ if field_info.to_many:
788
+ # normalized as the primary keys of the database (e.g. a UUID in upper case)
789
+ ids = {
790
+ str(self.relationships_service._parse_id(it['id'], rel_model))
791
+ for it in (value.get('data') or [])
792
+ }
793
+ if action == 'set':
794
+ current = getattr(item, related_field_name).values_list('pk', flat=True)
795
+ ids ^= {str(pk) for pk in current}
796
+ if not ids:
797
+ return
798
+ touched = {'data': [{'id': pk} for pk in ids]}
799
+ allowed = data.check_restrict_m2m(related_field_name, touched, rel_model)
800
+ allowed_ids = {str(pk) for pk in allowed.values_list('pk', flat=True)}
801
+ if ids - allowed_ids:
802
+ raise JsonApi403Exception()
803
+ else:
804
+ f_pk = None if action == 'remove' else (value.get('data') or {}).get('id')
805
+ if not data.check_restrict_rel(related_field_name, f_pk, rel_model, instance=item):
806
+ raise JsonApi403Exception()
807
+
808
+ def hook_before_relationships_change(
809
+ self, item: JsonApiMixin, data: JsonApiDataSchema, related_field_name: str, action: str
810
+ ):
811
+ """
812
+ Hook method called before a relationship of the item is changed through the
813
+ relationships endpoints. Can be overridden to add custom checks.
814
+ """
815
+ pass
816
+
817
+ def hook_after_relationships_change(
818
+ self, item: JsonApiMixin, data: JsonApiDataSchema, related_field_name: str, action: str
819
+ ):
820
+ """
821
+ Hook method called after a relationship of the item is changed through the
822
+ relationships endpoints (inside the transaction).
823
+ """
824
+ pass
825
+
720
826
  def hook_before_create(self, item: JsonApiMixin):
721
827
  """
722
828
  Hook method called before creating an item. Can be overridden to add custom
@@ -33,30 +33,18 @@ from typing import Any
33
33
  from pydantic import BaseModel
34
34
 
35
35
 
36
- __all__ = [
37
- 'DEFAULT_SCHEMA_CACHE_TTL',
38
- 'SCHEMAS_CACHE',
39
- 'TTLCache',
40
- 'get_schema_cache_ttl',
41
- 'get_schema_from_cache',
42
- 'set_schema_to_cache',
43
- ]
44
-
45
- DEFAULT_SCHEMA_CACHE_TTL = 3600
46
-
47
-
48
36
  def get_schema_cache_ttl() -> float:
49
37
  """
50
38
  Returns the idle lifetime of the cached schemas in seconds from the settings.
51
39
  """
52
40
  from django.conf import settings
53
41
 
54
- return getattr(settings, 'BAZIS_SCHEMA_CACHE_TTL', DEFAULT_SCHEMA_CACHE_TTL)
42
+ return settings.BAZIS_SCHEMA_CACHE_TTL
55
43
 
56
44
 
57
45
  class TTLCache:
58
46
  """
59
- Thread-safe dictionary-like cache that evicts entries not accessed for the TTL.
47
+ Thread-safe cache that evicts entries not accessed for the TTL.
60
48
 
61
49
  :param ttl: Idle lifetime in seconds, or a callable returning it; 0 disables eviction.
62
50
  :param on_evict: Optional callback called with the key and value of an evicted entry.
@@ -91,30 +79,11 @@ class TTLCache:
91
79
  self._data[key] = (value, now)
92
80
  return value
93
81
 
94
- def __getitem__(self, key: str) -> Any:
95
- marker = object()
96
- value = self.get(key, marker)
97
- if value is marker:
98
- raise KeyError(key)
99
- return value
100
-
101
82
  def __setitem__(self, key: str, value: Any) -> None:
102
83
  with self._lock:
103
84
  self._data[key] = (value, time.monotonic())
104
85
  self._sweep()
105
86
 
106
- def __contains__(self, key: str) -> bool:
107
- marker = object()
108
- return self.get(key, marker) is not marker
109
-
110
- def __len__(self) -> int:
111
- return len(self._data)
112
-
113
- def clear(self) -> None:
114
- with self._lock:
115
- for key in list(self._data):
116
- self._evict(key)
117
-
118
87
  def _evict(self, key: str) -> None:
119
88
  value, _ = self._data.pop(key)
120
89
  if self._on_evict:
@@ -81,7 +81,7 @@ class JsonApiDataSchema(BaseModel):
81
81
  restrict_q = QueryToOrm(restricts, f_model).q
82
82
  # if a value was previously set in the relation and the user has no access to it,
83
83
  # then the action is not allowed
84
- if instance and (rel_val := getattr(instance, f_name)):
84
+ if instance and (rel_val := getattr(instance, f_name, None)):
85
85
  if not f_model.objects.filter(restrict_q & Q(pk=rel_val.pk)).exists():
86
86
  return False
87
87
 
@@ -20,8 +20,6 @@ from pydantic import (
20
20
  ValidationInfo,
21
21
  )
22
22
 
23
- from typing_extensions import deprecated
24
-
25
23
  from bazis.core.models_abstract import InitialBase
26
24
 
27
25
  from .utils import get_types
@@ -55,9 +53,6 @@ def not_blank_validator(cls: type[BaseModel], v: Any, field: ValidationInfo):
55
53
  return v
56
54
 
57
55
 
58
- @deprecated(
59
- 'disabled because there are transition-type operations that require mandatory fields, which may well be read-only'
60
- )
61
56
  def readonly_validator(cls: type[BaseModel], values: Any):
62
57
  """
63
58
  Validator to remove read-only fields from the values if the schema action does
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bazis
3
- Version: 2.3.0
3
+ Version: 2.3.2
4
4
  Summary: A JsonAPI framework based on Django + FastAPI + Pydantic.
5
5
  Author-email: Ilya Kharyn <ilya.tt07@gmail.com>
6
6
  Maintainer-email: Ilya Kharyn <ilya.tt07@gmail.com>
@@ -47,7 +47,7 @@ Requires-Dist: gunicorn>=23
47
47
  Requires-Dist: itsdangerous>=2.2
48
48
  Requires-Dist: jsonref>=1.1
49
49
  Provides-Extra: test
50
- Requires-Dist: bazis-test-utils>=2.2.0; extra == "test"
50
+ Requires-Dist: bazis-test-utils>=2.3.0; extra == "test"
51
51
  Provides-Extra: dev
52
52
  Requires-Dist: ruff; extra == "dev"
53
53
  Dynamic: license-file
@@ -125,6 +125,8 @@ docs/_templates/custom-class-template.rst
125
125
  docs/_templates/custom-module-template.rst
126
126
  docs/releases/2.2.9.md
127
127
  docs/releases/2.3.0.md
128
+ docs/releases/2.3.1.md
129
+ docs/releases/2.3.2.md
128
130
  sample/generate_local_test_data.py
129
131
  sample/main.py
130
132
  sample/manage.py
@@ -185,6 +187,7 @@ tests/conftest.py
185
187
  tests/factories.py
186
188
  tests/test_core_config.py
187
189
  tests/test_models.py
190
+ tests/test_relationships_access.py
188
191
  tests/test_routes_add.py
189
192
  tests/test_routes_change.py
190
193
  tests/test_routes_core.py
@@ -29,4 +29,4 @@ jsonref>=1.1
29
29
  ruff
30
30
 
31
31
  [test]
32
- bazis-test-utils>=2.2.0
32
+ bazis-test-utils>=2.3.0
@@ -109,6 +109,8 @@
109
109
  "docs/relationships.rst",
110
110
  "docs/releases/2.2.9.md",
111
111
  "docs/releases/2.3.0.md",
112
+ "docs/releases/2.3.1.md",
113
+ "docs/releases/2.3.2.md",
112
114
  "docs/requirements.txt",
113
115
  "docs/response_models.rst",
114
116
  "docs/routes.rst",
@@ -179,6 +181,7 @@
179
181
  "tests/factories.py",
180
182
  "tests/test_core_config.py",
181
183
  "tests/test_models.py",
184
+ "tests/test_relationships_access.py",
182
185
  "tests/test_routes_add.py",
183
186
  "tests/test_routes_change.py",
184
187
  "tests/test_routes_core.py",
@@ -0,0 +1,8 @@
1
+ {
2
+ "tag": "2.3.2",
3
+ "distance": 0,
4
+ "node": "g4710cec4942971cfb3ea5b49042c0cc142a664a5",
5
+ "dirty": false,
6
+ "branch": "HEAD",
7
+ "node_date": "2026-10-01"
8
+ }
@@ -0,0 +1,29 @@
1
+ # Bazis 2.3.1
2
+
3
+ ## Security
4
+
5
+ - **The relationships endpoints changed any relation of any object.**
6
+ `POST/PATCH/DELETE /{id}/relationships/{name}` loaded the object directly from the model
7
+ and changed the relation named in the URL, without the checks of an update: the route
8
+ queryset, the update schema of the route (which relations are exposed and writable),
9
+ the `filter:` restrictions of the fields, and the checks of permission routes. For
10
+ example, any authenticated user could add himself to Django groups with all permissions
11
+ (`/users/user/<id>/relationships/groups`), and in projects with bazis-permit assign
12
+ himself any role or change objects he cannot change.
13
+
14
+ The endpoints now take the object through the route and accept only the relations of
15
+ the update schema of the route (for the current user); other names fail with 403 and
16
+ `ERR_RELATIONSHIP_READONLY`. The `filter:` restrictions apply to added and set objects,
17
+ and the change runs in a transaction between the new hooks
18
+ `hook_before_relationships_change` and `hook_after_relationships_change`, which
19
+ bazis-permit 2.3 uses for its checks. Unknown relation names fail with 403 instead
20
+ of 500.
21
+
22
+ A project that changed relations through these endpoints which its routes do not expose
23
+ must add them to the update schema of the route (`fields` for `CrudApiAction.UPDATE`).
24
+
25
+ ## Fixes
26
+
27
+ - `readonly_validator` was marked as deprecated while the schema builders use it, which
28
+ emitted a `DeprecationWarning` on every validated request.
29
+ - An invalid escape sequence in a docstring emitted a `SyntaxWarning` on import.
@@ -0,0 +1,27 @@
1
+ # Bazis 2.3.2
2
+
3
+ ## Security
4
+
5
+ - **The relationships endpoints did not apply the `filter:` restrictions to the unlinked
6
+ objects** (2.3.1 checked only the added and set ones). `DELETE /{id}/relationships/{name}`
7
+ removed objects the user may not touch, `PATCH` unlinked the objects left out of the
8
+ list, and `DELETE` of a to-one relation cleared a value the user could not change with an
9
+ update. Every object an action links or unlinks must now match the restrictions;
10
+ otherwise the request fails with 403. The objects a `PATCH` keeps are not checked.
11
+ (An update of the item keeps the linked objects that do not match the restrictions; the
12
+ relationships `PATCH` refuses to unlink them.)
13
+
14
+ ## Changes
15
+
16
+ - Ids of missing objects in a relationships request fail with 403 (since 2.3.1 for `POST` and
17
+ `PATCH`, now also for `DELETE`); malformed ids fail with 400.
18
+ - A response to a request with `include` is validated with the response schema of the
19
+ resource for every action, not only for retrieve (since 2.3.0).
20
+ - `with_cache_openapi_schema` no longer stores the schemas it returns: the cache was never
21
+ read and only held memory. `TTLCache` keeps only the methods the schema cache uses.
22
+
23
+ ## Release
24
+
25
+ - The Release workflow refused a release after its pre-release (`2.4.0` after
26
+ `2.4.0-rc1`): the versions are now compared with pre-releases before releases.
27
+ - The `test` extra requires bazis-test-utils 2.3.
@@ -64,7 +64,7 @@ dependencies = [
64
64
 
65
65
  [project.optional-dependencies]
66
66
  test = [
67
- "bazis-test-utils>=2.2.0"
67
+ "bazis-test-utils>=2.3.0"
68
68
  ]
69
69
  dev = [
70
70
  "ruff",
@@ -17,6 +17,7 @@ from django.apps import apps
17
17
  from sample.route_mixins import ContextTestRouteMixin
18
18
 
19
19
  from bazis.core.routes_abstract.jsonapi import JsonapiRouteBase
20
+ from bazis.core.schemas.enums import CrudApiAction
20
21
  from bazis.core.schemas.fields import SchemaField, SchemaFields
21
22
 
22
23
 
@@ -73,6 +74,13 @@ class ParentEntityRouteSet(JsonapiRouteBase):
73
74
  ),
74
75
  },
75
76
  ),
77
+ # writable through the relationships endpoints in the tests
78
+ CrudApiAction.UPDATE: SchemaFields(
79
+ include={
80
+ 'extended_entity_null': None,
81
+ 'dependent_entities_null': None,
82
+ },
83
+ ),
76
84
  }
77
85
 
78
86
 
@@ -0,0 +1,72 @@
1
+ # Copyright 2026 EcoFuture Technology Services LLC and contributors
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """
16
+ The relationships endpoints change only the relationships the update schema of the route
17
+ exposes, with the checks of an update.
18
+ """
19
+
20
+ import json
21
+
22
+ import pytest
23
+ from bazis_test_utils.utils import get_api_client
24
+
25
+ from tests import factories
26
+
27
+
28
+ @pytest.mark.django_db(transaction=True)
29
+ def test_relationship_not_in_schema_is_rejected(sample_app):
30
+ child = factories.ChildEntityFactory.create()
31
+ other_child = factories.ChildEntityFactory.create()
32
+ protected = factories.WithProtectedEntityFactory.create(child=other_child)
33
+ payload = {'data': [{'type': 'entity.with_protected_entity', 'id': str(protected.id)}]}
34
+ url = f'/api/v1/entity/child_entity/{child.id}/relationships/withprotectedentity_set'
35
+
36
+ client = get_api_client(sample_app)
37
+ for method in ('POST', 'PATCH', 'DELETE'):
38
+ response = client.client.request(method, url, headers=client.headers, json=payload)
39
+ assert response.status_code == 403, method
40
+ assert response.json()['errors'][0]['code'] == 'ERR_RELATIONSHIP_READONLY'
41
+
42
+ protected.refresh_from_db()
43
+ assert protected.child_id == other_child.id
44
+
45
+
46
+ @pytest.mark.django_db(transaction=True)
47
+ def test_unknown_relationship_is_rejected(sample_app):
48
+ child = factories.ChildEntityFactory.create()
49
+ response = get_api_client(sample_app).patch(
50
+ f'/api/v1/entity/child_entity/{child.id}/relationships/unknown',
51
+ data=json.dumps({'data': []}),
52
+ )
53
+ assert response.status_code == 403
54
+
55
+
56
+ @pytest.mark.django_db(transaction=True)
57
+ def test_relationship_of_missing_item(sample_app):
58
+ response = get_api_client(sample_app).patch(
59
+ '/api/v1/entity/child_entity/9b657232-4178-4d7f-8b0c-e8ab16f2b309/relationships/parent_entities',
60
+ data=json.dumps({'data': []}),
61
+ )
62
+ assert response.status_code == 404
63
+
64
+
65
+ @pytest.mark.django_db(transaction=True)
66
+ def test_relationship_of_invalid_type_is_rejected(sample_app):
67
+ child = factories.ChildEntityFactory.create()
68
+ response = get_api_client(sample_app).patch(
69
+ f'/api/v1/entity/child_entity/{child.id}/relationships/parent_entities',
70
+ data=json.dumps({'data': {'type': 'entity.parent_entity', 'id': 'not-a-list'}}),
71
+ )
72
+ assert response.status_code == 422
@@ -48,9 +48,8 @@ def test_ttl_cache_evicts_idle_entries(monkeypatch):
48
48
  # adding entries sweeps expired ones at most once per TTL
49
49
  clock.now += 11
50
50
  cache['c'] = 3
51
- assert 'a' not in cache._data
51
+ assert list(cache._data) == ['c']
52
52
  assert evicted == ['b', 'a']
53
- assert len(cache) == 1
54
53
 
55
54
 
56
55
  def test_ttl_cache_zero_ttl_never_evicts(monkeypatch):
@@ -61,8 +60,8 @@ def test_ttl_cache_zero_ttl_never_evicts(monkeypatch):
61
60
  cache['a'] = 1
62
61
  clock.now += 10**9
63
62
  cache['b'] = 2
64
- assert cache['a'] == 1
65
- assert len(cache) == 2
63
+ assert cache.get('a') == 1
64
+ assert list(cache._data) == ['a', 'b']
66
65
 
67
66
 
68
67
  def test_evicted_schema_is_removed_from_module(monkeypatch):