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.
- {bazis-2.3.0 → bazis-2.3.2}/.github/workflows/release.yml +5 -2
- {bazis-2.3.0 → bazis-2.3.2}/CLAUDE.md +27 -0
- {bazis-2.3.0 → bazis-2.3.2}/PKG-INFO +2 -2
- {bazis-2.3.0 → bazis-2.3.2}/bazis/core/_version.py +3 -3
- {bazis-2.3.0 → bazis-2.3.2}/bazis/core/routes_abstract/context.py +1 -1
- bazis-2.3.2/bazis/core/routes_abstract/jsonapi/cache.py +25 -0
- {bazis-2.3.0 → bazis-2.3.2}/bazis/core/routes_abstract/jsonapi/route_base.py +127 -21
- {bazis-2.3.0 → bazis-2.3.2}/bazis/core/schemas/cache.py +2 -33
- {bazis-2.3.0 → bazis-2.3.2}/bazis/core/schemas/schemas.py +1 -1
- {bazis-2.3.0 → bazis-2.3.2}/bazis/core/schemas/validators.py +0 -5
- {bazis-2.3.0 → bazis-2.3.2}/bazis.egg-info/PKG-INFO +2 -2
- {bazis-2.3.0 → bazis-2.3.2}/bazis.egg-info/SOURCES.txt +3 -0
- {bazis-2.3.0 → bazis-2.3.2}/bazis.egg-info/requires.txt +1 -1
- {bazis-2.3.0 → bazis-2.3.2}/bazis.egg-info/scm_file_list.json +3 -0
- bazis-2.3.2/bazis.egg-info/scm_version.json +8 -0
- bazis-2.3.2/docs/releases/2.3.1.md +29 -0
- bazis-2.3.2/docs/releases/2.3.2.md +27 -0
- {bazis-2.3.0 → bazis-2.3.2}/pyproject.toml +1 -1
- {bazis-2.3.0 → bazis-2.3.2}/sample/entity/routes.py +8 -0
- bazis-2.3.2/tests/test_relationships_access.py +72 -0
- {bazis-2.3.0 → bazis-2.3.2}/tests/test_schema_cache.py +3 -4
- bazis-2.3.0/bazis/core/routes_abstract/jsonapi/cache.py +0 -44
- bazis-2.3.0/bazis.egg-info/scm_version.json +0 -8
- {bazis-2.3.0 → bazis-2.3.2}/.dockerignore +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/.github/workflows/build-publish.yml +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/.github/workflows/tests.yml +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/.github/workflows/update-licenses.yml +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/.gitignore +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/LICENSE +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/NOTICE +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/README.md +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/bazis/core/__init__.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/bazis/core/admin_abstract.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/bazis/core/app.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/bazis/core/app_factory.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/bazis/core/apps.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/bazis/core/checks.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/bazis/core/conf.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/bazis/core/configure.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/bazis/core/constance_conf/__init__.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/bazis/core/constance_conf/base.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/bazis/core/errors.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/bazis/core/fields.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/bazis/core/i18n.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/bazis/core/locale/en/LC_MESSAGES/django.mo +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/bazis/core/locale/en/LC_MESSAGES/django.po +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/bazis/core/locale/messages.pot +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/bazis/core/locale/ru/LC_MESSAGES/django.mo +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/bazis/core/locale/ru/LC_MESSAGES/django.po +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/bazis/core/mail.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/bazis/core/management/__init__.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/bazis/core/management/commands/__init__.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/bazis/core/management/commands/admin_create.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/bazis/core/management/commands/schemas_build.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/bazis/core/management/commands/schemas_build_lang.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/bazis/core/migrations/0001_initial.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/bazis/core/migrations/0002_auto_20260114_1222.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/bazis/core/migrations/0003_rename_email_settings.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/bazis/core/migrations/__init__.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/bazis/core/models_abstract.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/bazis/core/router.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/bazis/core/routes_abstract/__init__.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/bazis/core/routes_abstract/initial.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/bazis/core/routes_abstract/jsonapi/__init__.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/bazis/core/routes_abstract/jsonapi/callbacks.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/bazis/core/routes_abstract/jsonapi/mixins.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/bazis/core/routes_abstract/jsonapi/schemas.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/bazis/core/routes_abstract/jsonapi/schemas_dict.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/bazis/core/routes_abstract/jsonapi/services.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/bazis/core/routing.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/bazis/core/schemas/__init__.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/bazis/core/schemas/builders.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/bazis/core/schemas/enums.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/bazis/core/schemas/factory.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/bazis/core/schemas/fields.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/bazis/core/schemas/meta.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/bazis/core/schemas/utils.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/bazis/core/services/__init__.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/bazis/core/services/filtering.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/bazis/core/services/includes.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/bazis/core/services/meta_fields.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/bazis/core/services/pagination.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/bazis/core/services/relationships.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/bazis/core/services/route_ctx.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/bazis/core/services/searching.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/bazis/core/services/sorting.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/bazis/core/services/sparse_fieldsets.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/bazis/core/triggers.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/bazis/core/urls.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/bazis/core/utils/__init__.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/bazis/core/utils/apps.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/bazis/core/utils/django_types.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/bazis/core/utils/fastapi_monkey_patch.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/bazis/core/utils/files.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/bazis/core/utils/functools.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/bazis/core/utils/imp.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/bazis/core/utils/locale.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/bazis/core/utils/logging_level.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/bazis/core/utils/model_meta.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/bazis/core/utils/network.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/bazis/core/utils/orm.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/bazis/core/utils/pghistory_monkey_patch.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/bazis/core/utils/query_complex.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/bazis/core/utils/schemas.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/bazis/core/utils/sets_order.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/bazis/core/utils/translated_field_utils.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/bazis/core/utils/triggers.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/bazis.egg-info/dependency_links.txt +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/bazis.egg-info/top_level.txt +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/docs/Makefile +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/docs/README.md +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/docs/_static/pydata-custom.css +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/docs/_static/readthedocs-custom.css +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/docs/_templates/custom-class-template.rst +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/docs/_templates/custom-module-template.rst +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/docs/conf.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/docs/dynamic_fields.rst +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/docs/endpoint_callbacks.rst +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/docs/generate_dynamic_rst.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/docs/index.rst +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/docs/make.bat +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/docs/manage.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/docs/openapi.rst +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/docs/relationships.rst +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/docs/releases/2.2.9.md +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/docs/releases/2.3.0.md +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/docs/requirements.txt +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/docs/response_models.rst +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/docs/routes.rst +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/docs/services.rst +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/docs/setting.rst +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/docs/settings.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/docs/sparse_fieldsets.rst +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/docs/structure.rst +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/pytest.ini +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/sample/deploy/admin.sh +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/sample/deploy/app.sh +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/sample/deploy/config/admin.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/sample/deploy/config/app.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/sample/deploy/db.sh +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/sample/deploy/init.sh +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/sample/dynamic/__init__.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/sample/dynamic/migrations/0001_initial.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/sample/dynamic/migrations/__init__.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/sample/dynamic/models.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/sample/dynamic/router.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/sample/dynamic/routes.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/sample/entity/__init__.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/sample/entity/admin.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/sample/entity/migrations/0001_initial.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/sample/entity/migrations/0002_parententity_field_parententity_state.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/sample/entity/migrations/0003_country_driver_vehiclebrand_vehicle_carriertask_and_more.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/sample/entity/migrations/0004_division_organization_driver_divisions_and_more.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/sample/entity/migrations/0005_extendedentitynull.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/sample/entity/migrations/0006_dependententitynull.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/sample/entity/migrations/0007_withprotectedentity.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/sample/entity/migrations/0008_withprotectedentitysystem.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/sample/entity/migrations/__init__.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/sample/entity/models.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/sample/entity/router.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/sample/generate_local_test_data.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/sample/main.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/sample/manage.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/sample/project.env +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/sample/pyproject.toml +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/sample/route_injection/__init__.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/sample/route_injection/migrations/0001_initial.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/sample/route_injection/migrations/__init__.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/sample/route_injection/models.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/sample/route_injection/route_bases.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/sample/route_injection/router.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/sample/route_injection/routes.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/sample/route_injection/schemas.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/sample/route_injection/services.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/sample/sample/__init__.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/sample/sample/main.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/sample/sample/route_mixins.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/sample/sample/router.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/sample/sample/settings.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/sample/sample/urls.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/sample/sample/wsgi.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/sample/sparse_fieldsets/__init__.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/sample/sparse_fieldsets/migrations/0001_initial.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/sample/sparse_fieldsets/migrations/0002_category_article_category.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/sample/sparse_fieldsets/migrations/__init__.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/sample/sparse_fieldsets/models.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/sample/sparse_fieldsets/router.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/sample/sparse_fieldsets/routes.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/sample/uv.lock +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/setup.cfg +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/tests/__init__.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/tests/conftest.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/tests/factories.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/tests/test_core_config.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/tests/test_models.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/tests/test_routes_add.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/tests/test_routes_change.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/tests/test_routes_core.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/tests/test_routes_delete.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/tests/test_routes_errors.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/tests/test_routes_view/__init__.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/tests/test_routes_view/dynamic/__init__.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/tests/test_routes_view/dynamic/field_types/__init__.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/tests/test_routes_view/dynamic/field_types/test_json__one_to_many.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/tests/test_routes_view/dynamic/field_types/test_json_many_to_many.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/tests/test_routes_view/dynamic/field_types/test_related__many_to_one.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/tests/test_routes_view/dynamic/field_types/test_related__one_to_one.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/tests/test_routes_view/dynamic/field_types/test_related_bool.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/tests/test_routes_view/dynamic/field_types/test_subaggr.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/tests/test_routes_view/dynamic/test_context.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/tests/test_routes_view/dynamic/test_errors.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/tests/test_routes_view/dynamic/test_route_filter_fields.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/tests/test_routes_view/entity/__init__.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/tests/test_routes_view/entity/field_types/__init__.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/tests/test_routes_view/entity/field_types/test_is_exists.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/tests/test_routes_view/entity/field_types/test_json.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/tests/test_routes_view/entity/field_types/test_related.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/tests/test_routes_view/entity/field_types/test_subaggr_aggr.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/tests/test_routes_view/entity/relationships/__init__.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/tests/test_routes_view/entity/relationships/test_fk.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/tests/test_routes_view/entity/relationships/test_m2m.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/tests/test_routes_view/entity/relationships/test_o2o.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/tests/test_routes_view/entity/test_calc_property.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/tests/test_routes_view/entity/test_context.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/tests/test_routes_view/entity/test_errors.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/tests/test_routes_view/entity/test_m2m.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/tests/test_routes_view/entity/test_reverse.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/tests/test_routes_view/entity/test_route_filter_fields.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/tests/test_routes_view/entity/test_routes.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/tests/test_routes_view/entity/test_routes_filter.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/tests/test_routes_view/entity/test_routes_included.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/tests/test_routes_view/entity/test_routes_pagination.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/tests/test_routes_view/entity/test_routes_search.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/tests/test_routes_view/entity/test_routes_sort.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/tests/test_routes_view/entity/test_slice_order_by.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/tests/test_routes_view/route_injection/__init__.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/tests/test_routes_view/route_injection/test_custom_meta.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/tests/test_routes_view/route_injection/test_custom_response_model.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/tests/test_settings.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/tests/test_sparse_fieldsets.py +0 -0
- {bazis-2.3.0 → bazis-2.3.2}/tests/utils/__init__.py +0 -0
- {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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
22
|
-
__version_tuple__ = version_tuple = (2, 3,
|
|
21
|
+
__version__ = version = '2.3.2'
|
|
22
|
+
__version_tuple__ = version_tuple = (2, 3, 2)
|
|
23
23
|
|
|
24
|
-
__commit_id__ = commit_id = '
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
42
|
+
return settings.BAZIS_SCHEMA_CACHE_TTL
|
|
55
43
|
|
|
56
44
|
|
|
57
45
|
class TTLCache:
|
|
58
46
|
"""
|
|
59
|
-
Thread-safe
|
|
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.
|
|
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.
|
|
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
|
|
@@ -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,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.
|
|
@@ -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
|
|
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
|
|
65
|
-
assert
|
|
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):
|