├── .coveragerc ├── .github └── workflows │ ├── documentation.yaml │ ├── python-publish.yml │ └── testing.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .readthedocs.yaml ├── LICENSE ├── MANIFEST.in ├── README.md ├── codecov.yaml ├── docs ├── Makefile ├── api_filtering_example.rst ├── api_limited_methods_example.rst ├── atomic_operations.rst ├── changelog.rst ├── client_generated_id.rst ├── conf.py ├── configuration.rst ├── custom_sql_filtering.rst ├── data_layer.rst ├── errors.rst ├── fastapi-jsonapi.rst ├── filtering.rst ├── http_snippets │ ├── README.md │ ├── api_filtering__get_users.json │ ├── api_filtering__get_users__filter_word_contains_in_array.json │ ├── api_filtering__get_users__filter_word_in_array.json │ ├── api_filtering__get_users__filter_words_in_array.json │ ├── example_atomic_fail__create_computer_and_update_user_bio.json │ ├── example_atomic_five__mixed_actions.json │ ├── example_atomic_five__only_remove_actions.json │ ├── example_atomic_four__create_many-to-many.json │ ├── example_atomic_four__get-many-to-many_with_includes.json │ ├── example_atomic_one__create_computer_and_separate_user.json │ ├── example_atomic_three__create_user_and_user_bio.json │ ├── example_atomic_two__after_update_computer_check_details.json │ ├── example_atomic_two__update_computer.json │ ├── example_atomic_two__update_user.json │ ├── example_one_api__create_computer_for_user.json │ ├── example_one_api__create_user.json │ ├── example_one_api__get_user.json │ ├── example_one_api__get_user_with_computers.json │ ├── example_one_api__get_users.json │ ├── example_one_api__get_users_with_computers.json │ ├── includes__many_to_many.json │ ├── minimal_api__create_user.json │ ├── minimal_api__delete_user.json │ ├── minimal_api__get_user.json │ ├── minimal_api__get_users.json │ ├── minimal_api__patch_user.json │ ├── relationship_api__create_computer.json │ ├── relationship_api__create_computer_relationship_for_user.json │ ├── relationship_api__create_user_with_computer_relationship.json │ ├── relationship_api__delete_computer.json │ ├── relationship_api__delete_computer_relationship.json │ ├── relationship_api__get_computers.json │ ├── relationship_api__get_user_related_computers.json │ ├── relationship_api__get_user_with_computers.json │ ├── relationship_api__patch_computer.json │ ├── relationship_api__update_user_with_computer_relationship.json │ ├── run_and_create.sh │ ├── snippets │ │ ├── .gitignore │ │ ├── api_filtering__get_users │ │ ├── api_filtering__get_users__filter_word_contains_in_array │ │ ├── api_filtering__get_users__filter_word_contains_in_array_result │ │ ├── api_filtering__get_users__filter_word_in_array │ │ ├── api_filtering__get_users__filter_word_in_array_result │ │ ├── api_filtering__get_users__filter_words_in_array │ │ ├── api_filtering__get_users__filter_words_in_array_result │ │ ├── api_filtering__get_users_result │ │ ├── client_generated_id__create_user │ │ ├── client_generated_id__create_user_result │ │ ├── errors__create_user │ │ ├── errors__create_user_result │ │ ├── example_atomic_fail__create_computer_and_update_user_bio │ │ ├── example_atomic_fail__create_computer_and_update_user_bio_result │ │ ├── example_atomic_five__mixed_actions │ │ ├── example_atomic_five__mixed_actions_result │ │ ├── example_atomic_five__only_remove_actions │ │ ├── example_atomic_five__only_remove_actions_result │ │ ├── example_atomic_four__create_many-to-many │ │ ├── example_atomic_four__create_many-to-many_result │ │ ├── example_atomic_four__get-many-to-many_with_includes │ │ ├── example_atomic_four__get-many-to-many_with_includes_result │ │ ├── example_atomic_one__create_computer_and_separate_user │ │ ├── example_atomic_one__create_computer_and_separate_user_result │ │ ├── example_atomic_three__create_user_and_user_bio │ │ ├── example_atomic_three__create_user_and_user_bio_result │ │ ├── example_atomic_two__after_update_computer_check_details │ │ ├── example_atomic_two__after_update_computer_check_details_result │ │ ├── example_atomic_two__update_computer │ │ ├── example_atomic_two__update_computer_result │ │ ├── example_atomic_two__update_user │ │ ├── example_atomic_two__update_user_result │ │ ├── example_one_api__create_computer_for_user │ │ ├── example_one_api__create_computer_for_user_result │ │ ├── example_one_api__create_user │ │ ├── example_one_api__create_user_result │ │ ├── example_one_api__get_user │ │ ├── example_one_api__get_user_result │ │ ├── example_one_api__get_user_with_computers │ │ ├── example_one_api__get_user_with_computers_result │ │ ├── example_one_api__get_users │ │ ├── example_one_api__get_users_result │ │ ├── example_one_api__get_users_with_computers │ │ ├── example_one_api__get_users_with_computers_result │ │ ├── includes__many_to_many │ │ ├── includes__many_to_many_result │ │ ├── minimal_api__create_user │ │ ├── minimal_api__create_user_result │ │ ├── minimal_api__delete_user │ │ ├── minimal_api__delete_user_result │ │ ├── minimal_api__get_user │ │ ├── minimal_api__get_user_result │ │ ├── minimal_api__get_users │ │ ├── minimal_api__get_users_result │ │ ├── minimal_api__patch_user │ │ ├── minimal_api__patch_user_result │ │ ├── relationship_api__create_computer │ │ ├── relationship_api__create_computer_relationship_for_user │ │ ├── relationship_api__create_computer_relationship_for_user_result │ │ ├── relationship_api__create_computer_result │ │ ├── relationship_api__create_user_with_computer_relationship │ │ ├── relationship_api__create_user_with_computer_relationship_result │ │ ├── relationship_api__delete_computer │ │ ├── relationship_api__delete_computer_relationship │ │ ├── relationship_api__delete_computer_relationship_result │ │ ├── relationship_api__delete_computer_result │ │ ├── relationship_api__get_computers │ │ ├── relationship_api__get_computers_result │ │ ├── relationship_api__get_user_related_computers │ │ ├── relationship_api__get_user_related_computers_result │ │ ├── relationship_api__get_user_with_computers │ │ ├── relationship_api__get_user_with_computers_as_relationship │ │ ├── relationship_api__get_user_with_computers_as_relationship_result │ │ ├── relationship_api__get_user_with_computers_result │ │ ├── relationship_api__patch_computer │ │ ├── relationship_api__patch_computer_result │ │ ├── relationship_api__update_user_with_computer_relationship │ │ ├── relationship_api__update_user_with_computer_relationship_result │ │ ├── view_dependencies__get_items_forbidden │ │ ├── view_dependencies__get_items_forbidden_result │ │ ├── view_dependencies__get_items_with_permissions │ │ └── view_dependencies__get_items_with_permissions_result │ └── update_snippets_with_responses.py ├── img │ └── schema.png ├── include_many_to_many.rst ├── include_related_objects.rst ├── index.rst ├── installation.rst ├── logical_data_abstraction.rst ├── minimal_api_example.rst ├── minimal_api_head.rst ├── oauth.rst ├── pagination.rst ├── permission.rst ├── python_snippets │ ├── client_generated_id │ │ └── schematic_example.py │ ├── data_layer │ │ └── custom_data_layer.py │ ├── relationships │ │ ├── models.py │ │ └── relationships_info_example.py │ ├── routing │ │ └── router.py │ └── view_dependencies │ │ ├── main_example.py │ │ └── several_dependencies.py ├── quickstart.rst ├── relationships.rst ├── requirements.txt ├── routing.rst ├── sorting.rst ├── sparse_fieldsets.rst ├── updated_includes_example.rst └── view_dependencies.rst ├── examples ├── __init__.py ├── api_for_sqlalchemy │ ├── README.md │ ├── __init__.py │ ├── api │ │ ├── __init__.py │ │ └── views_base.py │ ├── config.py │ ├── enums │ │ ├── __init__.py │ │ ├── enums.py │ │ └── user.py │ ├── main.py │ ├── models │ │ ├── __init__.py │ │ ├── base.py │ │ ├── child.py │ │ ├── computer.py │ │ ├── db.py │ │ ├── parent.py │ │ ├── parent_to_child_association.py │ │ ├── post.py │ │ ├── post_comment.py │ │ ├── user.py │ │ ├── user_bio.py │ │ └── workplace.py │ ├── schemas │ │ ├── __init__.py │ │ ├── child.py │ │ ├── computer.py │ │ ├── parent.py │ │ ├── parent_to_child_association.py │ │ ├── post.py │ │ ├── post_comment.py │ │ ├── user.py │ │ ├── user_bio.py │ │ └── workplace.py │ └── urls.py ├── api_minimal.py └── misc │ ├── __init__.py │ └── custom_filter_example.py ├── fastapi_jsonapi ├── VERSION ├── __init__.py ├── api │ ├── __init__.py │ ├── application_builder.py │ ├── endpoint_builder.py │ └── schemas.py ├── atomic │ ├── __init__.py │ ├── atomic.py │ ├── atomic_handler.py │ ├── prepared_atomic_operation.py │ └── schemas.py ├── common.py ├── data_layers │ ├── __init__.py │ ├── base.py │ ├── fields │ │ ├── __init__.py │ │ ├── enums.py │ │ └── mixins.py │ └── sqla │ │ ├── __init__.py │ │ ├── base_model.py │ │ ├── orm.py │ │ └── query_building.py ├── data_typing.py ├── exceptions │ ├── __init__.py │ ├── base.py │ ├── handlers.py │ └── json_api.py ├── misc │ ├── __init__.py │ └── sqla │ │ ├── __init__.py │ │ └── generics │ │ ├── __init__.py │ │ └── base.py ├── py.typed ├── querystring.py ├── schema.py ├── schema_base.py ├── schema_builder.py ├── signature.py ├── storages │ ├── __init__.py │ ├── models_storage.py │ ├── schemas_storage.py │ └── views_storage.py ├── types_metadata │ ├── __init__.py │ ├── client_can_set_id.py │ ├── custom_filter_sql.py │ ├── custom_sort_sql.py │ └── relationship_info.py ├── utils │ ├── __init__.py │ ├── dependency_helper.py │ ├── exceptions.py │ └── metadata_instance_search.py ├── validation_utils.py └── views │ ├── __init__.py │ ├── enums.py │ ├── schemas.py │ └── view_base.py ├── poetry.lock ├── pyproject.toml └── tests ├── __init__.py ├── common.py ├── common_user_api_test.py ├── conftest.py ├── fixtures ├── __init__.py ├── app.py ├── db_connection.py ├── debug_app.py ├── entities.py ├── models │ ├── __init__.py │ ├── alpha.py │ ├── beta.py │ ├── beta_delta_binding.py │ ├── beta_gamma_binding.py │ ├── cascade_case.py │ ├── contains_timestamp.py │ ├── custom_uuid_item.py │ ├── delta.py │ ├── gamma.py │ ├── self_relationship.py │ └── task.py ├── schemas │ ├── __init__.py │ ├── alpha.py │ ├── beta.py │ ├── cascade_case.py │ ├── custom_uuid.py │ ├── delta.py │ ├── gamma.py │ ├── self_relationship.py │ └── task.py ├── user.py └── views.py ├── misc ├── __init__.py └── utils.py ├── test_api ├── __init__.py ├── test_api_sqla_with_includes.py ├── test_custom_body_dependency.py ├── test_filter_by_inner_json_schema.py ├── test_routers.py └── test_validators.py ├── test_atomic ├── __init__.py ├── conftest.py ├── test_create_objects.py ├── test_current_atomic_operation.py ├── test_delete_objects.py ├── test_dependencies.py ├── test_mixed_atomic.py ├── test_request.py ├── test_response.py └── test_update_objects.py ├── test_data_layers ├── __init__.py └── test_filtering │ ├── __init__.py │ └── test_sqlalchemy.py ├── test_fastapi_jsonapi ├── __init__.py └── test_querystring.py └── test_utils ├── __init__.py └── test_dependency_helper.py /.coveragerc: -------------------------------------------------------------------------------- 1 | # .coveragerc to control coverage.py 2 | [run] 3 | concurrency = greenlet 4 | branch = True 5 | omit = 6 | tests/* 7 | examples/* 8 | docs/* 9 | # omit anything in a .local directory anywhere 10 | # */.local/* 11 | # omit everything in /usr 12 | # /usr/* 13 | # omit this single file 14 | # utils/tirefire.py 15 | 16 | [report] 17 | # Regexes for lines to exclude from consideration 18 | exclude_also = 19 | # Don't complain about missing debug-only code: 20 | def __repr__ 21 | if self\.debug 22 | 23 | # Don't complain if tests don't hit defensive assertion code: 24 | raise AssertionError 25 | raise NotImplementedError 26 | 27 | # Don't complain if non-runnable code isn't run: 28 | if 0: 29 | if __name__ == .__main__.: 30 | if TYPE_CHECKING: 31 | 32 | # Don't complain about abstract methods, they aren't run: 33 | @(abc\.)?abstractmethod 34 | 35 | # no cover 36 | pragma: no cover 37 | 38 | ignore_errors = True 39 | 40 | [html] 41 | directory = coverage_html_report 42 | -------------------------------------------------------------------------------- /.github/workflows/documentation.yaml: -------------------------------------------------------------------------------- 1 | # https://coderefinery.github.io/documentation/gh_workflow/ 2 | 3 | name: 📖 Docs 4 | on: 5 | - push 6 | # - pull_request 7 | # - workflow_dispatch 8 | 9 | permissions: 10 | contents: write 11 | 12 | jobs: 13 | docs: 14 | # if: ${{ !github.event.act }} # skip during local actions testing 15 | # if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@v3 19 | - uses: actions/setup-python@v3 20 | - name: Install dependencies 21 | run: | 22 | pip install -r docs/requirements.txt 23 | - name: Sphinx build 24 | run: | 25 | sphinx-build docs _build 26 | - name: Deploy 27 | uses: peaceiris/actions-gh-pages@v3 28 | if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} 29 | with: 30 | publish_branch: gh-pages 31 | github_token: ${{ secrets.GITHUB_TOKEN }} 32 | publish_dir: _build/ 33 | force_orphan: true 34 | -------------------------------------------------------------------------------- /.github/workflows/python-publish.yml: -------------------------------------------------------------------------------- 1 | name: 🚀 Publish to PyPI 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | permissions: 8 | contents: read 9 | 10 | jobs: 11 | pypi-publish: 12 | if: ${{ !github.event.act }} # skip during local actions testing 13 | 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v3 18 | 19 | - name: Set up Python 🐍 20 | uses: actions/setup-python@v3 21 | with: 22 | python-version: '3.12' 23 | 24 | - name: Install Hatch 🐣 25 | run: pip install --upgrade pip setuptools wheel twine "hatch==1.7.0" 26 | 27 | - name: Build 🔨 28 | run: hatch build 29 | 30 | - name: 🚀 Publish 31 | env: 32 | TWINE_USERNAME: __token__ 33 | TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} 34 | run: twine upload dist/* 35 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/mirrors-mypy 3 | rev: 'bbc3dc1' 4 | hooks: 5 | - id: mypy 6 | args: 7 | - --check-untyped-defs 8 | - --ignore-missing-imports 9 | - --install-types 10 | - --non-interactive 11 | - --scripts-are-modules 12 | - --warn-unused-ignores 13 | stages: 14 | - manual 15 | 16 | - repo: https://github.com/pre-commit/pre-commit-hooks 17 | rev: "v4.1.0" 18 | hooks: 19 | - id: trailing-whitespace 20 | - id: end-of-file-fixer 21 | - id: check-yaml 22 | - id: check-added-large-files 23 | - id: mixed-line-ending 24 | - id: requirements-txt-fixer 25 | - id: pretty-format-json 26 | exclude: "docs/" 27 | 28 | - repo: https://github.com/psf/black 29 | rev: "25.1.0" 30 | hooks: 31 | - id: black 32 | 33 | - repo: https://github.com/charliermarsh/ruff-pre-commit 34 | rev: "v0.9.4" 35 | hooks: 36 | - id: ruff 37 | args: [--fix, --exit-non-zero-on-fix, --unsafe-fixes] 38 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # Read the Docs configuration file for Sphinx projects 2 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 3 | 4 | # Required 5 | version: 2 6 | 7 | # Set the OS, Python version and other tools you might need 8 | build: 9 | os: ubuntu-22.04 10 | tools: 11 | python: "3.12" 12 | # You can also specify other tool versions: 13 | # nodejs: "20" 14 | # rust: "1.70" 15 | # golang: "1.20" 16 | 17 | # Build documentation in the "docs/" directory with Sphinx 18 | sphinx: 19 | configuration: docs/conf.py 20 | # You can configure Sphinx to use a different builder, for instance use the dirhtml builder for simpler URLs 21 | # builder: "dirhtml" 22 | # Fail on all warnings to avoid broken references 23 | # fail_on_warning: true 24 | 25 | # Optionally build your docs in additional formats such as PDF and ePub 26 | formats: 27 | # pdf build fails and stops doc update 28 | # - pdf 29 | - epub 30 | - htmlzip 31 | 32 | # Optional but recommended, declare the Python requirements required 33 | # to build your documentation 34 | # See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html 35 | python: 36 | install: 37 | - requirements: docs/requirements.txt 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Sebastián Ramírez 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | include LICENSE 3 | include requirements.txt 4 | -------------------------------------------------------------------------------- /codecov.yaml: -------------------------------------------------------------------------------- 1 | codecov: 2 | require_ci_to_pass: yes 3 | branch: main 4 | 5 | coverage: 6 | precision: 2 7 | round: down 8 | range: "90..100" 9 | 10 | parsers: 11 | gcov: 12 | branch_detection: 13 | conditional: yes 14 | loop: yes 15 | method: no 16 | macro: no 17 | 18 | comment: 19 | layout: "reach,diff,flags,tree" 20 | behavior: default 21 | require_changes: no 22 | branches: 23 | - main 24 | after_n_builds: 8 # 4 python versions by 2 dbs 25 | -------------------------------------------------------------------------------- /docs/api_filtering_example.rst: -------------------------------------------------------------------------------- 1 | Filtering API example 2 | ====================== 3 | 4 | .. literalinclude:: ../examples/custom_filter_example.py 5 | :language: python 6 | 7 | 8 | 9 | Filter by jsonb contains. Before using the filter, you must define it and apply it 10 | to the schema as shown here :ref:`custom_sql_filtering`. Some useful filters are 11 | defined in module **fastapi_jsonapi.types_metadata.custom_filter_sql.py** 12 | 13 | .. code-block:: json 14 | 15 | [ 16 | { 17 | "name": "words", 18 | "op": "sqlite_json_contains", 19 | "val": {"location": "Moscow", "spam": "eggs"} 20 | } 21 | ] 22 | 23 | Request: 24 | 25 | .. literalinclude:: ./http_snippets/snippets/api_filtering__get_users__filter_word_contains_in_array 26 | :language: HTTP 27 | 28 | Response: 29 | 30 | .. literalinclude:: ./http_snippets/snippets/api_filtering__get_users__filter_word_contains_in_array_result 31 | :language: HTTP 32 | 33 | 34 | Other examples 35 | -------------- 36 | 37 | .. code-block:: python 38 | 39 | # pseudo-code 40 | 41 | class User: 42 | name: str = ... 43 | words: list[str] = ... 44 | 45 | 46 | Filter by word 47 | 48 | .. code-block:: json 49 | 50 | [ 51 | { 52 | "name": "words", 53 | "op": "in", 54 | "val": "spam" 55 | } 56 | ] 57 | 58 | Request: 59 | 60 | .. literalinclude:: ./http_snippets/snippets/api_filtering__get_users__filter_word_in_array 61 | :language: HTTP 62 | 63 | Response: 64 | 65 | .. literalinclude:: ./http_snippets/snippets/api_filtering__get_users__filter_word_in_array_result 66 | :language: HTTP 67 | 68 | 69 | Filter by words 70 | 71 | .. code-block:: json 72 | 73 | [ 74 | { 75 | "name": "words", 76 | "op": "in", 77 | "val": ["bar", "eggs"] 78 | } 79 | ] 80 | 81 | Request: 82 | 83 | .. literalinclude:: ./http_snippets/snippets/api_filtering__get_users__filter_words_in_array 84 | :language: HTTP 85 | 86 | Response: 87 | 88 | .. literalinclude:: ./http_snippets/snippets/api_filtering__get_users__filter_words_in_array_result 89 | :language: HTTP 90 | -------------------------------------------------------------------------------- /docs/api_limited_methods_example.rst: -------------------------------------------------------------------------------- 1 | .. _api_limited_methods_example: 2 | 3 | Limit API methods 4 | ################# 5 | 6 | Sometimes you won't need all the CRUD methods. 7 | For example, you want to create only GET, POST and GET LIST methods, 8 | so user can't update or delete any items. 9 | 10 | 11 | Set ``operations`` on Routers registration: 12 | 13 | .. code-block:: python 14 | 15 | builder = ApplicationBuilder(app) 16 | builder.add_resource( 17 | router=router, 18 | path="/users", 19 | tags=["User"], 20 | view=UserView, 21 | schema=UserSchema, 22 | model=User, 23 | resource_type="user", 24 | operations=[ 25 | Operation.GET_LIST, 26 | Operation.POST, 27 | Operation.GET, 28 | ], 29 | ) 30 | 31 | 32 | This will limit generated views to: 33 | 34 | ======================== ====== ============= =========================== 35 | URL method endpoint Usage 36 | ======================== ====== ============= =========================== 37 | /users GET user_list Get a collection of users 38 | /users POST user_list Create a user 39 | /users/{user_id} GET user_detail Get user details 40 | ======================== ====== ============= =========================== 41 | 42 | 43 | Full code example (should run "as is"): 44 | 45 | .. literalinclude:: ../examples/api_limited_methods.py 46 | :language: python 47 | -------------------------------------------------------------------------------- /docs/client_generated_id.rst: -------------------------------------------------------------------------------- 1 | .. _client_generated_id: 2 | 3 | Client generated id 4 | =================== 5 | 6 | .. currentmodule:: fastapi_jsonapi 7 | 8 | According to the specification `JSON:API doc `_ 9 | it is possible to create an ``id`` on the client and pass 10 | it to the server. Let's define the id type as a UUID. 11 | 12 | Request: 13 | 14 | .. literalinclude:: ./http_snippets/snippets/client_generated_id__create_user 15 | :language: http 16 | 17 | Response: 18 | 19 | .. literalinclude:: ./http_snippets/snippets/client_generated_id__create_user_result 20 | :language: http 21 | 22 | 23 | In order to do this you need to define an ``id`` with the Field keyword **client_can_set_id** in the 24 | ``schema`` or ``schema_in_post``. 25 | 26 | Example: 27 | 28 | .. literalinclude:: ./python_snippets/client_generated_id/schematic_example.py 29 | :language: python 30 | 31 | In case the key **client_can_set_id** is not set, the ``id`` field will be ignored in post requests. 32 | 33 | In fact, the library deviates slightly from the specification and allows you to use any type, not just UUID. 34 | Just define the one you need in the Pydantic model to do it. 35 | -------------------------------------------------------------------------------- /docs/configuration.rst: -------------------------------------------------------------------------------- 1 | .. _configuration: 2 | 3 | Configuration 4 | ============= 5 | 6 | You have access to 5 configuration keys: 7 | 8 | * PAGE_SIZE: the number of items in a page (default is 30) 9 | * MAX_PAGE_SIZE: the maximum page size. If you specify a page size greater than this value you will receive a 400 Bad Request response. 10 | * MAX_INCLUDE_DEPTH: the maximum length of an include through schema relationships 11 | * ALLOW_DISABLE_PAGINATION: if you want to disallow to disable pagination you can set this configuration key to False 12 | -------------------------------------------------------------------------------- /docs/custom_sql_filtering.rst: -------------------------------------------------------------------------------- 1 | .. _custom_sql_filtering: 2 | 3 | 4 | Custom SQL filtering 5 | #################### 6 | 7 | .. currentmodule:: fastapi_jsonapi 8 | 9 | Sometimes you need custom filtering that's not supported natively. 10 | You can define new filtering rules as in this example: 11 | 12 | 13 | 14 | Prepare pydantic schema which is used in ApplicationBuilder as schema 15 | ----------------------------------------------------------------- 16 | 17 | .. literalinclude:: ../examples/misc/custom_filter_example.py 18 | :language: python 19 | 20 | 21 | Declare models as usual, create routes as usual. 22 | 23 | Search for objects 24 | ------------------ 25 | 26 | 27 | .. note:: 28 | Note that url has to be quoted. It's unquoted only for an example 29 | 30 | Request: 31 | 32 | 33 | .. sourcecode:: http 34 | 35 | GET /pictures?filter=[{"name":"picture","op":"sqlite_json_ilike","val":["meta": "Moscow"]}] HTTP/1.1 36 | Accept: application/vnd.api+json 37 | 38 | 39 | Filter value has to be a valid JSON: 40 | 41 | .. sourcecode:: JSON 42 | 43 | [ 44 | { 45 | "name":"picture", 46 | "op":"sqlite_json_ilike", 47 | "val":["meta": "Moscow"] 48 | } 49 | ] 50 | -------------------------------------------------------------------------------- /docs/data_layer.rst: -------------------------------------------------------------------------------- 1 | .. _data_layer: 2 | 3 | Data layer 4 | ========== 5 | 6 | .. currentmodule:: fastapi_jsonapi 7 | 8 | | The data layer is a CRUD interface between resource manager and data. It is a very flexible system to use any ORM or data storage. You can even create a data layer that uses multiple ORMs and data storage to manage your own objects. The data layer implements a CRUD interface for objects and relationships. It also manages pagination, filtering and sorting. 9 | | 10 | | FastAPI-JSONAPI has a full-featured data layer that uses the popular ORM `SQLAlchemy `_. 11 | 12 | To configure the data layer you have to set its required parameters in the resource manager. 13 | 14 | Example: 15 | 16 | .. literalinclude:: ./python_snippets/data_layer/custom_data_layer.py 17 | -------------------------------------------------------------------------------- /docs/http_snippets/README.md: -------------------------------------------------------------------------------- 1 | # Generate HTTP code snippets 2 | 3 | 4 | ## Install 5 | 6 | 7 | ```shell 8 | # checked on node@14 and npm@6.14.18 9 | # to use in cli 10 | npm install --global httpsnippet@2 11 | ``` 12 | 13 | ```shell 14 | # to use as a module 15 | npm install --save httpsnippet@2 16 | ``` 17 | 18 | ## Spec 19 | 20 | Create HAR specs (.json files) in this directory. 21 | 22 | > **Don't edit any files in the `./snippets` directory manually!** 23 | 24 | ## Generate 25 | 26 | ### Create all snippets and run requests for them 27 | 28 | ```shell 29 | # run and create for all minimal api requests 30 | ./run_and_create.sh minimal_api 31 | ``` 32 | 33 | ```shell 34 | # run and create for all relationship api requests 35 | ./run_and_create.sh relationship_api 36 | ``` 37 | 38 | ```shell 39 | # run and create for delete example relationship api requests 40 | ./run_and_create.sh relationship_api__delete 41 | ``` 42 | 43 | ### Or do it manually: 44 | 45 | ```shell 46 | # example 47 | httpsnippet example.json --target node --client unirest --output ./snippets 48 | ``` 49 | 50 | ```shell 51 | # minimal api to python3 52 | httpsnippet minimal_api__create_user.json --target python --client python3 --output ./snippets 53 | ``` 54 | 55 | ```shell 56 | # minimal api to http 57 | httpsnippet minimal_api__create_user.json --target http --output ./snippets 58 | ``` 59 | 60 | 61 | ```shell 62 | # minimal api to http. don't write Host, Content-Length 63 | httpsnippet minimal_api__create_user.json --target http --output ./snippets -x '{"autoHost": false, "autoContentLength": false}' 64 | ``` 65 | 66 | 67 | ```shell 68 | # process multiple 69 | httpsnippet ./*.json --target http --output ./snippets 70 | ``` 71 | 72 | 73 | ### Create requests and run them, write results 74 | 75 | ```shell 76 | # create python-requests requests snippets 77 | httpsnippet ./*.json --target python --client requests --output ./snippets 78 | ``` 79 | 80 | ```shell 81 | # Run requests for minimal api, save output 82 | python3 update_snippets_with_responses.py minimal_api 83 | ``` 84 | 85 | ```shell 86 | # Run requests for relationship api, save output 87 | python3 update_snippets_with_responses.py relationship_api 88 | ``` 89 | 90 | #### Verbose logs (DEBUG level) 91 | 92 | ```shell 93 | # Run requests for relationship api, save output 94 | python3 update_snippets_with_responses.py relationship_api --verbose 95 | ``` 96 | 97 | > **Pro tip:** run webserver for specs before running update_snippets_with_responses, otherwise it won't work 😉 98 | 99 | 100 | Copy-paste resulting help text (from between the "===" lines) to make includes. 101 | -------------------------------------------------------------------------------- /docs/http_snippets/api_filtering__get_users.json: -------------------------------------------------------------------------------- 1 | { 2 | "method": "GET", 3 | "url": "http://localhost:5000/users", 4 | "httpVersion": "HTTP/1.1", 5 | "queryString": [ 6 | ], 7 | "headers": [ 8 | { 9 | "name": "content-type", 10 | "value": "application/vnd.api+json" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /docs/http_snippets/api_filtering__get_users__filter_word_contains_in_array.json: -------------------------------------------------------------------------------- 1 | { 2 | "method": "GET", 3 | "url": "http://localhost:5000/users?filter=[{\"name\":\"words\",\"op\":\"ilike_in_str_array\",\"val\":\"green\"}]", 4 | "httpVersion": "HTTP/1.1", 5 | "queryString": [ 6 | ], 7 | "headers": [ 8 | { 9 | "name": "content-type", 10 | "value": "application/vnd.api+json" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /docs/http_snippets/api_filtering__get_users__filter_word_in_array.json: -------------------------------------------------------------------------------- 1 | { 2 | "method": "GET", 3 | "url": "http://localhost:5000/users?filter=[{\"name\":\"words\",\"op\":\"in\",\"val\":\"spam\"}]", 4 | "httpVersion": "HTTP/1.1", 5 | "queryString": [ 6 | ], 7 | "headers": [ 8 | { 9 | "name": "content-type", 10 | "value": "application/vnd.api+json" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /docs/http_snippets/api_filtering__get_users__filter_words_in_array.json: -------------------------------------------------------------------------------- 1 | { 2 | "method": "GET", 3 | "url": "http://localhost:5000/users?filter=[{\"name\":\"words\",\"op\":\"in\",\"val\":[\"bar\",\"eggs\"]}]", 4 | "httpVersion": "HTTP/1.1", 5 | "queryString": [ 6 | ], 7 | "headers": [ 8 | { 9 | "name": "content-type", 10 | "value": "application/vnd.api+json" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /docs/http_snippets/example_atomic_fail__create_computer_and_update_user_bio.json: -------------------------------------------------------------------------------- 1 | { 2 | "method": "POST", 3 | "url": "http://localhost:8000/operations", 4 | "httpVersion": "HTTP/1.1", 5 | "queryString": [ 6 | ], 7 | "headers": [ 8 | { 9 | "name": "content-type", 10 | "value": "application/vnd.api+json" 11 | } 12 | ], 13 | "postData": { 14 | "mimeType": "application/json", 15 | "text": "{\n \"atomic:operations\": [\n {\n \"op\": \"add\",\n \"data\": {\n \"type\": \"computer\",\n \"attributes\": {\n \"name\": \"Commodore\"\n }\n }\n },\n {\n \"op\": \"update\",\n \"data\": {\n \"type\": \"user_bio\",\n \"attributes\": {\n \"favourite_movies\": \"Saw\"\n }\n }\n }\n ]\n}" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /docs/http_snippets/example_atomic_five__mixed_actions.json: -------------------------------------------------------------------------------- 1 | { 2 | "method": "POST", 3 | "url": "http://localhost:8000/operations", 4 | "httpVersion": "HTTP/1.1", 5 | "queryString": [ 6 | ], 7 | "headers": [ 8 | { 9 | "name": "content-type", 10 | "value": "application/vnd.api+json" 11 | } 12 | ], 13 | "postData": { 14 | "mimeType": "application/json", 15 | "text": "{\n \"atomic:operations\": [\n {\n \"op\": \"add\",\n \"data\": {\n \"type\": \"computer\",\n \"attributes\": {\n \"name\": \"Liza\"\n },\n \"relationships\": {\n \"user\": {\n \"data\": {\n \"id\": \"1\",\n \"type\": \"user\"\n }\n }\n }\n }\n },\n {\n \"op\": \"update\",\n \"data\": {\n \"id\": \"2\",\n \"type\": \"user_bio\",\n \"attributes\": {\n \"birth_city\": \"Saint Petersburg\",\n \"favourite_movies\": \"\\\"The Good, the Bad and the Ugly\\\", \\\"Once Upon a Time in America\\\"\"\n }\n }\n },\n {\n \"op\": \"remove\",\n \"ref\": {\n \"id\": \"2\",\n \"type\": \"child\"\n }\n }\n ]\n}" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /docs/http_snippets/example_atomic_five__only_remove_actions.json: -------------------------------------------------------------------------------- 1 | { 2 | "method": "POST", 3 | "url": "http://localhost:8000/operations", 4 | "httpVersion": "HTTP/1.1", 5 | "queryString": [ 6 | ], 7 | "headers": [ 8 | { 9 | "name": "content-type", 10 | "value": "application/vnd.api+json" 11 | } 12 | ], 13 | "postData": { 14 | "mimeType": "application/json", 15 | "text": "{\n \"atomic:operations\": [\n {\n \"op\": \"remove\",\n \"ref\": {\n \"id\": \"6\",\n \"type\": \"computer\"\n }\n },\n {\n \"op\": \"remove\",\n \"ref\": {\n \"id\": \"7\",\n \"type\": \"computer\"\n }\n }\n ]\n}" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /docs/http_snippets/example_atomic_four__create_many-to-many.json: -------------------------------------------------------------------------------- 1 | { 2 | "method": "POST", 3 | "url": "http://localhost:8000/operations", 4 | "httpVersion": "HTTP/1.1", 5 | "queryString": [ 6 | ], 7 | "headers": [ 8 | { 9 | "name": "content-type", 10 | "value": "application/vnd.api+json" 11 | } 12 | ], 13 | "postData": { 14 | "mimeType": "application/json", 15 | "text": "{\n \"atomic:operations\":[\n {\n \"op\":\"add\",\n \"data\":{\n \"lid\":\"new-parent\",\n \"type\":\"parent\",\n \"attributes\":{\n \"name\":\"David Newton\"\n }\n }\n },\n {\n \"op\":\"add\",\n \"data\":{\n \"lid\":\"new-child\",\n \"type\":\"child\",\n \"attributes\":{\n \"name\":\"James Owen\"\n }\n }\n },\n {\n \"op\":\"add\",\n \"data\":{\n \"type\":\"parent-to-child-association\",\n \"attributes\":{\n \"extra_data\":\"Lay piece happy box.\"\n },\n \"relationships\":{\n \"parent\":{\n \"data\":{\n \"lid\":\"new-parent\",\n \"type\":\"parent\"\n }\n },\n \"child\":{\n \"data\":{\n \"lid\":\"new-child\",\n \"type\":\"child\"\n }\n }\n }\n }\n }\n ]\n}" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /docs/http_snippets/example_atomic_four__get-many-to-many_with_includes.json: -------------------------------------------------------------------------------- 1 | { 2 | "method": "GET", 3 | "url": "http://localhost:8000/parent-to-child-association/1?include=parent,child", 4 | "httpVersion": "HTTP/1.1", 5 | "queryString": [ 6 | ], 7 | "headers": [ 8 | { 9 | "name": "content-type", 10 | "value": "application/vnd.api+json" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /docs/http_snippets/example_atomic_one__create_computer_and_separate_user.json: -------------------------------------------------------------------------------- 1 | { 2 | "method": "POST", 3 | "url": "http://localhost:8000/operations", 4 | "httpVersion": "HTTP/1.1", 5 | "queryString": [ 6 | ], 7 | "headers": [ 8 | { 9 | "name": "content-type", 10 | "value": "application/vnd.api+json" 11 | } 12 | ], 13 | "postData": { 14 | "mimeType": "application/json", 15 | "text": "{\n \"atomic:operations\":[\n {\n \"op\":\"add\",\n \"data\":{\n \"type\":\"computer\",\n \"attributes\":{\n \"name\":\"Commodore\"\n }\n }\n },\n {\n \"op\":\"add\",\n \"data\":{\n \"type\":\"user\",\n \"attributes\":{\n \"first_name\":\"Kate\",\n \"last_name\":\"Grey\"\n }\n }\n }\n ]\n}" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /docs/http_snippets/example_atomic_three__create_user_and_user_bio.json: -------------------------------------------------------------------------------- 1 | { 2 | "method": "POST", 3 | "url": "http://localhost:8000/operations", 4 | "httpVersion": "HTTP/1.1", 5 | "queryString": [ 6 | ], 7 | "headers": [ 8 | { 9 | "name": "content-type", 10 | "value": "application/vnd.api+json" 11 | } 12 | ], 13 | "postData": { 14 | "mimeType": "application/json", 15 | "text": "{\n \"atomic:operations\":[\n {\n \"op\":\"add\",\n \"data\":{\n \"lid\":\"some-local-id\",\n \"type\":\"user\",\n \"attributes\":{\n \"first_name\":\"Bob\",\n \"last_name\":\"Pink\"\n }\n }\n },\n {\n \"op\":\"add\",\n \"data\":{\n \"type\":\"user_bio\",\n \"attributes\":{\n \"birth_city\":\"Moscow\",\n \"favourite_movies\":\"Jaws, Alien\"\n },\n \"relationships\":{\n \"user\":{\n \"data\":{\n \"lid\":\"some-local-id\",\n \"type\":\"user\"\n }\n }\n }\n }\n }\n ]\n}" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /docs/http_snippets/example_atomic_two__after_update_computer_check_details.json: -------------------------------------------------------------------------------- 1 | { 2 | "method": "GET", 3 | "url": "http://localhost:8000/computers/4?include=user", 4 | "httpVersion": "HTTP/1.1", 5 | "queryString": [ 6 | ], 7 | "headers": [ 8 | { 9 | "name": "content-type", 10 | "value": "application/vnd.api+json" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /docs/http_snippets/example_atomic_two__update_computer.json: -------------------------------------------------------------------------------- 1 | { 2 | "method": "POST", 3 | "url": "http://localhost:8000/operations", 4 | "httpVersion": "HTTP/1.1", 5 | "queryString": [ 6 | ], 7 | "headers": [ 8 | { 9 | "name": "content-type", 10 | "value": "application/vnd.api+json" 11 | } 12 | ], 13 | "postData": { 14 | "mimeType": "application/json", 15 | "text": "{\n \"atomic:operations\":[\n {\n \"op\":\"update\",\n \"data\":{\n \"id\":\"4\",\n \"type\":\"computer\",\n \"attributes\":{\n \"name\":\"Commodore PET test\"\n },\n \"relationships\":{\n \"user\":{\n \"data\":{\n \"id\":\"5\",\n \"type\":\"user\"\n }\n }\n }\n }\n }\n ]\n}" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /docs/http_snippets/example_atomic_two__update_user.json: -------------------------------------------------------------------------------- 1 | { 2 | "method": "POST", 3 | "url": "http://localhost:8000/operations", 4 | "httpVersion": "HTTP/1.1", 5 | "queryString": [ 6 | ], 7 | "headers": [ 8 | { 9 | "name": "content-type", 10 | "value": "application/vnd.api+json" 11 | } 12 | ], 13 | "postData": { 14 | "mimeType": "application/json", 15 | "text": "{\n \"atomic:operations\":[\n {\n \"op\":\"update\",\n \"data\":{\n \"id\":\"5\",\n \"type\":\"user\",\n \"attributes\":{\n \"last_name\":\"White\",\n \"email\":\"kate@example.com\"\n }\n }\n }\n ]\n}" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /docs/http_snippets/example_one_api__create_computer_for_user.json: -------------------------------------------------------------------------------- 1 | { 2 | "method": "POST", 3 | "url": "http://localhost:8000/computers?include=user", 4 | "httpVersion": "HTTP/1.1", 5 | "queryString": [ 6 | ], 7 | "headers": [ 8 | { 9 | "name": "content-type", 10 | "value": "application/vnd.api+json" 11 | } 12 | ], 13 | "postData": { 14 | "mimeType": "application/json", 15 | "text": "{\n \"data\": {\n \"type\": \"computer\",\n \"attributes\": {\n \"name\": \"Amstrad\"\n },\n \"relationships\": {\n \"user\": {\n \"data\": {\n \"id\": \"3\",\n \"type\": \"user\"\n }\n }\n }\n }\n}" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /docs/http_snippets/example_one_api__create_user.json: -------------------------------------------------------------------------------- 1 | { 2 | "method": "POST", 3 | "url": "http://localhost:8000/users", 4 | "httpVersion": "HTTP/1.1", 5 | "queryString": [ 6 | ], 7 | "headers": [ 8 | { 9 | "name": "content-type", 10 | "value": "application/vnd.api+json" 11 | } 12 | ], 13 | "postData": { 14 | "mimeType": "application/json", 15 | "text": "{\n \"data\": {\n \"type\": \"user\",\n \"attributes\": {\n \"first_name\": \"Bob\",\n \"last_name\": \"Green\",\n \"age\": 37,\n \"status\": \"active\",\n \"email\": \"bob@example.com\"\n }\n }\n}" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /docs/http_snippets/example_one_api__get_user.json: -------------------------------------------------------------------------------- 1 | { 2 | "method": "GET", 3 | "url": "http://localhost:8000/users/3", 4 | "httpVersion": "HTTP/1.1", 5 | "queryString": [ 6 | ], 7 | "headers": [ 8 | { 9 | "name": "content-type", 10 | "value": "application/vnd.api+json" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /docs/http_snippets/example_one_api__get_user_with_computers.json: -------------------------------------------------------------------------------- 1 | { 2 | "method": "GET", 3 | "url": "http://localhost:8000/users/3?include=computers", 4 | "httpVersion": "HTTP/1.1", 5 | "queryString": [ 6 | ], 7 | "headers": [ 8 | { 9 | "name": "content-type", 10 | "value": "application/vnd.api+json" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /docs/http_snippets/example_one_api__get_users.json: -------------------------------------------------------------------------------- 1 | { 2 | "method": "GET", 3 | "url": "http://localhost:8000/users", 4 | "httpVersion": "HTTP/1.1", 5 | "queryString": [ 6 | ], 7 | "headers": [ 8 | { 9 | "name": "content-type", 10 | "value": "application/vnd.api+json" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /docs/http_snippets/example_one_api__get_users_with_computers.json: -------------------------------------------------------------------------------- 1 | { 2 | "method": "GET", 3 | "url": "http://localhost:8000/users?include=computers", 4 | "httpVersion": "HTTP/1.1", 5 | "queryString": [ 6 | ], 7 | "headers": [ 8 | { 9 | "name": "content-type", 10 | "value": "application/vnd.api+json" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /docs/http_snippets/includes__many_to_many.json: -------------------------------------------------------------------------------- 1 | { 2 | "method": "GET", 3 | "url": "http://localhost:8082/parents?include=children,children.child", 4 | "httpVersion": "HTTP/1.1", 5 | "queryString": [ 6 | ], 7 | "headers": [ 8 | { 9 | "name": "content-type", 10 | "value": "application/vnd.api+json" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /docs/http_snippets/minimal_api__create_user.json: -------------------------------------------------------------------------------- 1 | { 2 | "method": "POST", 3 | "url": "http://localhost:5000/users", 4 | "httpVersion": "HTTP/1.1", 5 | "queryString": [ 6 | ], 7 | "headers": [ 8 | { 9 | "name": "content-type", 10 | "value": "application/vnd.api+json" 11 | } 12 | ], 13 | "postData": { 14 | "mimeType": "application/json", 15 | "text": "{\n \"data\": {\n \"type\": \"user\",\n \"attributes\": {\n \"name\": \"John\"\n }\n }\n}" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /docs/http_snippets/minimal_api__delete_user.json: -------------------------------------------------------------------------------- 1 | { 2 | "method": "DELETE", 3 | "url": "http://localhost:5000/users/1", 4 | "httpVersion": "HTTP/1.1", 5 | "queryString": [ 6 | ], 7 | "headers": [ 8 | { 9 | "name": "content-type", 10 | "value": "application/vnd.api+json" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /docs/http_snippets/minimal_api__get_user.json: -------------------------------------------------------------------------------- 1 | { 2 | "method": "GET", 3 | "url": "http://localhost:5000/users/1", 4 | "httpVersion": "HTTP/1.1", 5 | "queryString": [ 6 | ], 7 | "headers": [ 8 | { 9 | "name": "content-type", 10 | "value": "application/vnd.api+json" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /docs/http_snippets/minimal_api__get_users.json: -------------------------------------------------------------------------------- 1 | { 2 | "method": "GET", 3 | "url": "http://localhost:5000/users", 4 | "httpVersion": "HTTP/1.1", 5 | "queryString": [ 6 | ], 7 | "headers": [ 8 | { 9 | "name": "content-type", 10 | "value": "application/vnd.api+json" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /docs/http_snippets/minimal_api__patch_user.json: -------------------------------------------------------------------------------- 1 | { 2 | "method": "PATCH", 3 | "url": "http://localhost:5000/users/1", 4 | "httpVersion": "HTTP/1.1", 5 | "queryString": [ 6 | ], 7 | "headers": [ 8 | { 9 | "name": "content-type", 10 | "value": "application/vnd.api+json" 11 | } 12 | ], 13 | "postData": { 14 | "mimeType": "application/json", 15 | "text": "{\n \"data\": {\n \"id\": 1,\n \"type\": \"user\",\n \"attributes\": {\n \"name\": \"Sam\"\n }\n }\n}" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /docs/http_snippets/relationship_api__create_computer.json: -------------------------------------------------------------------------------- 1 | { 2 | "method": "POST", 3 | "url": "http://localhost:5000/computers", 4 | "httpVersion": "HTTP/1.1", 5 | "queryString": [ 6 | ], 7 | "headers": [ 8 | { 9 | "name": "content-type", 10 | "value": "application/vnd.api+json" 11 | } 12 | ], 13 | "postData": { 14 | "mimeType": "application/json", 15 | "text": "{\n \"data\": {\n \"type\": \"computer\",\n \"attributes\": {\n \"serial\": \"Amstrad\"\n }\n }\n}" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /docs/http_snippets/relationship_api__create_computer_relationship_for_user.json: -------------------------------------------------------------------------------- 1 | { 2 | "method": "POST", 3 | "url": "http://localhost:5000/users/1/relationships/computers", 4 | "httpVersion": "HTTP/1.1", 5 | "queryString": [ 6 | ], 7 | "headers": [ 8 | { 9 | "name": "content-type", 10 | "value": "application/vnd.api+json" 11 | } 12 | ], 13 | "postData": { 14 | "mimeType": "application/json", 15 | "text": "{\n \"data\": [\n {\n \"type\": \"computer\",\n \"id\": \"4\"\n }\n ]\n}" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /docs/http_snippets/relationship_api__create_user_with_computer_relationship.json: -------------------------------------------------------------------------------- 1 | { 2 | "method": "POST", 3 | "url": "http://localhost:5000/users?include=computers", 4 | "httpVersion": "HTTP/1.1", 5 | "queryString": [ 6 | ], 7 | "headers": [ 8 | { 9 | "name": "content-type", 10 | "value": "application/vnd.api+json" 11 | } 12 | ], 13 | "postData": { 14 | "mimeType": "application/json", 15 | "text": "{\n \"data\": {\n \"type\": \"user\",\n \"attributes\": {\n \"name\": \"John\",\n \"email\": \"john@exmple.com\"\n },\n \"relationships\": {\n \"computers\": {\n \"data\": [\n {\n \"type\": \"computer\",\n \"id\": \"2\"\n }\n ]\n }\n }\n }\n}" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /docs/http_snippets/relationship_api__delete_computer.json: -------------------------------------------------------------------------------- 1 | { 2 | "method": "DELETE", 3 | "url": "http://localhost:5000/computers/1", 4 | "httpVersion": "HTTP/1.1", 5 | "queryString": [ 6 | ], 7 | "headers": [ 8 | { 9 | "name": "content-type", 10 | "value": "application/vnd.api+json" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /docs/http_snippets/relationship_api__delete_computer_relationship.json: -------------------------------------------------------------------------------- 1 | { 2 | "method": "DELETE", 3 | "url": "http://localhost:5000/users/1/relationships/computers", 4 | "httpVersion": "HTTP/1.1", 5 | "queryString": [ 6 | ], 7 | "headers": [ 8 | { 9 | "name": "content-type", 10 | "value": "application/vnd.api+json" 11 | } 12 | ], 13 | "postData": { 14 | "mimeType": "application/json", 15 | "text": "{\n \"data\": [\n {\n \"type\": \"computer\",\n \"id\": \"3\"\n }\n ]\n}" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /docs/http_snippets/relationship_api__get_computers.json: -------------------------------------------------------------------------------- 1 | { 2 | "method": "GET", 3 | "url": "http://localhost:5000/computers", 4 | "httpVersion": "HTTP/1.1", 5 | "queryString": [ 6 | ], 7 | "headers": [ 8 | { 9 | "name": "content-type", 10 | "value": "application/vnd.api+json" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /docs/http_snippets/relationship_api__get_user_related_computers.json: -------------------------------------------------------------------------------- 1 | { 2 | "method": "GET", 3 | "url": "http://localhost:5000/users/1/computers", 4 | "httpVersion": "HTTP/1.1", 5 | "queryString": [ 6 | ], 7 | "headers": [ 8 | { 9 | "name": "content-type", 10 | "value": "application/vnd.api+json" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /docs/http_snippets/relationship_api__get_user_with_computers.json: -------------------------------------------------------------------------------- 1 | { 2 | "method": "GET", 3 | "url": "http://localhost:5000/users/1?include=computers", 4 | "httpVersion": "HTTP/1.1", 5 | "queryString": [ 6 | ], 7 | "headers": [ 8 | { 9 | "name": "content-type", 10 | "value": "application/vnd.api+json" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /docs/http_snippets/relationship_api__patch_computer.json: -------------------------------------------------------------------------------- 1 | { 2 | "method": "PATCH", 3 | "url": "http://localhost:5000/computers/1", 4 | "httpVersion": "HTTP/1.1", 5 | "queryString": [ 6 | ], 7 | "headers": [ 8 | { 9 | "name": "content-type", 10 | "value": "application/vnd.api+json" 11 | } 12 | ], 13 | "postData": { 14 | "mimeType": "application/json", 15 | "text": "{\n \"data\": {\n \"type\": \"computer\",\n \"id\": \"1\",\n \"attributes\": {\n \"serial\": \"New Amstrad\"\n }\n }\n}" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /docs/http_snippets/relationship_api__update_user_with_computer_relationship.json: -------------------------------------------------------------------------------- 1 | { 2 | "method": "PATCH", 3 | "url": "http://localhost:5000/users/1?include=computers", 4 | "httpVersion": "HTTP/1.1", 5 | "queryString": [ 6 | ], 7 | "headers": [ 8 | { 9 | "name": "content-type", 10 | "value": "application/vnd.api+json" 11 | } 12 | ], 13 | "postData": { 14 | "mimeType": "application/json", 15 | "text": "{\n \"data\": {\n \"type\": \"user\",\n \"id\": \"1\",\n \"attributes\": {\n \"email\": \"john@example.com\"\n },\n \"relationships\": {\n \"computers\": {\n \"data\": [\n {\n \"type\": \"computer\",\n \"id\": \"3\"\n }\n ]\n }\n }\n }\n}" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /docs/http_snippets/run_and_create.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | echo "Create HTTP and Python snippets" 4 | 5 | for filename in ./"$1"*.json; do 6 | echo "process $filename" 7 | httpsnippet "$filename" --target http --output ./snippets -x '{"autoHost": false, "autoContentLength": false}' 8 | httpsnippet "$filename" --target python --client requests --output ./snippets 9 | done 10 | 11 | echo "Run requests" 12 | python3 update_snippets_with_responses.py "$1" 13 | -------------------------------------------------------------------------------- /docs/http_snippets/snippets/.gitignore: -------------------------------------------------------------------------------- 1 | *.py 2 | -------------------------------------------------------------------------------- /docs/http_snippets/snippets/api_filtering__get_users: -------------------------------------------------------------------------------- 1 | GET /users HTTP/1.1 2 | Content-Type: application/vnd.api+json 3 | -------------------------------------------------------------------------------- /docs/http_snippets/snippets/api_filtering__get_users__filter_word_contains_in_array: -------------------------------------------------------------------------------- 1 | GET /photos?filter=%5B%7B%22name%22%3A%22words%22%2C%22op%22%3A%22jsonb_contains%22%2C%22val%22%3A%7B%22location%22%3A%22Moscow%22%2C%22spam%22%3A%22eggs%22%7D%7D%5D%0A HTTP/1.1 2 | Content-Type: application/vnd.api+json 3 | -------------------------------------------------------------------------------- /docs/http_snippets/snippets/api_filtering__get_users__filter_word_contains_in_array_result: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Content-Type: application/vnd.api+json 3 | 4 | { 5 | "data": [ 6 | { 7 | "attributes": { 8 | "name": "pic-qwerty", 9 | "words": { 10 | "location": "Moscow", 11 | "spam": "eggs", 12 | "foo": "bar", 13 | "qwe": "abc" 14 | } 15 | }, 16 | "id": "7", 17 | "type": "photo" 18 | } 19 | ], 20 | "jsonapi": { 21 | "version": "1.0" 22 | }, 23 | "meta": { 24 | "count": 1 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /docs/http_snippets/snippets/api_filtering__get_users__filter_word_in_array: -------------------------------------------------------------------------------- 1 | GET /users?filter=%5B%7B%22name%22%3A%22words%22%2C%22op%22%3A%22in%22%2C%22val%22%3A%22spam%22%7D%5D HTTP/1.1 2 | Content-Type: application/vnd.api+json 3 | -------------------------------------------------------------------------------- /docs/http_snippets/snippets/api_filtering__get_users__filter_word_in_array_result: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Content-Type: application/vnd.api+json 3 | 4 | { 5 | "data": [ 6 | { 7 | "attributes": { 8 | "name": "Sam", 9 | "words": [ 10 | "spam", 11 | "eggs", 12 | "green-apple" 13 | ] 14 | }, 15 | "id": "2", 16 | "links": { 17 | "self": "/users/2" 18 | }, 19 | "type": "user" 20 | } 21 | ], 22 | "jsonapi": { 23 | "version": "1.0" 24 | }, 25 | "links": { 26 | "self": "http://localhost:5000/users?filter=%5B%7B%22name%22%3A%22words%22%2C%22op%22%3A%22in%22%2C%22val%22%3A%22spam%22%7D%5D" 27 | }, 28 | "meta": { 29 | "count": 1 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /docs/http_snippets/snippets/api_filtering__get_users__filter_words_in_array: -------------------------------------------------------------------------------- 1 | GET /users?filter=%5B%7B%22name%22%3A%22words%22%2C%22op%22%3A%22in%22%2C%22val%22%3A%5B%22bar%22%2C%22eggs%22%5D%7D%5D HTTP/1.1 2 | Content-Type: application/vnd.api+json 3 | -------------------------------------------------------------------------------- /docs/http_snippets/snippets/api_filtering__get_users__filter_words_in_array_result: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Content-Type: application/vnd.api+json 3 | 4 | { 5 | "data": [ 6 | { 7 | "attributes": { 8 | "name": "John", 9 | "words": [ 10 | "foo", 11 | "bar", 12 | "green-grass" 13 | ] 14 | }, 15 | "id": "1", 16 | "links": { 17 | "self": "/users/1" 18 | }, 19 | "type": "user" 20 | }, 21 | { 22 | "attributes": { 23 | "name": "Sam", 24 | "words": [ 25 | "spam", 26 | "eggs", 27 | "green-apple" 28 | ] 29 | }, 30 | "id": "2", 31 | "links": { 32 | "self": "/users/2" 33 | }, 34 | "type": "user" 35 | } 36 | ], 37 | "jsonapi": { 38 | "version": "1.0" 39 | }, 40 | "links": { 41 | "self": "http://localhost:5000/users?filter=%5B%7B%22name%22%3A%22words%22%2C%22op%22%3A%22in%22%2C%22val%22%3A%5B%22bar%22%2C%22eggs%22%5D%7D%5D" 42 | }, 43 | "meta": { 44 | "count": 2 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /docs/http_snippets/snippets/api_filtering__get_users_result: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Content-Type: application/vnd.api+json 3 | 4 | { 5 | "data": [ 6 | { 7 | "attributes": { 8 | "name": "John", 9 | "words": [ 10 | "foo", 11 | "bar", 12 | "green-grass" 13 | ] 14 | }, 15 | "id": "1", 16 | "links": { 17 | "self": "/users/1" 18 | }, 19 | "type": "user" 20 | }, 21 | { 22 | "attributes": { 23 | "name": "Sam", 24 | "words": [ 25 | "spam", 26 | "eggs", 27 | "green-apple" 28 | ] 29 | }, 30 | "id": "2", 31 | "links": { 32 | "self": "/users/2" 33 | }, 34 | "type": "user" 35 | } 36 | ], 37 | "jsonapi": { 38 | "version": "1.0" 39 | }, 40 | "links": { 41 | "self": "http://localhost:5000/users" 42 | }, 43 | "meta": { 44 | "count": 2 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /docs/http_snippets/snippets/client_generated_id__create_user: -------------------------------------------------------------------------------- 1 | POST /users HTTP/1.1 2 | Content-Type: application/vnd.api+json 3 | 4 | { 5 | "data": { 6 | "type": "user", 7 | "attributes": { 8 | "name": "John" 9 | }, 10 | "id": "867ab602-d9f7-44d0-8d3a-d2ae6d96b3a7" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /docs/http_snippets/snippets/client_generated_id__create_user_result: -------------------------------------------------------------------------------- 1 | HTTP/1.1 201 Created 2 | Content-Type: application/vnd.api+json 3 | 4 | { 5 | "data": { 6 | "type": "user", 7 | "id": "867ab602-d9f7-44d0-8d3a-d2ae6d96b3a7", 8 | "attributes": { 9 | "name": "John" 10 | }, 11 | "links": { 12 | "self": "/users/867ab602-d9f7-44d0-8d3a-d2ae6d96b3a7" 13 | }, 14 | }, 15 | "jsonapi": { 16 | "version": "1.0" 17 | }, 18 | "links": { 19 | "self": "/users/867ab602-d9f7-44d0-8d3a-d2ae6d96b3a7" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /docs/http_snippets/snippets/errors__create_user: -------------------------------------------------------------------------------- 1 | POST /computers HTTP/1.1 2 | Content-Type: application/vnd.api+json 3 | 4 | { 5 | "data": { 6 | "type": "computer", 7 | "attributes": { 8 | "name": "John" 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /docs/http_snippets/snippets/errors__create_user_result: -------------------------------------------------------------------------------- 1 | HTTP/1.1 400 Bad Request 2 | Content-Type: application/vnd.api+json 3 | 4 | { 5 | "errors":[ 6 | { 7 | "detail":"My custom err", 8 | "source":{"pointer":""}, 9 | "status_code":400, 10 | "title":"Bad Request" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /docs/http_snippets/snippets/example_atomic_fail__create_computer_and_update_user_bio: -------------------------------------------------------------------------------- 1 | POST /operations HTTP/1.1 2 | Content-Type: application/vnd.api+json 3 | 4 | { 5 | "atomic:operations": [ 6 | { 7 | "op": "add", 8 | "data": { 9 | "type": "computer", 10 | "attributes": { 11 | "name": "Commodore" 12 | } 13 | } 14 | }, 15 | { 16 | "op": "update", 17 | "data": { 18 | "type": "user_bio", 19 | "attributes": { 20 | "favourite_movies": "Saw" 21 | } 22 | } 23 | } 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /docs/http_snippets/snippets/example_atomic_fail__create_computer_and_update_user_bio_result: -------------------------------------------------------------------------------- 1 | HTTP/1.1 422 Unprocessable Entity 2 | Content-Type: application/json 3 | 4 | { 5 | "detail": { 6 | "data": { 7 | "attributes": { 8 | "favourite_movies": "Saw" 9 | }, 10 | "id": null, 11 | "lid": null, 12 | "relationships": null, 13 | "type": "user_bio" 14 | }, 15 | "errors": [ 16 | { 17 | "loc": [ 18 | "data", 19 | "attributes", 20 | "birth_city" 21 | ], 22 | "msg": "field required", 23 | "type": "value_error.missing" 24 | }, 25 | { 26 | "loc": [ 27 | "data", 28 | "id" 29 | ], 30 | "msg": "none is not an allowed value", 31 | "type": "type_error.none.not_allowed" 32 | } 33 | ], 34 | "message": "Validation error on operation update", 35 | "ref": null 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /docs/http_snippets/snippets/example_atomic_five__mixed_actions: -------------------------------------------------------------------------------- 1 | POST /operations HTTP/1.1 2 | Content-Type: application/vnd.api+json 3 | 4 | { 5 | "atomic:operations": [ 6 | { 7 | "op": "add", 8 | "data": { 9 | "type": "computer", 10 | "attributes": { 11 | "name": "Liza" 12 | }, 13 | "relationships": { 14 | "user": { 15 | "data": { 16 | "id": "1", 17 | "type": "user" 18 | } 19 | } 20 | } 21 | } 22 | }, 23 | { 24 | "op": "update", 25 | "data": { 26 | "id": "2", 27 | "type": "user_bio", 28 | "attributes": { 29 | "birth_city": "Saint Petersburg", 30 | "favourite_movies": "\"The Good, the Bad and the Ugly\", \"Once Upon a Time in America\"" 31 | } 32 | } 33 | }, 34 | { 35 | "op": "remove", 36 | "ref": { 37 | "id": "2", 38 | "type": "child" 39 | } 40 | } 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /docs/http_snippets/snippets/example_atomic_five__mixed_actions_result: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Content-Type: application/json 3 | 4 | { 5 | "atomic:results": [ 6 | { 7 | "data": { 8 | "attributes": { 9 | "name": "Liza" 10 | }, 11 | "id": "5", 12 | "type": "computer" 13 | }, 14 | "meta": null 15 | }, 16 | { 17 | "data": { 18 | "attributes": { 19 | "birth_city": "Saint Petersburg", 20 | "favourite_movies": "\"The Good, the Bad and the Ugly\", \"Once Upon a Time in America\"", 21 | }, 22 | "id": "2", 23 | "type": "user_bio" 24 | }, 25 | "meta": null 26 | }, 27 | { 28 | "data": null, 29 | "meta": null 30 | } 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /docs/http_snippets/snippets/example_atomic_five__only_remove_actions: -------------------------------------------------------------------------------- 1 | POST /operations HTTP/1.1 2 | Content-Type: application/vnd.api+json 3 | 4 | { 5 | "atomic:operations": [ 6 | { 7 | "op": "remove", 8 | "ref": { 9 | "id": "6", 10 | "type": "computer" 11 | } 12 | }, 13 | { 14 | "op": "remove", 15 | "ref": { 16 | "id": "7", 17 | "type": "computer" 18 | } 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /docs/http_snippets/snippets/example_atomic_five__only_remove_actions_result: -------------------------------------------------------------------------------- 1 | HTTP/1.1 204 No Content 2 | -------------------------------------------------------------------------------- /docs/http_snippets/snippets/example_atomic_four__create_many-to-many: -------------------------------------------------------------------------------- 1 | POST /operations HTTP/1.1 2 | Content-Type: application/vnd.api+json 3 | 4 | { 5 | "atomic:operations":[ 6 | { 7 | "op":"add", 8 | "data":{ 9 | "lid":"new-parent", 10 | "type":"parent", 11 | "attributes":{ 12 | "name":"David Newton" 13 | } 14 | } 15 | }, 16 | { 17 | "op":"add", 18 | "data":{ 19 | "lid":"new-child", 20 | "type":"child", 21 | "attributes":{ 22 | "name":"James Owen" 23 | } 24 | } 25 | }, 26 | { 27 | "op":"add", 28 | "data":{ 29 | "type":"parent-to-child-association", 30 | "attributes":{ 31 | "extra_data":"Lay piece happy box." 32 | }, 33 | "relationships":{ 34 | "parent":{ 35 | "data":{ 36 | "lid":"new-parent", 37 | "type":"parent" 38 | } 39 | }, 40 | "child":{ 41 | "data":{ 42 | "lid":"new-child", 43 | "type":"child" 44 | } 45 | } 46 | } 47 | } 48 | } 49 | ] 50 | } 51 | -------------------------------------------------------------------------------- /docs/http_snippets/snippets/example_atomic_four__create_many-to-many_result: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Content-Type: application/json 3 | 4 | { 5 | "atomic:results": [ 6 | { 7 | "data": { 8 | "attributes": { 9 | "name": "David Newton" 10 | }, 11 | "id": "1", 12 | "type": "parent" 13 | }, 14 | "meta": null 15 | }, 16 | { 17 | "data": { 18 | "attributes": { 19 | "name": "James Owen" 20 | }, 21 | "id": "1", 22 | "type": "child" 23 | }, 24 | "meta": null 25 | }, 26 | { 27 | "data": { 28 | "attributes": { 29 | "extra_data": "Lay piece happy box." 30 | }, 31 | "id": "1", 32 | "type": "parent-to-child-association" 33 | }, 34 | "meta": null 35 | } 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /docs/http_snippets/snippets/example_atomic_four__get-many-to-many_with_includes: -------------------------------------------------------------------------------- 1 | GET /parent-to-child-association/1?include=parent%2Cchild HTTP/1.1 2 | Content-Type: application/vnd.api+json 3 | -------------------------------------------------------------------------------- /docs/http_snippets/snippets/example_atomic_four__get-many-to-many_with_includes_result: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Content-Type: application/json 3 | 4 | { 5 | "data": { 6 | "attributes": { 7 | "extra_data": "Lay piece happy box." 8 | }, 9 | "id": "1", 10 | "relationships": { 11 | "child": { 12 | "data": { 13 | "id": "1", 14 | "type": "child" 15 | } 16 | }, 17 | "parent": { 18 | "data": { 19 | "id": "1", 20 | "type": "parent" 21 | } 22 | } 23 | }, 24 | "type": "parent-to-child-association" 25 | }, 26 | "included": [ 27 | { 28 | "attributes": { 29 | "name": "James Owen" 30 | }, 31 | "id": "1", 32 | "type": "child" 33 | }, 34 | { 35 | "attributes": { 36 | "name": "David Newton" 37 | }, 38 | "id": "1", 39 | "type": "parent" 40 | } 41 | ], 42 | "jsonapi": { 43 | "version": "1.0" 44 | }, 45 | "meta": null 46 | } 47 | -------------------------------------------------------------------------------- /docs/http_snippets/snippets/example_atomic_one__create_computer_and_separate_user: -------------------------------------------------------------------------------- 1 | POST /operations HTTP/1.1 2 | Content-Type: application/vnd.api+json 3 | 4 | { 5 | "atomic:operations": [ 6 | { 7 | "op": "add", 8 | "data": { 9 | "type": "computer", 10 | "attributes": { 11 | "name": "Commodore" 12 | } 13 | } 14 | }, 15 | { 16 | "op": "add", 17 | "data": { 18 | "type": "user", 19 | "attributes": { 20 | "first_name": "Kate", 21 | "last_name": "Grey" 22 | } 23 | } 24 | } 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /docs/http_snippets/snippets/example_atomic_one__create_computer_and_separate_user_result: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Content-Type: application/json 3 | 4 | { 5 | "atomic:results": [ 6 | { 7 | "data": { 8 | "attributes": { 9 | "name": "Commodore" 10 | }, 11 | "id": "4", 12 | "type": "computer" 13 | }, 14 | "meta": null 15 | }, 16 | { 17 | "data": { 18 | "attributes": { 19 | "age": null, 20 | "email": null, 21 | "first_name": "Kate", 22 | "last_name": "Grey", 23 | "status": "active" 24 | }, 25 | "id": "5", 26 | "type": "user" 27 | }, 28 | "meta": null 29 | } 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /docs/http_snippets/snippets/example_atomic_three__create_user_and_user_bio: -------------------------------------------------------------------------------- 1 | POST /operations HTTP/1.1 2 | Content-Type: application/vnd.api+json 3 | 4 | { 5 | "atomic:operations":[ 6 | { 7 | "op":"add", 8 | "data":{ 9 | "lid":"some-local-id", 10 | "type":"user", 11 | "attributes":{ 12 | "first_name":"Bob", 13 | "last_name":"Pink" 14 | } 15 | } 16 | }, 17 | { 18 | "op":"add", 19 | "data":{ 20 | "type":"user_bio", 21 | "attributes":{ 22 | "birth_city":"Moscow", 23 | "favourite_movies":"Jaws, Alien" 24 | }, 25 | "relationships":{ 26 | "user":{ 27 | "data":{ 28 | "lid":"some-local-id", 29 | "type":"user" 30 | } 31 | } 32 | } 33 | } 34 | } 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /docs/http_snippets/snippets/example_atomic_three__create_user_and_user_bio_result: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Content-Type: application/json 3 | 4 | { 5 | "atomic:results": [ 6 | { 7 | "data": { 8 | "attributes": { 9 | "age": null, 10 | "email": null, 11 | "first_name": "Bob", 12 | "last_name": "Pink", 13 | "status": "active" 14 | }, 15 | "id": "7", 16 | "type": "user" 17 | }, 18 | "meta": null 19 | }, 20 | { 21 | "data": { 22 | "attributes": { 23 | "birth_city": "Moscow", 24 | "favourite_movies": "Jaws, Alien" 25 | }, 26 | "id": "2", 27 | "type": "user_bio" 28 | }, 29 | "meta": null 30 | } 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /docs/http_snippets/snippets/example_atomic_two__after_update_computer_check_details: -------------------------------------------------------------------------------- 1 | GET /computers/4?include=user HTTP/1.1 2 | Content-Type: application/vnd.api+json 3 | -------------------------------------------------------------------------------- /docs/http_snippets/snippets/example_atomic_two__after_update_computer_check_details_result: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Content-Type: application/json 3 | 4 | { 5 | "data": { 6 | "attributes": { 7 | "name": "Commodore PET" 8 | }, 9 | "id": "4", 10 | "relationships": { 11 | "user": { 12 | "data": { 13 | "id": "5", 14 | "type": "user" 15 | } 16 | } 17 | }, 18 | "type": "computer" 19 | }, 20 | "included": [ 21 | { 22 | "attributes": { 23 | "age": null, 24 | "email": "kate@example.com", 25 | "first_name": "Kate", 26 | "last_name": "White", 27 | "status": "active" 28 | }, 29 | "id": "5", 30 | "type": "user" 31 | } 32 | ], 33 | "jsonapi": { 34 | "version": "1.0" 35 | }, 36 | "meta": null 37 | } 38 | -------------------------------------------------------------------------------- /docs/http_snippets/snippets/example_atomic_two__update_computer: -------------------------------------------------------------------------------- 1 | POST /operations HTTP/1.1 2 | Content-Type: application/vnd.api+json 3 | 4 | { 5 | "atomic:operations": [ 6 | { 7 | "op": "update", 8 | "data": { 9 | "id": "4", 10 | "type": "computer", 11 | "attributes": { 12 | "name": "Commodore PET" 13 | }, 14 | "relationships": { 15 | "user": { 16 | "data": { 17 | "id": "5", 18 | "type": "user" 19 | } 20 | } 21 | } 22 | } 23 | } 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /docs/http_snippets/snippets/example_atomic_two__update_computer_result: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Content-Type: application/json 3 | 4 | { 5 | "atomic:results": [ 6 | { 7 | "data": { 8 | "attributes": { 9 | "name": "Commodore PET" 10 | }, 11 | "id": "4", 12 | "type": "computer" 13 | }, 14 | "meta": null 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /docs/http_snippets/snippets/example_atomic_two__update_user: -------------------------------------------------------------------------------- 1 | POST /operations HTTP/1.1 2 | Content-Type: application/vnd.api+json 3 | 4 | { 5 | "atomic:operations": [ 6 | { 7 | "op": "update", 8 | "data": { 9 | "id": "5", 10 | "type": "user", 11 | "attributes": { 12 | "last_name": "White", 13 | "email": "kate@example.com" 14 | } 15 | } 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /docs/http_snippets/snippets/example_atomic_two__update_user_result: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Content-Type: application/json 3 | 4 | { 5 | "atomic:results": [ 6 | { 7 | "data": { 8 | "attributes": { 9 | "age": null, 10 | "email": "kate@example.com", 11 | "first_name": "Kate", 12 | "last_name": "White", 13 | "status": "active" 14 | }, 15 | "id": "5", 16 | "type": "user" 17 | }, 18 | "meta": null 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /docs/http_snippets/snippets/example_one_api__create_computer_for_user: -------------------------------------------------------------------------------- 1 | POST /computers?include=user HTTP/1.1 2 | Content-Type: application/vnd.api+json 3 | 4 | { 5 | "data": { 6 | "type": "computer", 7 | "attributes": { 8 | "name": "Amstrad" 9 | }, 10 | "relationships": { 11 | "user": { 12 | "data": { 13 | "id": "3", 14 | "type": "user" 15 | } 16 | } 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /docs/http_snippets/snippets/example_one_api__create_computer_for_user_result: -------------------------------------------------------------------------------- 1 | HTTP/1.1 201 Created 2 | Content-Type: application/json 3 | 4 | { 5 | "data": { 6 | "attributes": { 7 | "name": "Amstrad" 8 | }, 9 | "id": "2", 10 | "relationships": { 11 | "user": { 12 | "data": { 13 | "id": "3", 14 | "type": "user" 15 | } 16 | } 17 | }, 18 | "type": "computer" 19 | }, 20 | "included": [ 21 | { 22 | "attributes": { 23 | "age": 37, 24 | "email": "bob@example.com", 25 | "first_name": "Bob", 26 | "last_name": "Green", 27 | "status": "active" 28 | }, 29 | "id": "3", 30 | "type": "user" 31 | } 32 | ], 33 | "jsonapi": { 34 | "version": "1.0" 35 | }, 36 | "meta": null 37 | } 38 | -------------------------------------------------------------------------------- /docs/http_snippets/snippets/example_one_api__create_user: -------------------------------------------------------------------------------- 1 | POST /users HTTP/1.1 2 | Content-Type: application/vnd.api+json 3 | 4 | { 5 | "data": { 6 | "type": "user", 7 | "attributes": { 8 | "first_name": "Bob", 9 | "last_name": "Green", 10 | "age": 37, 11 | "status": "active", 12 | "email": "bob@example.com" 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /docs/http_snippets/snippets/example_one_api__create_user_result: -------------------------------------------------------------------------------- 1 | HTTP/1.1 201 Created 2 | Content-Type: application/json 3 | 4 | { 5 | "data": { 6 | "attributes": { 7 | "age": 37, 8 | "email": "bob@example.com", 9 | "first_name": "Bob", 10 | "last_name": "Green", 11 | "status": "active" 12 | }, 13 | "id": "3", 14 | "type": "user" 15 | }, 16 | "jsonapi": { 17 | "version": "1.0" 18 | }, 19 | "meta": null 20 | } 21 | -------------------------------------------------------------------------------- /docs/http_snippets/snippets/example_one_api__get_user: -------------------------------------------------------------------------------- 1 | GET /users/3 HTTP/1.1 2 | Content-Type: application/vnd.api+json 3 | -------------------------------------------------------------------------------- /docs/http_snippets/snippets/example_one_api__get_user_result: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Content-Type: application/json 3 | 4 | { 5 | "data": { 6 | "attributes": { 7 | "age": 37, 8 | "email": "bob@example.com", 9 | "first_name": "Bob", 10 | "last_name": "Green", 11 | "status": "active" 12 | }, 13 | "id": "3", 14 | "type": "user" 15 | }, 16 | "jsonapi": { 17 | "version": "1.0" 18 | }, 19 | "meta": null 20 | } 21 | -------------------------------------------------------------------------------- /docs/http_snippets/snippets/example_one_api__get_user_with_computers: -------------------------------------------------------------------------------- 1 | GET /users/3?include=computers HTTP/1.1 2 | Content-Type: application/vnd.api+json 3 | -------------------------------------------------------------------------------- /docs/http_snippets/snippets/example_one_api__get_user_with_computers_result: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Content-Type: application/json 3 | 4 | { 5 | "data": { 6 | "attributes": { 7 | "age": 37, 8 | "email": "bob@example.com", 9 | "first_name": "Bob", 10 | "last_name": "Green", 11 | "status": "active" 12 | }, 13 | "id": "3", 14 | "relationships": { 15 | "computers": { 16 | "data": [ 17 | { 18 | "id": "2", 19 | "type": "computer" 20 | } 21 | ] 22 | } 23 | }, 24 | "type": "user" 25 | }, 26 | "included": [ 27 | { 28 | "attributes": { 29 | "name": "Amstrad" 30 | }, 31 | "id": "2", 32 | "type": "computer" 33 | } 34 | ], 35 | "jsonapi": { 36 | "version": "1.0" 37 | }, 38 | "meta": null 39 | } 40 | -------------------------------------------------------------------------------- /docs/http_snippets/snippets/example_one_api__get_users: -------------------------------------------------------------------------------- 1 | GET /users HTTP/1.1 2 | Content-Type: application/vnd.api+json 3 | -------------------------------------------------------------------------------- /docs/http_snippets/snippets/example_one_api__get_users_result: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Content-Type: application/json 3 | 4 | { 5 | "data": [ 6 | { 7 | "attributes": { 8 | "age": 21, 9 | "email": "john@example.com", 10 | "first_name": "John", 11 | "last_name": "Smith", 12 | "status": "active" 13 | }, 14 | "id": "1", 15 | "type": "user" 16 | }, 17 | { 18 | "attributes": { 19 | "age": 42, 20 | "email": "sam@example.com", 21 | "first_name": "Sam", 22 | "last_name": "White", 23 | "status": "active" 24 | }, 25 | "id": "2", 26 | "type": "user" 27 | }, 28 | { 29 | "attributes": { 30 | "age": 37, 31 | "email": "bob@example.com", 32 | "first_name": "Bob", 33 | "last_name": "Green", 34 | "status": "active" 35 | }, 36 | "id": "3", 37 | "type": "user" 38 | } 39 | ], 40 | "jsonapi": { 41 | "version": "1.0" 42 | }, 43 | "meta": { 44 | "count": 3, 45 | "totalPages": 1 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /docs/http_snippets/snippets/example_one_api__get_users_with_computers: -------------------------------------------------------------------------------- 1 | GET /users?include=computers HTTP/1.1 2 | Content-Type: application/vnd.api+json 3 | -------------------------------------------------------------------------------- /docs/http_snippets/snippets/example_one_api__get_users_with_computers_result: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Content-Type: application/json 3 | 4 | { 5 | "data": [ 6 | { 7 | "attributes": { 8 | "age": 21, 9 | "email": "john@example.com", 10 | "first_name": "John", 11 | "last_name": "Smith", 12 | "status": "active" 13 | }, 14 | "id": "1", 15 | "relationships": { 16 | "computers": { 17 | "data": [] 18 | } 19 | }, 20 | "type": "user" 21 | }, 22 | { 23 | "attributes": { 24 | "age": 42, 25 | "email": "sam@example.com", 26 | "first_name": "Sam", 27 | "last_name": "White", 28 | "status": "active" 29 | }, 30 | "id": "2", 31 | "relationships": { 32 | "computers": { 33 | "data": [ 34 | { 35 | "id": "1", 36 | "type": "computer" 37 | } 38 | ] 39 | } 40 | }, 41 | "type": "user" 42 | }, 43 | { 44 | "attributes": { 45 | "age": 37, 46 | "email": "bob@example.com", 47 | "first_name": "Bob", 48 | "last_name": "Green", 49 | "status": "active" 50 | }, 51 | "id": "3", 52 | "relationships": { 53 | "computers": { 54 | "data": [ 55 | { 56 | "id": "2", 57 | "type": "computer" 58 | } 59 | ] 60 | } 61 | }, 62 | "type": "user" 63 | } 64 | ], 65 | "included": [ 66 | { 67 | "attributes": { 68 | "name": "ZX Spectrum" 69 | }, 70 | "id": "1", 71 | "type": "computer" 72 | }, 73 | { 74 | "attributes": { 75 | "name": "Amstrad" 76 | }, 77 | "id": "2", 78 | "type": "computer" 79 | } 80 | ], 81 | "jsonapi": { 82 | "version": "1.0" 83 | }, 84 | "meta": { 85 | "count": 3, 86 | "totalPages": 1 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /docs/http_snippets/snippets/includes__many_to_many: -------------------------------------------------------------------------------- 1 | GET /parents?include=children%2Cchildren.child HTTP/1.1 2 | Content-Type: application/vnd.api+json 3 | -------------------------------------------------------------------------------- /docs/http_snippets/snippets/minimal_api__create_user: -------------------------------------------------------------------------------- 1 | POST /users HTTP/1.1 2 | Content-Type: application/vnd.api+json 3 | 4 | { 5 | "data": { 6 | "type": "user", 7 | "attributes": { 8 | "name": "John" 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /docs/http_snippets/snippets/minimal_api__create_user_result: -------------------------------------------------------------------------------- 1 | HTTP/1.1 201 Created 2 | Content-Type: application/vnd.api+json 3 | 4 | { 5 | "data": { 6 | "attributes": { 7 | "name": "John" 8 | }, 9 | "id": "1", 10 | "links": { 11 | "self": "/users/1" 12 | }, 13 | "type": "user" 14 | }, 15 | "jsonapi": { 16 | "version": "1.0" 17 | }, 18 | "links": { 19 | "self": "/users/1" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /docs/http_snippets/snippets/minimal_api__delete_user: -------------------------------------------------------------------------------- 1 | DELETE /users/1 HTTP/1.1 2 | Content-Type: application/vnd.api+json 3 | -------------------------------------------------------------------------------- /docs/http_snippets/snippets/minimal_api__delete_user_result: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Content-Type: application/vnd.api+json 3 | 4 | { 5 | "jsonapi": { 6 | "version": "1.0" 7 | }, 8 | "meta": { 9 | "message": "Object successfully deleted" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /docs/http_snippets/snippets/minimal_api__get_user: -------------------------------------------------------------------------------- 1 | GET /users/1 HTTP/1.1 2 | Content-Type: application/vnd.api+json 3 | -------------------------------------------------------------------------------- /docs/http_snippets/snippets/minimal_api__get_user_result: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Content-Type: application/vnd.api+json 3 | 4 | { 5 | "data": { 6 | "attributes": { 7 | "name": "John" 8 | }, 9 | "id": "1", 10 | "links": { 11 | "self": "/users/1" 12 | }, 13 | "type": "user" 14 | }, 15 | "jsonapi": { 16 | "version": "1.0" 17 | }, 18 | "links": { 19 | "self": "/users/1" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /docs/http_snippets/snippets/minimal_api__get_users: -------------------------------------------------------------------------------- 1 | GET /users HTTP/1.1 2 | Content-Type: application/vnd.api+json 3 | -------------------------------------------------------------------------------- /docs/http_snippets/snippets/minimal_api__get_users_result: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Content-Type: application/vnd.api+json 3 | 4 | { 5 | "data": [ 6 | { 7 | "attributes": { 8 | "name": "John" 9 | }, 10 | "id": "1", 11 | "links": { 12 | "self": "/users/1" 13 | }, 14 | "type": "user" 15 | } 16 | ], 17 | "jsonapi": { 18 | "version": "1.0" 19 | }, 20 | "links": { 21 | "self": "http://localhost:5000/users" 22 | }, 23 | "meta": { 24 | "count": 1 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /docs/http_snippets/snippets/minimal_api__patch_user: -------------------------------------------------------------------------------- 1 | PATCH /users/1 HTTP/1.1 2 | Content-Type: application/vnd.api+json 3 | 4 | { 5 | "data": { 6 | "id": 1, 7 | "type": "user", 8 | "attributes": { 9 | "name": "Sam" 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /docs/http_snippets/snippets/minimal_api__patch_user_result: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Content-Type: application/vnd.api+json 3 | 4 | { 5 | "data": { 6 | "attributes": { 7 | "name": "Sam" 8 | }, 9 | "id": "1", 10 | "links": { 11 | "self": "/users/1" 12 | }, 13 | "type": "user" 14 | }, 15 | "jsonapi": { 16 | "version": "1.0" 17 | }, 18 | "links": { 19 | "self": "/users/1" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /docs/http_snippets/snippets/relationship_api__create_computer: -------------------------------------------------------------------------------- 1 | POST /computers HTTP/1.1 2 | Content-Type: application/vnd.api+json 3 | 4 | { 5 | "data": { 6 | "type": "computer", 7 | "attributes": { 8 | "serial": "Amstrad" 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /docs/http_snippets/snippets/relationship_api__create_computer_relationship_for_user: -------------------------------------------------------------------------------- 1 | POST /users/1/relationships/computers HTTP/1.1 2 | Content-Type: application/vnd.api+json 3 | 4 | { 5 | "data": [ 6 | { 7 | "type": "computer", 8 | "id": "4" 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /docs/http_snippets/snippets/relationship_api__create_computer_relationship_for_user_result: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Content-Type: application/vnd.api+json 3 | 4 | { 5 | "jsonapi": { 6 | "version": "1.0" 7 | }, 8 | "meta": { 9 | "message": "Relationship successfully created" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /docs/http_snippets/snippets/relationship_api__create_computer_result: -------------------------------------------------------------------------------- 1 | HTTP/1.1 201 Created 2 | Content-Type: application/vnd.api+json 3 | 4 | { 5 | "data": { 6 | "attributes": { 7 | "serial": "Amstrad" 8 | }, 9 | "id": "1", 10 | "links": { 11 | "self": "/computers/1" 12 | }, 13 | "relationships": { 14 | "owner": { 15 | "links": { 16 | "related": "/computers/1/owner", 17 | "self": "/computers/1/relationships/owner" 18 | } 19 | } 20 | }, 21 | "type": "computer" 22 | }, 23 | "jsonapi": { 24 | "version": "1.0" 25 | }, 26 | "links": { 27 | "self": "/computers/1" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /docs/http_snippets/snippets/relationship_api__create_user_with_computer_relationship: -------------------------------------------------------------------------------- 1 | POST /users?include=computers HTTP/1.1 2 | Content-Type: application/vnd.api+json 3 | 4 | { 5 | "data": { 6 | "type": "user", 7 | "attributes": { 8 | "name": "John", 9 | "email": "john@exmple.com" 10 | }, 11 | "relationships": { 12 | "computers": { 13 | "data": [ 14 | { 15 | "type": "computer", 16 | "id": "2" 17 | } 18 | ] 19 | } 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /docs/http_snippets/snippets/relationship_api__create_user_with_computer_relationship_result: -------------------------------------------------------------------------------- 1 | HTTP/1.1 201 Created 2 | Content-Type: application/vnd.api+json 3 | 4 | { 5 | "data": { 6 | "attributes": { 7 | "display_name": "JOHN ", 8 | "name": "John" 9 | }, 10 | "id": "1", 11 | "links": { 12 | "self": "/users/1" 13 | }, 14 | "relationships": { 15 | "computers": { 16 | "data": [ 17 | { 18 | "id": "2", 19 | "type": "computer" 20 | } 21 | ], 22 | "links": { 23 | "related": "/users/1/computers", 24 | "self": "/users/1/relationships/computers" 25 | } 26 | } 27 | }, 28 | "type": "user" 29 | }, 30 | "included": [ 31 | { 32 | "attributes": { 33 | "serial": "Halo" 34 | }, 35 | "id": "2", 36 | "links": { 37 | "self": "/computers/2" 38 | }, 39 | "relationships": { 40 | "owner": { 41 | "links": { 42 | "related": "/computers/2/owner", 43 | "self": "/computers/2/relationships/owner" 44 | } 45 | } 46 | }, 47 | "type": "computer" 48 | } 49 | ], 50 | "jsonapi": { 51 | "version": "1.0" 52 | }, 53 | "links": { 54 | "self": "/users/1" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /docs/http_snippets/snippets/relationship_api__delete_computer: -------------------------------------------------------------------------------- 1 | DELETE /computers/1 HTTP/1.1 2 | Content-Type: application/vnd.api+json 3 | -------------------------------------------------------------------------------- /docs/http_snippets/snippets/relationship_api__delete_computer_relationship: -------------------------------------------------------------------------------- 1 | DELETE /users/1/relationships/computers HTTP/1.1 2 | Content-Type: application/vnd.api+json 3 | 4 | { 5 | "data": [ 6 | { 7 | "type": "computer", 8 | "id": "3" 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /docs/http_snippets/snippets/relationship_api__delete_computer_relationship_result: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Content-Type: application/vnd.api+json 3 | 4 | { 5 | "jsonapi": { 6 | "version": "1.0" 7 | }, 8 | "meta": { 9 | "message": "Relationship successfully updated" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /docs/http_snippets/snippets/relationship_api__delete_computer_result: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Content-Type: application/vnd.api+json 3 | 4 | { 5 | "jsonapi": { 6 | "version": "1.0" 7 | }, 8 | "meta": { 9 | "message": "Object successfully deleted" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /docs/http_snippets/snippets/relationship_api__get_computers: -------------------------------------------------------------------------------- 1 | GET /computers HTTP/1.1 2 | Content-Type: application/vnd.api+json 3 | -------------------------------------------------------------------------------- /docs/http_snippets/snippets/relationship_api__get_computers_result: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Content-Type: application/vnd.api+json 3 | 4 | { 5 | "data": [ 6 | { 7 | "attributes": { 8 | "serial": "Amstrad" 9 | }, 10 | "id": "1", 11 | "links": { 12 | "self": "/computers/1" 13 | }, 14 | "relationships": { 15 | "owner": { 16 | "links": { 17 | "related": "/computers/1/owner", 18 | "self": "/computers/1/relationships/owner" 19 | } 20 | } 21 | }, 22 | "type": "computer" 23 | } 24 | ], 25 | "jsonapi": { 26 | "version": "1.0" 27 | }, 28 | "links": { 29 | "self": "http://localhost:5000/computers" 30 | }, 31 | "meta": { 32 | "count": 1 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /docs/http_snippets/snippets/relationship_api__get_user_related_computers: -------------------------------------------------------------------------------- 1 | GET /users/1/computers HTTP/1.1 2 | Content-Type: application/vnd.api+json 3 | -------------------------------------------------------------------------------- /docs/http_snippets/snippets/relationship_api__get_user_related_computers_result: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Content-Type: application/vnd.api+json 3 | 4 | { 5 | "data": [ 6 | { 7 | "attributes": { 8 | "serial": "Nestor" 9 | }, 10 | "id": "3", 11 | "links": { 12 | "self": "/computers/3" 13 | }, 14 | "relationships": { 15 | "owner": { 16 | "links": { 17 | "related": "/computers/3/owner", 18 | "self": "/computers/3/relationships/owner" 19 | } 20 | } 21 | }, 22 | "type": "computer" 23 | }, 24 | { 25 | "attributes": { 26 | "serial": "Commodore" 27 | }, 28 | "id": "4", 29 | "links": { 30 | "self": "/computers/4" 31 | }, 32 | "relationships": { 33 | "owner": { 34 | "links": { 35 | "related": "/computers/4/owner", 36 | "self": "/computers/4/relationships/owner" 37 | } 38 | } 39 | }, 40 | "type": "computer" 41 | } 42 | ], 43 | "jsonapi": { 44 | "version": "1.0" 45 | }, 46 | "links": { 47 | "first": "http://localhost:5000/computers", 48 | "last": "http://localhost:5000/computers?page%5Bnumber%5D=2", 49 | "next": "http://localhost:5000/computers?page%5Bnumber%5D=2", 50 | "self": "http://localhost:5000/computers" 51 | }, 52 | "meta": { 53 | "count": 2 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /docs/http_snippets/snippets/relationship_api__get_user_with_computers: -------------------------------------------------------------------------------- 1 | GET /users/1?include=computers HTTP/1.1 2 | Content-Type: application/vnd.api+json 3 | -------------------------------------------------------------------------------- /docs/http_snippets/snippets/relationship_api__get_user_with_computers_as_relationship: -------------------------------------------------------------------------------- 1 | GET /users/1/relationships/computers HTTP/1.1 2 | Content-Type: application/vnd.api+json 3 | -------------------------------------------------------------------------------- /docs/http_snippets/snippets/relationship_api__get_user_with_computers_as_relationship_result: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Content-Type: application/vnd.api+json 3 | 4 | { 5 | "data": { 6 | "attributes": { 7 | "serial": "Amstrad" 8 | }, 9 | "id": "1", 10 | "links": { 11 | "self": "/computers/1" 12 | }, 13 | "relationships": { 14 | "owner": { 15 | "links": { 16 | "related": "/computers/1/owner", 17 | "self": "/computers/1/relationships/owner" 18 | } 19 | } 20 | }, 21 | "type": "computer" 22 | }, 23 | "jsonapi": { 24 | "version": "1.0" 25 | }, 26 | "links": { 27 | "self": "/computers/1" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /docs/http_snippets/snippets/relationship_api__get_user_with_computers_result: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Content-Type: application/vnd.api+json 3 | 4 | { 5 | "data": { 6 | "attributes": { 7 | "display_name": "JOHN ", 8 | "name": "John" 9 | }, 10 | "id": "1", 11 | "links": { 12 | "self": "/users/1" 13 | }, 14 | "relationships": { 15 | "computers": { 16 | "data": [ 17 | { 18 | "id": "3", 19 | "type": "computer" 20 | }, 21 | { 22 | "id": "4", 23 | "type": "computer" 24 | } 25 | ], 26 | "links": { 27 | "related": "/users/1/computers", 28 | "self": "/users/1/relationships/computers" 29 | } 30 | } 31 | }, 32 | "type": "user" 33 | }, 34 | "included": [ 35 | { 36 | "attributes": { 37 | "serial": "Nestor" 38 | }, 39 | "id": "3", 40 | "links": { 41 | "self": "/computers/3" 42 | }, 43 | "relationships": { 44 | "owner": { 45 | "links": { 46 | "related": "/computers/3/owner", 47 | "self": "/computers/3/relationships/owner" 48 | } 49 | } 50 | }, 51 | "type": "computer" 52 | }, 53 | { 54 | "attributes": { 55 | "serial": "Commodore" 56 | }, 57 | "id": "4", 58 | "links": { 59 | "self": "/computers/4" 60 | }, 61 | "relationships": { 62 | "owner": { 63 | "links": { 64 | "related": "/computers/4/owner", 65 | "self": "/computers/4/relationships/owner" 66 | } 67 | } 68 | }, 69 | "type": "computer" 70 | } 71 | ], 72 | "jsonapi": { 73 | "version": "1.0" 74 | }, 75 | "links": { 76 | "self": "/users/1" 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /docs/http_snippets/snippets/relationship_api__patch_computer: -------------------------------------------------------------------------------- 1 | PATCH /computers/1 HTTP/1.1 2 | Content-Type: application/vnd.api+json 3 | 4 | { 5 | "data": { 6 | "type": "computer", 7 | "id": "1", 8 | "attributes": { 9 | "serial": "New Amstrad" 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /docs/http_snippets/snippets/relationship_api__patch_computer_result: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Content-Type: application/vnd.api+json 3 | 4 | { 5 | "data": { 6 | "attributes": { 7 | "serial": "New Amstrad" 8 | }, 9 | "id": "1", 10 | "links": { 11 | "self": "/computers/1" 12 | }, 13 | "relationships": { 14 | "owner": { 15 | "links": { 16 | "related": "/computers/1/owner", 17 | "self": "/computers/1/relationships/owner" 18 | } 19 | } 20 | }, 21 | "type": "computer" 22 | }, 23 | "jsonapi": { 24 | "version": "1.0" 25 | }, 26 | "links": { 27 | "self": "/computers/1" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /docs/http_snippets/snippets/relationship_api__update_user_with_computer_relationship: -------------------------------------------------------------------------------- 1 | PATCH /users/1?include=computers HTTP/1.1 2 | Content-Type: application/vnd.api+json 3 | 4 | { 5 | "data": { 6 | "type": "user", 7 | "id": "1", 8 | "attributes": { 9 | "email": "john@example.com" 10 | }, 11 | "relationships": { 12 | "computers": { 13 | "data": [ 14 | { 15 | "type": "computer", 16 | "id": "3" 17 | } 18 | ] 19 | } 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /docs/http_snippets/snippets/relationship_api__update_user_with_computer_relationship_result: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Content-Type: application/vnd.api+json 3 | 4 | { 5 | "data": { 6 | "attributes": { 7 | "display_name": "JOHN ", 8 | "name": "John" 9 | }, 10 | "id": "1", 11 | "links": { 12 | "self": "/users/1" 13 | }, 14 | "relationships": { 15 | "computers": { 16 | "data": [ 17 | { 18 | "id": "3", 19 | "type": "computer" 20 | } 21 | ], 22 | "links": { 23 | "related": "/users/1/computers", 24 | "self": "/users/1/relationships/computers" 25 | } 26 | } 27 | }, 28 | "type": "user" 29 | }, 30 | "included": [ 31 | { 32 | "attributes": { 33 | "serial": "Nestor" 34 | }, 35 | "id": "3", 36 | "links": { 37 | "self": "/computers/3" 38 | }, 39 | "relationships": { 40 | "owner": { 41 | "links": { 42 | "related": "/computers/3/owner", 43 | "self": "/computers/3/relationships/owner" 44 | } 45 | } 46 | }, 47 | "type": "computer" 48 | } 49 | ], 50 | "jsonapi": { 51 | "version": "1.0" 52 | }, 53 | "links": { 54 | "self": "/users/1" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /docs/http_snippets/snippets/view_dependencies__get_items_forbidden: -------------------------------------------------------------------------------- 1 | GET /users HTTP/1.1 2 | Content-Type: application/vnd.api+json 3 | -------------------------------------------------------------------------------- /docs/http_snippets/snippets/view_dependencies__get_items_forbidden_result: -------------------------------------------------------------------------------- 1 | HTTP/1.1 403 Forbidden 2 | Content-Type: application/vnd.api+json 3 | 4 | { 5 | "errors": [ 6 | { 7 | "detail": "Only admin user have permissions to this endpoint", 8 | "status_code": 403, 9 | "title": "Forbidden" 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /docs/http_snippets/snippets/view_dependencies__get_items_with_permissions: -------------------------------------------------------------------------------- 1 | GET /users HTTP/1.1 2 | Content-Type: application/vnd.api+json 3 | X-AUTH: admin 4 | -------------------------------------------------------------------------------- /docs/http_snippets/snippets/view_dependencies__get_items_with_permissions_result: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Content-Type: application/vnd.api+json 3 | 4 | { 5 | "data": [ 6 | { 7 | "attributes": { 8 | "name": "John" 9 | }, 10 | "id": "1", 11 | "links": { 12 | "self": "/users/1" 13 | }, 14 | "type": "user" 15 | } 16 | ], 17 | "jsonapi": { 18 | "version": "1.0" 19 | }, 20 | "links": { 21 | "self": "http://localhost:5000/users" 22 | }, 23 | "meta": { 24 | "count": 1 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /docs/img/schema.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mts-ai/FastAPI-JSONAPI/a1a4fc5a868ab6bb563919c462ea248250e83b26/docs/img/schema.png -------------------------------------------------------------------------------- /docs/include_many_to_many.rst: -------------------------------------------------------------------------------- 1 | .. _include_many_to_many: 2 | 3 | Include nested and related, Many-to-Many 4 | ######################################## 5 | 6 | .. currentmodule:: fastapi_jsonapi 7 | 8 | The same as usual includes. Here's an example with an association object. 9 | 10 | Example (sources `here `_): 11 | 12 | Prepare models and schemas 13 | ========================== 14 | 15 | 16 | Define SQLAlchemy models 17 | ------------------------ 18 | 19 | 20 | Parent model 21 | ^^^^^^^^^^^^ 22 | 23 | ``models/parent.py``: 24 | 25 | .. literalinclude:: ../examples/api_for_sqlalchemy/models/parent.py 26 | :language: python 27 | 28 | 29 | 30 | Child model 31 | ^^^^^^^^^^^ 32 | 33 | ``models/child.py``: 34 | 35 | .. literalinclude:: ../examples/api_for_sqlalchemy/models/child.py 36 | :language: python 37 | 38 | 39 | 40 | Parent to Child Association model 41 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 42 | 43 | ``models/parent_child_association.py``: 44 | 45 | .. literalinclude:: ../examples/api_for_sqlalchemy/models/parent_child_association.py 46 | :language: python 47 | 48 | 49 | 50 | 51 | Define pydantic schemas 52 | ----------------------- 53 | 54 | 55 | Parent Schema 56 | ^^^^^^^^^^^^^ 57 | 58 | ``schemas/parent.py``: 59 | 60 | .. literalinclude:: ../examples/api_for_sqlalchemy/models/schemas/parent.py 61 | :language: python 62 | 63 | 64 | Child Schema 65 | ^^^^^^^^^^^^ 66 | 67 | ``schemas/child.py``: 68 | 69 | .. literalinclude:: ../examples/api_for_sqlalchemy/models/schemas/child.py 70 | :language: python 71 | 72 | 73 | Parent to Child Association Schema 74 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 75 | 76 | ``schemas/parent_child_association.py``: 77 | 78 | .. literalinclude:: ../examples/api_for_sqlalchemy/models/schemas/parent_child_association.py 79 | :language: python 80 | 81 | 82 | 83 | 84 | Define view classes 85 | ------------------- 86 | 87 | 88 | Base Views 89 | ^^^^^^^^^^ 90 | 91 | ``api/base.py``: 92 | 93 | .. literalinclude:: ../examples/api_for_sqlalchemy/api/views_base.py 94 | :language: python 95 | 96 | 97 | List Parent objects with Children through an Association object 98 | =============================================================== 99 | 100 | Request: 101 | 102 | .. literalinclude:: ./http_snippets/snippets/includes__many_to_many 103 | :language: HTTP 104 | 105 | Response: 106 | 107 | .. literalinclude:: ./http_snippets/snippets/includes__many_to_many_result 108 | :language: HTTP 109 | -------------------------------------------------------------------------------- /docs/installation.rst: -------------------------------------------------------------------------------- 1 | .. _installation: 2 | 3 | Installation 4 | ============ 5 | 6 | .. currentmodule:: fastapi_jsonapi 7 | 8 | Install FastAPI-JSONAPI with ``pip`` :: 9 | 10 | pip install FastAPI-JSONAPI 11 | 12 | 13 | The development version can be downloaded from `its page at GitHub 14 | `_. :: 15 | 16 | git clone https://github.com/mts-ai/FastAPI-JSONAPI.git 17 | cd fastapi-jsonapi 18 | poetry install poetry install --all-extras 19 | 20 | .. note:: 21 | 22 | If you don't have virtualenv please install it 23 | 24 | $ pip install virtualenv 25 | 26 | If you don't have poetry please install it 27 | 28 | $ pip install poetry 29 | -------------------------------------------------------------------------------- /docs/minimal_api_example.rst: -------------------------------------------------------------------------------- 1 | .. _minimal_api_example: 2 | 3 | .. include:: ./minimal_api_head.rst 4 | 5 | Request: 6 | 7 | .. literalinclude:: ./http_snippets/snippets/minimal_api__create_user 8 | :language: HTTP 9 | 10 | Response: 11 | 12 | .. literalinclude:: ./http_snippets/snippets/minimal_api__create_user_result 13 | :language: HTTP 14 | 15 | 16 | Request: 17 | 18 | .. literalinclude:: ./http_snippets/snippets/minimal_api__get_user 19 | :language: HTTP 20 | 21 | Response: 22 | 23 | .. literalinclude:: ./http_snippets/snippets/minimal_api__get_user_result 24 | :language: HTTP 25 | 26 | 27 | Request: 28 | 29 | .. literalinclude:: ./http_snippets/snippets/minimal_api__get_users 30 | :language: HTTP 31 | 32 | Response: 33 | 34 | .. literalinclude:: ./http_snippets/snippets/minimal_api__get_users_result 35 | :language: HTTP 36 | 37 | 38 | Request: 39 | 40 | .. literalinclude:: ./http_snippets/snippets/minimal_api__patch_user 41 | :language: HTTP 42 | 43 | Response: 44 | 45 | .. literalinclude:: ./http_snippets/snippets/minimal_api__patch_user_result 46 | :language: HTTP 47 | 48 | 49 | Request: 50 | 51 | .. literalinclude:: ./http_snippets/snippets/minimal_api__delete_user 52 | :language: HTTP 53 | 54 | Response: 55 | 56 | .. literalinclude:: ./http_snippets/snippets/minimal_api__delete_user_result 57 | :language: HTTP 58 | -------------------------------------------------------------------------------- /docs/minimal_api_head.rst: -------------------------------------------------------------------------------- 1 | A minimal API 2 | ============= 3 | 4 | .. literalinclude:: ../examples/api_minimal.py 5 | :language: python 6 | 7 | 8 | This example provides the following API structure: 9 | 10 | ======================== ====== ============= =========================== 11 | URL method endpoint Usage 12 | ======================== ====== ============= =========================== 13 | /users GET user_list Get a collection of users 14 | /users POST user_list Create a user 15 | /users DELETE user_list Delete users 16 | /users/{user_id} GET user_detail Get user details 17 | /users/{user_id} PATCH user_detail Update a user 18 | /users/{user_id} DELETE user_detail Delete a user 19 | ======================== ====== ============= =========================== 20 | -------------------------------------------------------------------------------- /docs/oauth.rst: -------------------------------------------------------------------------------- 1 | .. _oauth: 2 | 3 | OAuth 4 | ===== 5 | 6 | .. currentmodule:: fastapi_jsonapi 7 | 8 | in developing 9 | -------------------------------------------------------------------------------- /docs/pagination.rst: -------------------------------------------------------------------------------- 1 | .. _pagination: 2 | 3 | Pagination 4 | ========== 5 | 6 | .. currentmodule:: fastapi_jsonapi 7 | 8 | When you use the default implementation of the get method on a ResourceList 9 | your results will be paginated by default. 10 | Default pagination size is 30 but you can manage it from querystring parameter named "page". 11 | 12 | .. note:: 13 | 14 | Examples are not URL encoded for a better readability 15 | 16 | Size 17 | ---- 18 | 19 | You can control page size like this: 20 | 21 | .. sourcecode:: http 22 | 23 | GET /users?page[size]=10 HTTP/1.1 24 | Accept: application/vnd.api+json 25 | 26 | Number 27 | ------ 28 | 29 | You can control page number like this: 30 | 31 | .. sourcecode:: http 32 | 33 | GET /users?page[number]=2 HTTP/1.1 34 | Accept: application/vnd.api+json 35 | 36 | Size + Number 37 | ------------- 38 | 39 | Of course, you can control both like this: 40 | 41 | .. sourcecode:: http 42 | 43 | GET /users?page[size]=10&page[number]=2 HTTP/1.1 44 | Accept: application/vnd.api+json 45 | 46 | Disable pagination 47 | ------------------ 48 | 49 | You can disable pagination by setting size to 0 50 | 51 | .. sourcecode:: http 52 | 53 | GET /users?page[size]=0 HTTP/1.1 54 | Accept: application/vnd.api+json 55 | -------------------------------------------------------------------------------- /docs/permission.rst: -------------------------------------------------------------------------------- 1 | .. _permission: 2 | 3 | Permission 4 | ========== 5 | 6 | .. currentmodule:: fastapi_jsonapi 7 | 8 | in developing 9 | -------------------------------------------------------------------------------- /docs/python_snippets/data_layer/custom_data_layer.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | 3 | from fastapi_jsonapi import ApplicationBuilder 4 | from fastapi_jsonapi.data_layers.base import BaseDataLayer 5 | from fastapi_jsonapi.data_layers.sqla.orm import SqlalchemyDataLayer 6 | from fastapi_jsonapi.views import ViewBase 7 | 8 | 9 | class MyCustomDataLayer(BaseDataLayer): 10 | """Overload abstract methods here""" 11 | 12 | 13 | class MyCustomSqlaDataLayer(SqlalchemyDataLayer): 14 | """Overload any methods here""" 15 | 16 | async def before_delete_objects(self, objects: list, view_kwargs: dict): 17 | raise Exception("not allowed to delete objects") 18 | 19 | 20 | class UserView(ViewBase): 21 | data_layer_cls = MyCustomDataLayer 22 | 23 | 24 | app = FastAPI() 25 | builder = ApplicationBuilder(app) 26 | builder.add_resource( 27 | # ... 28 | view=UserView, 29 | # ... 30 | ) 31 | -------------------------------------------------------------------------------- /docs/python_snippets/relationships/models.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import Optional 4 | 5 | from sqlalchemy import ForeignKey 6 | from sqlalchemy.orm import relationship, Mapped, mapped_column 7 | 8 | from examples.api_for_sqlalchemy.models.base import Base 9 | 10 | 11 | class User(Base): 12 | __tablename__ = "users" 13 | 14 | name: Mapped[str] 15 | 16 | bio: Mapped[UserBio] = relationship(back_populates="user") 17 | computers: Mapped[list[Computer]] = relationship(back_populates="user") 18 | 19 | 20 | class Computer(Base): 21 | __tablename__ = "computers" 22 | 23 | name: Mapped[str] 24 | 25 | user_id: Mapped[Optional[int]] = mapped_column(ForeignKey("users.id")) 26 | user: Mapped[User] = relationship(back_populates="computers") 27 | 28 | 29 | class UserBio(Base): 30 | __tablename__ = "user_bio" 31 | 32 | birth_city: Mapped[str] = mapped_column(default="", server_default="") 33 | favourite_movies: Mapped[str] = mapped_column(default="", server_default="") 34 | 35 | user_id: Mapped[int] = mapped_column(ForeignKey("users.id"), unique=True) 36 | user: Mapped[User] = relationship(back_populates="bio") 37 | -------------------------------------------------------------------------------- /docs/python_snippets/relationships/relationships_info_example.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import Optional, Annotated 4 | 5 | from pydantic import BaseModel, ConfigDict 6 | 7 | from fastapi_jsonapi.types_metadata import RelationshipInfo 8 | 9 | 10 | class UserBaseSchema(BaseModel): 11 | model_config = ConfigDict( 12 | from_attributes=True, 13 | ) 14 | 15 | id: int 16 | name: str 17 | 18 | bio: Annotated[ 19 | Optional[UserBioBaseSchema], 20 | RelationshipInfo( 21 | resource_type="user_bio", 22 | ), 23 | ] = None 24 | computers: Annotated[ 25 | Optional[list[ComputerBaseSchema]], 26 | RelationshipInfo( 27 | resource_type="computer", 28 | many=True, 29 | ), 30 | ] = None 31 | 32 | 33 | class UserSchema(BaseModel): 34 | id: int 35 | name: str 36 | 37 | 38 | class UserBioBaseSchema(BaseModel): 39 | birth_city: str 40 | favourite_movies: str 41 | 42 | user: Annotated[ 43 | Optional[UserSchema], 44 | RelationshipInfo( 45 | resource_type="user", 46 | ), 47 | ] = None 48 | 49 | 50 | class ComputerBaseSchema(BaseModel): 51 | id: int 52 | name: str 53 | 54 | user: Annotated[ 55 | Optional[UserSchema], 56 | RelationshipInfo( 57 | resource_type="user", 58 | ), 59 | ] = None 60 | -------------------------------------------------------------------------------- /docs/python_snippets/routing/router.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | 3 | from examples.api_for_sqlalchemy.models import User 4 | from examples.api_for_sqlalchemy.schemas import UserInSchema, UserPatchSchema, UserSchema 5 | from examples.api_for_sqlalchemy.urls import ViewBase 6 | from fastapi_jsonapi import ApplicationBuilder 7 | 8 | 9 | def add_routes(app: FastAPI): 10 | builder = ApplicationBuilder(app) 11 | builder.add_resource( 12 | path="/users", 13 | tags=["User"], 14 | view=ViewBase, 15 | model=User, 16 | schema=UserSchema, 17 | resource_type="user", 18 | schema_in_patch=UserPatchSchema, 19 | schema_in_post=UserInSchema, 20 | ) 21 | 22 | 23 | app = FastAPI() 24 | add_routes(app) 25 | -------------------------------------------------------------------------------- /docs/python_snippets/view_dependencies/main_example.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, ClassVar 2 | 3 | from fastapi import Depends, Header 4 | from pydantic import BaseModel, ConfigDict 5 | from sqlalchemy.ext.asyncio import AsyncSession 6 | from typing_extensions import Annotated 7 | 8 | from examples.api_for_sqlalchemy.models.db import DB 9 | from fastapi_jsonapi.exceptions import Forbidden 10 | from fastapi_jsonapi.misc.sqla.generics.base import ViewBaseGeneric 11 | from fastapi_jsonapi.views import ViewBase, Operation, OperationConfig 12 | 13 | db = DB( 14 | url="sqlite+aiosqlite:///tmp/db.sqlite3", 15 | ) 16 | 17 | 18 | class SessionDependency(BaseModel): 19 | model_config = ConfigDict( 20 | arbitrary_types_allowed=True, 21 | ) 22 | 23 | session: AsyncSession = Depends(db.session) 24 | 25 | 26 | async def common_handler(view: ViewBase, dto: SessionDependency) -> dict: 27 | return { 28 | "session": dto.session, 29 | } 30 | 31 | 32 | async def check_that_user_is_admin(x_auth: Annotated[str, Header()]): 33 | if x_auth != "admin": 34 | raise Forbidden(detail="Only admin user have permissions to this endpoint.") 35 | 36 | 37 | class AdminOnlyPermission(BaseModel): 38 | is_admin: Optional[bool] = Depends(check_that_user_is_admin) 39 | 40 | 41 | class View(ViewBaseGeneric): 42 | operation_dependencies: ClassVar[dict[Operation, OperationConfig]] = { 43 | Operation.ALL: OperationConfig( 44 | dependencies=SessionDependency, 45 | prepare_data_layer_kwargs=common_handler, 46 | ), 47 | Operation.GET: OperationConfig( 48 | dependencies=AdminOnlyPermission, 49 | ), 50 | } 51 | -------------------------------------------------------------------------------- /docs/python_snippets/view_dependencies/several_dependencies.py: -------------------------------------------------------------------------------- 1 | from typing import ClassVar 2 | 3 | from fastapi import Depends 4 | from pydantic import BaseModel 5 | 6 | from fastapi_jsonapi.misc.sqla.generics.base import ViewBaseGeneric 7 | from fastapi_jsonapi.views import ViewBase, Operation, OperationConfig 8 | 9 | 10 | def one(): 11 | return 1 12 | 13 | 14 | def two(): 15 | return 2 16 | 17 | 18 | class CommonDependency(BaseModel): 19 | key_1: int = Depends(one) 20 | 21 | 22 | class GetDependency(BaseModel): 23 | key_2: int = Depends(two) 24 | 25 | 26 | class DependencyMix(CommonDependency, GetDependency): 27 | pass 28 | 29 | 30 | def common_handler(view: ViewBase, dto: CommonDependency) -> dict: 31 | return {"key_1": dto.key_1} 32 | 33 | 34 | def get_handler(view: ViewBase, dto: DependencyMix): 35 | return {"key_2": dto.key_2} 36 | 37 | 38 | class View(ViewBaseGeneric): 39 | operation_dependencies: ClassVar = { 40 | Operation.ALL: OperationConfig( 41 | dependencies=CommonDependency, 42 | prepare_data_layer_kwargs=common_handler, 43 | ), 44 | Operation.GET: OperationConfig( 45 | dependencies=GetDependency, 46 | prepare_data_layer_kwargs=get_handler, 47 | ), 48 | } 49 | -------------------------------------------------------------------------------- /docs/relationships.rst: -------------------------------------------------------------------------------- 1 | .. _relationships: 2 | 3 | Define relationships 4 | ==================== 5 | 6 | .. currentmodule:: fastapi_jsonapi 7 | 8 | As noted in quickstart, objects can accept a relationships. In order to make it technically 9 | possible to create, update, and modify relationships, you must declare a **RelationShipInfo** when 10 | creating a schema. 11 | 12 | As an example, let's say you have a user model, their biography, and the computers they own. The user 13 | and biographies are connected by To-One relationship, the user and computers are connected by To-Many 14 | relationship 15 | 16 | Models: 17 | 18 | .. literalinclude:: ./python_snippets/relationships/models.py 19 | :language: python 20 | 21 | 22 | Schemas: 23 | 24 | .. literalinclude:: ./python_snippets/relationships/relationships_info_example.py 25 | :language: python 26 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | fastapi>=0.112.3 2 | orjson>=3.2.1 3 | pydantic>=2.6.0 4 | sphinx 5 | sphinx_rtd_theme 6 | sqlalchemy>=2.0.26 7 | -------------------------------------------------------------------------------- /docs/routing.rst: -------------------------------------------------------------------------------- 1 | .. _routing: 2 | 3 | Routing 4 | ======= 5 | 6 | .. currentmodule:: fastapi_jsonapi 7 | 8 | Example: 9 | 10 | .. literalinclude:: ./python_snippets/routing/router.py 11 | :language: python 12 | -------------------------------------------------------------------------------- /docs/sorting.rst: -------------------------------------------------------------------------------- 1 | .. _sorting: 2 | 3 | Sorting 4 | ======= 5 | 6 | .. currentmodule:: fastapi_jsonapi 7 | 8 | You can sort results using the query string parameter named "sort" 9 | 10 | .. note:: 11 | 12 | Examples are not URL encoded for better readability 13 | 14 | Example: 15 | 16 | .. sourcecode:: http 17 | 18 | GET /users?sort=name HTTP/1.1 19 | Accept: application/vnd.api+json 20 | 21 | Multiple sort 22 | ------------- 23 | 24 | You can sort on multiple fields like this: 25 | 26 | .. sourcecode:: http 27 | 28 | GET /users?sort=name,birth_date HTTP/1.1 29 | Accept: application/vnd.api+json 30 | 31 | Descending sort 32 | --------------- 33 | 34 | You can in descending order using a minus symbol, "-", like this: 35 | 36 | .. sourcecode:: http 37 | 38 | GET /users?sort=-name HTTP/1.1 39 | Accept: application/vnd.api+json 40 | 41 | Multiple sort + Descending sort 42 | ------------------------------- 43 | 44 | Of course, you can combine both like this: 45 | 46 | .. sourcecode:: http 47 | 48 | GET /users?sort=-name,birth_date HTTP/1.1 49 | Accept: application/vnd.api+json 50 | -------------------------------------------------------------------------------- /docs/sparse_fieldsets.rst: -------------------------------------------------------------------------------- 1 | .. _sparse_fieldsets: 2 | 3 | Sparse fieldsets 4 | ================ 5 | 6 | .. currentmodule:: fastapi_jsonapi 7 | 8 | You can restrict the fields returned by your API using the query string parameter called "fields". It is very useful for performance purposes because fields not returned are not resolved by the API. You can use the "fields" parameter on any kind of route (classical CRUD route or relationships route) and any kind of HTTP methods as long as the method returns data. 9 | 10 | .. note:: 11 | 12 | Examples are not URL encoded for better readability 13 | 14 | The syntax of the fields parameter is :: 15 | 16 | ?fields[]= 17 | 18 | Example: 19 | 20 | .. sourcecode:: http 21 | 22 | GET /users?fields[user]=display_name HTTP/1.1 23 | Accept: application/vnd.api+json 24 | 25 | In this example user's display_name is the only field returned by the API. No relationship links are returned so the response is very fast because the API doesn't have to do any expensive computation of relationship links. 26 | 27 | You can manage returned fields for the entire response even for included objects 28 | 29 | Example: 30 | 31 | If you don't want to compute relationship links for included computers of a user you can do something like this 32 | 33 | .. sourcecode:: http 34 | 35 | GET /users/1?include=computers&fields[computer]=serial HTTP/1.1 36 | Accept: application/vnd.api+json 37 | 38 | And of course you can combine both like this: 39 | 40 | Example: 41 | 42 | .. sourcecode:: http 43 | 44 | GET /users/1?include=computers&fields[computer]=serial&fields[user]=name,computers HTTP/1.1 45 | Accept: application/vnd.api+json 46 | 47 | .. warning:: 48 | 49 | If you want to use both "fields" and "include", don't forget to specify the name of the relationship in "fields"; if you don't, the include wont work. 50 | -------------------------------------------------------------------------------- /docs/updated_includes_example.rst: -------------------------------------------------------------------------------- 1 | .. _updated_includes_example: 2 | 3 | Create and include related objects (updated example) 4 | #################################################### 5 | 6 | .. currentmodule:: fastapi_jsonapi 7 | 8 | You can include related object(s) details in responses with the query string parameter named "include". You can use the "include" parameter on any kind of route (classical CRUD route or relationships route) and any kind of HTTP methods as long as the method returns data. 9 | 10 | This feature will add an additional key in the result named "included" 11 | 12 | Example 13 | ======= 14 | 15 | 16 | Create user 17 | ----------- 18 | 19 | Request: 20 | 21 | .. literalinclude:: ./http_snippets/snippets/example_one_api__create_user 22 | :language: HTTP 23 | 24 | Response: 25 | 26 | .. literalinclude:: ./http_snippets/snippets/example_one_api__create_user_result 27 | :language: HTTP 28 | 29 | 30 | Create computer for user and fetch related user 31 | ----------------------------------------------- 32 | 33 | Request: 34 | 35 | .. literalinclude:: ./http_snippets/snippets/example_one_api__create_computer_for_user 36 | :language: HTTP 37 | 38 | Response: 39 | 40 | .. literalinclude:: ./http_snippets/snippets/example_one_api__create_computer_for_user_result 41 | :language: HTTP 42 | 43 | 44 | Get user 45 | -------- 46 | 47 | Request: 48 | 49 | .. literalinclude:: ./http_snippets/snippets/example_one_api__get_user 50 | :language: HTTP 51 | 52 | Response: 53 | 54 | .. literalinclude:: ./http_snippets/snippets/example_one_api__get_user_result 55 | :language: HTTP 56 | 57 | 58 | Get user with related computers 59 | ------------------------------- 60 | 61 | Request: 62 | 63 | .. literalinclude:: ./http_snippets/snippets/example_one_api__get_user_with_computers 64 | :language: HTTP 65 | 66 | Response: 67 | 68 | .. literalinclude:: ./http_snippets/snippets/example_one_api__get_user_with_computers_result 69 | :language: HTTP 70 | 71 | 72 | Get users 73 | --------- 74 | 75 | Request: 76 | 77 | .. literalinclude:: ./http_snippets/snippets/example_one_api__get_users 78 | :language: HTTP 79 | 80 | Response: 81 | 82 | .. literalinclude:: ./http_snippets/snippets/example_one_api__get_users_result 83 | :language: HTTP 84 | 85 | 86 | Get users with related computers 87 | -------------------------------- 88 | 89 | Request: 90 | 91 | .. literalinclude:: ./http_snippets/snippets/example_one_api__get_users_with_computers 92 | :language: HTTP 93 | 94 | Response: 95 | 96 | .. literalinclude:: ./http_snippets/snippets/example_one_api__get_users_with_computers_result 97 | :language: HTTP 98 | -------------------------------------------------------------------------------- /examples/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mts-ai/FastAPI-JSONAPI/a1a4fc5a868ab6bb563919c462ea248250e83b26/examples/__init__.py -------------------------------------------------------------------------------- /examples/api_for_sqlalchemy/README.md: -------------------------------------------------------------------------------- 1 | ## App API-FOR-SQLALCHEMY-ORM 2 | 3 | ### Start app 4 | ```shell 5 | # in dir fastapi-jsonapi 6 | 7 | cd examples 8 | python api_minimal.py 9 | ``` 10 | http://0.0.0.0:8080/docs 11 | -------------------------------------------------------------------------------- /examples/api_for_sqlalchemy/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mts-ai/FastAPI-JSONAPI/a1a4fc5a868ab6bb563919c462ea248250e83b26/examples/api_for_sqlalchemy/__init__.py -------------------------------------------------------------------------------- /examples/api_for_sqlalchemy/api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mts-ai/FastAPI-JSONAPI/a1a4fc5a868ab6bb563919c462ea248250e83b26/examples/api_for_sqlalchemy/api/__init__.py -------------------------------------------------------------------------------- /examples/api_for_sqlalchemy/api/views_base.py: -------------------------------------------------------------------------------- 1 | from typing import ClassVar 2 | 3 | from fastapi import Depends 4 | from pydantic import BaseModel, ConfigDict 5 | from sqlalchemy.engine import make_url 6 | from sqlalchemy.ext.asyncio import AsyncSession 7 | 8 | from examples.api_for_sqlalchemy import config 9 | from examples.api_for_sqlalchemy.models.db import DB 10 | from fastapi_jsonapi.data_layers.sqla.orm import SqlalchemyDataLayer 11 | from fastapi_jsonapi.misc.sqla.generics.base import ViewBaseGeneric 12 | from fastapi_jsonapi.views import Operation, OperationConfig, ViewBase 13 | 14 | db = DB( 15 | url=make_url(config.SQLA_URI), 16 | ) 17 | 18 | 19 | class SessionDependency(BaseModel): 20 | model_config = ConfigDict( 21 | arbitrary_types_allowed=True, 22 | ) 23 | 24 | session: AsyncSession = Depends(db.session) 25 | 26 | 27 | def handler(view: ViewBase, dto: SessionDependency) -> dict: 28 | return { 29 | "session": dto.session, 30 | } 31 | 32 | 33 | class ViewBase(ViewBaseGeneric): 34 | """ 35 | Generic view base (detail) 36 | """ 37 | 38 | data_layer_cls = SqlalchemyDataLayer 39 | 40 | operation_dependencies: ClassVar = { 41 | Operation.ALL: OperationConfig( 42 | dependencies=SessionDependency, 43 | prepare_data_layer_kwargs=handler, 44 | ), 45 | } 46 | -------------------------------------------------------------------------------- /examples/api_for_sqlalchemy/config.py: -------------------------------------------------------------------------------- 1 | from os import getenv 2 | 3 | SQLA_URI = getenv("SQLA_URI", "sqlite+aiosqlite:///./db.sqlite3") 4 | SQLA_ECHO = getenv("SQLA_ECHO", "0") == "1" 5 | -------------------------------------------------------------------------------- /examples/api_for_sqlalchemy/enums/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mts-ai/FastAPI-JSONAPI/a1a4fc5a868ab6bb563919c462ea248250e83b26/examples/api_for_sqlalchemy/enums/__init__.py -------------------------------------------------------------------------------- /examples/api_for_sqlalchemy/enums/enums.py: -------------------------------------------------------------------------------- 1 | from enum import Enum as EnumOriginal 2 | from typing import Type, TypeVar, Union 3 | 4 | from sqlalchemy import types 5 | from sqlalchemy.engine import Dialect 6 | 7 | from fastapi_jsonapi.data_layers.fields.mixins import MixinEnum 8 | 9 | TypeEnum = TypeVar("TypeEnum", bound=MixinEnum) 10 | 11 | 12 | class EnumColumn(types.TypeDecorator): 13 | impl = types.Text 14 | cache_ok = True 15 | 16 | def __init__(self, enum: Union[Type[EnumOriginal], Type[TypeEnum]], *args: list, **kwargs: dict): 17 | if not issubclass(enum, EnumOriginal): 18 | msg = f"{enum} is not a subtype of Enum" 19 | raise TypeError(msg) 20 | self.enum = enum 21 | super().__init__(*args, **kwargs) 22 | 23 | def process_bind_param(self, value: Union[Type[EnumOriginal], Type[TypeEnum]], dialect: Dialect): 24 | if isinstance(value, EnumOriginal) and isinstance(value.value, (str, int)): 25 | return value.value 26 | if isinstance(value, str): 27 | return self.enum[value].value 28 | return value 29 | 30 | def process_result_value(self, value: Union[str, int], dialect: Dialect): 31 | return self.enum.value_to_enum(value) 32 | -------------------------------------------------------------------------------- /examples/api_for_sqlalchemy/enums/user.py: -------------------------------------------------------------------------------- 1 | from fastapi_jsonapi.data_layers.fields.enums import Enum 2 | 3 | 4 | class UserStatusEnum(str, Enum): 5 | """ 6 | Status user. 7 | """ 8 | 9 | active = "active" 10 | archive = "archive" 11 | block = "block" 12 | -------------------------------------------------------------------------------- /examples/api_for_sqlalchemy/main.py: -------------------------------------------------------------------------------- 1 | """ 2 | Main module for w_mount service. 3 | 4 | In module placed db initialization functions, app factory. 5 | """ 6 | 7 | import sys 8 | from contextlib import asynccontextmanager 9 | from pathlib import Path 10 | 11 | import uvicorn 12 | from fastapi import FastAPI 13 | from fastapi.responses import ORJSONResponse as JSONResponse 14 | 15 | from examples.api_for_sqlalchemy.api.views_base import db 16 | from examples.api_for_sqlalchemy.models.base import Base 17 | from examples.api_for_sqlalchemy.urls import add_routes 18 | 19 | CURRENT_DIR = Path(__file__).resolve().parent 20 | sys.path.append(f"{CURRENT_DIR.parent.parent}") 21 | 22 | 23 | # noinspection PyUnusedLocal 24 | @asynccontextmanager 25 | async def lifespan(app: FastAPI): 26 | app.config = {"MAX_INCLUDE_DEPTH": 5} 27 | add_routes(app) 28 | 29 | async with db.engine.begin() as conn: 30 | await conn.run_sync(Base.metadata.create_all) 31 | 32 | yield 33 | 34 | await db.engine.dispose() 35 | 36 | 37 | app = FastAPI( 38 | title="FastAPI and SQLAlchemy", 39 | lifespan=lifespan, 40 | debug=True, 41 | default_response_class=JSONResponse, 42 | docs_url="/docs", 43 | openapi_url="/openapi.json", 44 | ) 45 | 46 | 47 | if __name__ == "__main__": 48 | uvicorn.run( 49 | "main:app", 50 | host="0.0.0.0", 51 | port=8082, 52 | reload=True, 53 | app_dir=f"{CURRENT_DIR}", 54 | ) 55 | -------------------------------------------------------------------------------- /examples/api_for_sqlalchemy/models/__init__.py: -------------------------------------------------------------------------------- 1 | from examples.api_for_sqlalchemy.models.child import Child 2 | from examples.api_for_sqlalchemy.models.computer import Computer 3 | from examples.api_for_sqlalchemy.models.parent import Parent 4 | from examples.api_for_sqlalchemy.models.parent_to_child_association import ParentToChildAssociation 5 | from examples.api_for_sqlalchemy.models.post import Post 6 | from examples.api_for_sqlalchemy.models.post_comment import PostComment 7 | from examples.api_for_sqlalchemy.models.user import User 8 | from examples.api_for_sqlalchemy.models.user_bio import UserBio 9 | from examples.api_for_sqlalchemy.models.workplace import Workplace 10 | 11 | __all__ = ( 12 | "Child", 13 | "Computer", 14 | "Parent", 15 | "ParentToChildAssociation", 16 | "Post", 17 | "PostComment", 18 | "User", 19 | "UserBio", 20 | "Workplace", 21 | ) 22 | -------------------------------------------------------------------------------- /examples/api_for_sqlalchemy/models/base.py: -------------------------------------------------------------------------------- 1 | from typing import Any, ClassVar 2 | 3 | from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column 4 | 5 | 6 | class Base(DeclarativeBase): 7 | __table_args__: ClassVar[dict[str, Any]] = { 8 | "extend_existing": True, 9 | } 10 | 11 | id: Mapped[int] = mapped_column(primary_key=True) 12 | -------------------------------------------------------------------------------- /examples/api_for_sqlalchemy/models/child.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.orm import Mapped, relationship 2 | 3 | from .base import Base 4 | from .parent_to_child_association import ParentToChildAssociation 5 | 6 | 7 | class Child(Base): 8 | __tablename__ = "right_table_children" 9 | 10 | name: Mapped[str] 11 | 12 | parents: Mapped[list[ParentToChildAssociation]] = relationship(back_populates="child", cascade="delete") 13 | -------------------------------------------------------------------------------- /examples/api_for_sqlalchemy/models/computer.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import TYPE_CHECKING, Optional 4 | 5 | from sqlalchemy import ForeignKey 6 | from sqlalchemy.orm import Mapped, mapped_column, relationship 7 | 8 | from .base import Base 9 | 10 | if TYPE_CHECKING: 11 | from .user import User 12 | 13 | 14 | class Computer(Base): 15 | __tablename__ = "computers" 16 | 17 | name: Mapped[str] 18 | 19 | user_id: Mapped[Optional[int]] = mapped_column(ForeignKey("users.id")) 20 | user: Mapped[User] = relationship(back_populates="computers") 21 | -------------------------------------------------------------------------------- /examples/api_for_sqlalchemy/models/db.py: -------------------------------------------------------------------------------- 1 | from collections.abc import AsyncIterator 2 | from typing import Union 3 | 4 | from sqlalchemy.engine import URL 5 | from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, async_sessionmaker, create_async_engine 6 | 7 | 8 | class DB: 9 | def __init__( 10 | self, 11 | url: Union[str, URL], 12 | echo: bool = False, 13 | echo_pool: bool = False, 14 | ): 15 | self.engine: AsyncEngine = create_async_engine( 16 | url=url, 17 | echo=echo, 18 | echo_pool=echo_pool, 19 | ) 20 | 21 | self.session_maker: async_sessionmaker[AsyncSession] = async_sessionmaker( 22 | autocommit=False, 23 | bind=self.engine, 24 | expire_on_commit=False, 25 | ) 26 | 27 | async def dispose(self): 28 | await self.engine.dispose() 29 | 30 | async def session(self) -> AsyncIterator[AsyncSession]: 31 | async with self.session_maker() as session: 32 | yield session 33 | -------------------------------------------------------------------------------- /examples/api_for_sqlalchemy/models/parent.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.orm import Mapped, relationship 2 | 3 | from .base import Base 4 | from .parent_to_child_association import ParentToChildAssociation 5 | 6 | 7 | class Parent(Base): 8 | __tablename__ = "left_table_parents" 9 | 10 | name: Mapped[str] 11 | 12 | children: Mapped[list[ParentToChildAssociation]] = relationship(back_populates="parent", cascade="delete") 13 | -------------------------------------------------------------------------------- /examples/api_for_sqlalchemy/models/parent_to_child_association.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import TYPE_CHECKING 4 | 5 | from sqlalchemy import ForeignKey, Index, String 6 | from sqlalchemy.orm import Mapped, mapped_column, relationship 7 | 8 | from .base import Base 9 | 10 | if TYPE_CHECKING: 11 | from .child import Child 12 | from .parent import Parent 13 | 14 | 15 | class ParentToChildAssociation(Base): 16 | __table_args__ = ( 17 | # JSON:API requires `id` field on any model, 18 | # so we can't create a composite PK here 19 | # that's why we need to create this index 20 | Index( 21 | "ix_parent_child_association_unique", 22 | "parent_left_id", 23 | "child_right_id", 24 | unique=True, 25 | ), 26 | ) 27 | 28 | __tablename__ = "parent_to_child_association_table" 29 | 30 | extra_data: Mapped[str] = mapped_column(String(50)) 31 | 32 | child_right_id: Mapped[int] = mapped_column(ForeignKey("right_table_children.id")) 33 | child: Mapped[Child] = relationship(back_populates="parents") 34 | parent_left_id: Mapped[int] = mapped_column(ForeignKey("left_table_parents.id")) 35 | parent: Mapped[Parent] = relationship(back_populates="children") 36 | -------------------------------------------------------------------------------- /examples/api_for_sqlalchemy/models/post.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import TYPE_CHECKING 4 | 5 | from sqlalchemy import ForeignKey 6 | from sqlalchemy.orm import Mapped, mapped_column, relationship 7 | 8 | from .base import Base 9 | from .post_comment import PostComment 10 | 11 | if TYPE_CHECKING: 12 | from .user import User 13 | 14 | 15 | class Post(Base): 16 | __tablename__ = "posts" 17 | 18 | body: Mapped[str] = mapped_column(default="", server_default="") 19 | title: Mapped[str] 20 | 21 | comments: Mapped[list[PostComment]] = relationship(back_populates="post", cascade="delete") 22 | user_id: Mapped[int] = mapped_column(ForeignKey("users.id")) 23 | user: Mapped[User] = relationship(back_populates="posts") 24 | -------------------------------------------------------------------------------- /examples/api_for_sqlalchemy/models/post_comment.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import TYPE_CHECKING 4 | 5 | from sqlalchemy import ForeignKey 6 | from sqlalchemy.orm import Mapped, mapped_column, relationship 7 | 8 | from .base import Base 9 | 10 | if TYPE_CHECKING: 11 | from .post import Post 12 | from .user import User 13 | 14 | 15 | class PostComment(Base): 16 | __tablename__ = "post_comments" 17 | 18 | text: Mapped[str] = mapped_column(default="", server_default="") 19 | 20 | user_id: Mapped[int] = mapped_column(ForeignKey("users.id"), unique=False) 21 | user: Mapped[User] = relationship(back_populates="comments") 22 | post_id: Mapped[int] = mapped_column(ForeignKey("posts.id"), unique=False) 23 | post: Mapped[Post] = relationship(back_populates="comments") 24 | -------------------------------------------------------------------------------- /examples/api_for_sqlalchemy/models/user.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from sqlalchemy.orm import Mapped, mapped_column, relationship 4 | 5 | from examples.api_for_sqlalchemy.enums.enums import EnumColumn 6 | from examples.api_for_sqlalchemy.enums.user import UserStatusEnum 7 | 8 | from .base import Base 9 | from .computer import Computer 10 | from .post import Post 11 | from .post_comment import PostComment 12 | from .user_bio import UserBio 13 | from .workplace import Workplace 14 | 15 | 16 | class User(Base): 17 | __tablename__ = "users" 18 | 19 | age: Mapped[Optional[int]] 20 | email: Mapped[Optional[str]] 21 | name: Mapped[Optional[str]] = mapped_column(unique=True) 22 | status: Mapped[UserStatusEnum] = mapped_column( 23 | EnumColumn(UserStatusEnum), 24 | default=UserStatusEnum.active, 25 | ) 26 | 27 | bio: Mapped[UserBio] = relationship(back_populates="user", cascade="delete") 28 | comments: Mapped[list[PostComment]] = relationship(back_populates="user", cascade="delete") 29 | computers: Mapped[list[Computer]] = relationship(back_populates="user") 30 | posts: Mapped[list[Post]] = relationship(back_populates="user", cascade="delete") 31 | workplace: Mapped[Workplace] = relationship(back_populates="user") 32 | 33 | class Enum: 34 | Status = UserStatusEnum 35 | -------------------------------------------------------------------------------- /examples/api_for_sqlalchemy/models/user_bio.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import TYPE_CHECKING 4 | 5 | from sqlalchemy import ForeignKey 6 | from sqlalchemy.orm import Mapped, mapped_column, relationship 7 | 8 | from .base import Base 9 | 10 | if TYPE_CHECKING: 11 | from .user import User 12 | 13 | 14 | class UserBio(Base): 15 | __tablename__ = "user_bio" 16 | 17 | birth_city: Mapped[str] = mapped_column(default="", server_default="") 18 | favourite_movies: Mapped[str] = mapped_column(default="", server_default="") 19 | 20 | user_id: Mapped[int] = mapped_column(ForeignKey("users.id"), unique=True) 21 | user: Mapped[User] = relationship(back_populates="bio") 22 | -------------------------------------------------------------------------------- /examples/api_for_sqlalchemy/models/workplace.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import TYPE_CHECKING, Optional 4 | 5 | from sqlalchemy import ForeignKey 6 | from sqlalchemy.orm import Mapped, mapped_column, relationship 7 | 8 | from .base import Base 9 | 10 | if TYPE_CHECKING: 11 | from .user import User 12 | 13 | 14 | class Workplace(Base): 15 | __tablename__ = "workplaces" 16 | 17 | name: Mapped[str] 18 | 19 | user_id: Mapped[Optional[int]] = mapped_column(ForeignKey("users.id")) 20 | user: Mapped[User] = relationship(back_populates="workplace") 21 | -------------------------------------------------------------------------------- /examples/api_for_sqlalchemy/schemas/__init__.py: -------------------------------------------------------------------------------- 1 | from .child import ( 2 | ChildAttributesSchema, 3 | ChildInSchema, 4 | ChildPatchSchema, 5 | ChildSchema, 6 | ) 7 | from .computer import ( 8 | ComputerAttributesBaseSchema, 9 | ComputerInSchema, 10 | ComputerPatchSchema, 11 | ComputerSchema, 12 | ) 13 | from .parent import ( 14 | ParentAttributesSchema, 15 | ParentInSchema, 16 | ParentPatchSchema, 17 | ParentSchema, 18 | ) 19 | from .parent_to_child_association import ( 20 | ParentToChildAssociationAttributesSchema, 21 | ParentToChildAssociationSchema, 22 | ) 23 | from .post import ( 24 | PostAttributesBaseSchema, 25 | PostInSchema, 26 | PostPatchSchema, 27 | PostSchema, 28 | ) 29 | from .post_comment import ( 30 | PostCommentAttributesBaseSchema, 31 | PostCommentSchema, 32 | ) 33 | from .user import ( 34 | CustomUserAttributesSchema, 35 | UserAttributesBaseSchema, 36 | UserInSchema, 37 | UserInSchemaAllowIdOnPost, 38 | UserPatchSchema, 39 | UserSchema, 40 | ) 41 | from .user_bio import ( 42 | UserBioAttributesBaseSchema, 43 | UserBioBaseSchema, 44 | UserBioInSchema, 45 | UserBioPatchSchema, 46 | ) 47 | from .workplace import ( 48 | WorkplaceInSchema, 49 | WorkplacePatchSchema, 50 | WorkplaceSchema, 51 | ) 52 | 53 | __all__ = ( 54 | "ChildAttributesSchema", 55 | "ChildInSchema", 56 | "ChildPatchSchema", 57 | "ChildSchema", 58 | "ComputerAttributesBaseSchema", 59 | "ComputerInSchema", 60 | "ComputerPatchSchema", 61 | "ComputerSchema", 62 | "CustomUserAttributesSchema", 63 | "ParentAttributesSchema", 64 | "ParentInSchema", 65 | "ParentPatchSchema", 66 | "ParentSchema", 67 | "ParentToChildAssociationAttributesSchema", 68 | "ParentToChildAssociationSchema", 69 | "PostAttributesBaseSchema", 70 | "PostCommentAttributesBaseSchema", 71 | "PostCommentSchema", 72 | "PostInSchema", 73 | "PostPatchSchema", 74 | "PostSchema", 75 | "UserAttributesBaseSchema", 76 | "UserBioAttributesBaseSchema", 77 | "UserBioBaseSchema", 78 | "UserBioInSchema", 79 | "UserBioPatchSchema", 80 | "UserInSchema", 81 | "UserInSchemaAllowIdOnPost", 82 | "UserPatchSchema", 83 | "UserSchema", 84 | "WorkplaceInSchema", 85 | "WorkplacePatchSchema", 86 | "WorkplaceSchema", 87 | ) 88 | -------------------------------------------------------------------------------- /examples/api_for_sqlalchemy/schemas/child.py: -------------------------------------------------------------------------------- 1 | from typing import Annotated, Optional 2 | 3 | from pydantic import ConfigDict 4 | 5 | from fastapi_jsonapi.schema_base import BaseModel 6 | from fastapi_jsonapi.types_metadata import RelationshipInfo 7 | 8 | from .parent_to_child_association import ParentToChildAssociationSchema 9 | 10 | 11 | class ChildAttributesSchema(BaseModel): 12 | model_config = ConfigDict( 13 | from_attributes=True, 14 | ) 15 | 16 | name: str 17 | 18 | 19 | class ChildBaseSchema(ChildAttributesSchema): 20 | """Child base schema.""" 21 | 22 | parents: Annotated[ 23 | Optional[list[ParentToChildAssociationSchema]], 24 | RelationshipInfo( 25 | resource_type="parent_child_association", 26 | many=True, 27 | ), 28 | ] = None 29 | 30 | 31 | class ChildPatchSchema(ChildBaseSchema): 32 | """Child PATCH schema.""" 33 | 34 | 35 | class ChildInSchema(ChildBaseSchema): 36 | """Child input schema.""" 37 | 38 | 39 | class ChildSchema(ChildInSchema): 40 | """Child item schema.""" 41 | 42 | id: int 43 | -------------------------------------------------------------------------------- /examples/api_for_sqlalchemy/schemas/computer.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import TYPE_CHECKING, Annotated, Optional 4 | 5 | from pydantic import ConfigDict 6 | 7 | from fastapi_jsonapi.schema_base import BaseModel 8 | from fastapi_jsonapi.types_metadata import RelationshipInfo 9 | 10 | if TYPE_CHECKING: 11 | from .user import UserSchema 12 | 13 | 14 | class ComputerAttributesBaseSchema(BaseModel): 15 | model_config = ConfigDict( 16 | from_attributes=True, 17 | ) 18 | 19 | name: str 20 | 21 | 22 | class ComputerBaseSchema(ComputerAttributesBaseSchema): 23 | """Computer base schema.""" 24 | 25 | user: Annotated[ 26 | Optional[UserSchema], 27 | RelationshipInfo( 28 | resource_type="user", 29 | ), 30 | ] = None 31 | 32 | 33 | class ComputerPatchSchema(ComputerBaseSchema): 34 | """Computer PATCH schema.""" 35 | 36 | 37 | class ComputerInSchema(ComputerBaseSchema): 38 | """Computer input schema.""" 39 | 40 | 41 | class ComputerSchema(ComputerInSchema): 42 | """Computer item schema.""" 43 | 44 | id: int 45 | -------------------------------------------------------------------------------- /examples/api_for_sqlalchemy/schemas/parent.py: -------------------------------------------------------------------------------- 1 | from typing import Annotated, Optional 2 | 3 | from pydantic import ConfigDict 4 | 5 | from fastapi_jsonapi.schema_base import BaseModel 6 | from fastapi_jsonapi.types_metadata import RelationshipInfo 7 | 8 | from .parent_to_child_association import ParentToChildAssociationSchema 9 | 10 | 11 | class ParentAttributesSchema(BaseModel): 12 | model_config = ConfigDict( 13 | from_attributes=True, 14 | ) 15 | 16 | name: str 17 | 18 | 19 | class ParentBaseSchema(ParentAttributesSchema): 20 | """Parent base schema.""" 21 | 22 | children: Annotated[ 23 | Optional[list[ParentToChildAssociationSchema]], 24 | RelationshipInfo( 25 | resource_type="parent_child_association", 26 | many=True, 27 | ), 28 | ] = None 29 | 30 | 31 | class ParentPatchSchema(ParentBaseSchema): 32 | """Parent PATCH schema.""" 33 | 34 | 35 | class ParentInSchema(ParentBaseSchema): 36 | """Parent input schema.""" 37 | 38 | 39 | class ParentSchema(ParentInSchema): 40 | """Parent item schema.""" 41 | 42 | id: int 43 | -------------------------------------------------------------------------------- /examples/api_for_sqlalchemy/schemas/parent_to_child_association.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import TYPE_CHECKING, Annotated, Optional 4 | 5 | from pydantic import ConfigDict 6 | 7 | from fastapi_jsonapi.schema_base import BaseModel 8 | from fastapi_jsonapi.types_metadata import RelationshipInfo 9 | 10 | if TYPE_CHECKING: 11 | from .child import ChildSchema 12 | from .parent import ParentSchema 13 | 14 | 15 | class ParentToChildAssociationAttributesSchema(BaseModel): 16 | model_config = ConfigDict( 17 | from_attributes=True, 18 | ) 19 | 20 | extra_data: str 21 | 22 | 23 | class ParentToChildAssociationSchema(ParentToChildAssociationAttributesSchema): 24 | parent: Annotated[ 25 | Optional[ParentSchema], 26 | RelationshipInfo( 27 | resource_type="parent", 28 | ), 29 | ] = None 30 | child: Annotated[ 31 | Optional[ChildSchema], 32 | RelationshipInfo( 33 | resource_type="child", 34 | ), 35 | ] = None 36 | -------------------------------------------------------------------------------- /examples/api_for_sqlalchemy/schemas/post.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import TYPE_CHECKING, Annotated, Optional 4 | 5 | from pydantic import ConfigDict 6 | 7 | from fastapi_jsonapi.schema_base import BaseModel 8 | from fastapi_jsonapi.types_metadata import RelationshipInfo 9 | 10 | from .post_comment import PostCommentSchema 11 | 12 | if TYPE_CHECKING: 13 | from .user import UserSchema 14 | 15 | 16 | class PostAttributesBaseSchema(BaseModel): 17 | model_config = ConfigDict( 18 | from_attributes=True, 19 | ) 20 | 21 | body: str 22 | title: str 23 | 24 | 25 | class PostBaseSchema(PostAttributesBaseSchema): 26 | """Post base schema.""" 27 | 28 | user: Annotated[ 29 | Optional[UserSchema], 30 | RelationshipInfo( 31 | resource_type="user", 32 | ), 33 | ] = None 34 | comments: Annotated[ 35 | Optional[list[PostCommentSchema]], 36 | RelationshipInfo( 37 | resource_type="post_comment", 38 | many=True, 39 | ), 40 | ] = None 41 | 42 | 43 | class PostPatchSchema(PostBaseSchema): 44 | """Post PATCH schema.""" 45 | 46 | 47 | class PostInSchema(PostBaseSchema): 48 | """Post input schema.""" 49 | 50 | 51 | class PostSchema(PostInSchema): 52 | """Post item schema.""" 53 | 54 | id: int 55 | -------------------------------------------------------------------------------- /examples/api_for_sqlalchemy/schemas/post_comment.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import TYPE_CHECKING, Annotated 4 | 5 | from pydantic import ConfigDict 6 | 7 | from fastapi_jsonapi.schema_base import BaseModel 8 | from fastapi_jsonapi.types_metadata import RelationshipInfo 9 | 10 | if TYPE_CHECKING: 11 | from .post import PostSchema 12 | from .user import UserSchema 13 | 14 | 15 | class PostCommentAttributesBaseSchema(BaseModel): 16 | model_config = ConfigDict( 17 | from_attributes=True, 18 | ) 19 | 20 | text: str 21 | 22 | 23 | class PostCommentBaseSchema(PostCommentAttributesBaseSchema): 24 | """PostComment base schema.""" 25 | 26 | post: Annotated[ 27 | PostSchema, 28 | RelationshipInfo( 29 | resource_type="post", 30 | ), 31 | ] 32 | user: Annotated[ 33 | UserSchema, 34 | RelationshipInfo( 35 | resource_type="user", 36 | ), 37 | ] 38 | 39 | 40 | class PostCommentSchema(PostCommentBaseSchema): 41 | """PostComment item schema.""" 42 | 43 | id: int 44 | -------------------------------------------------------------------------------- /examples/api_for_sqlalchemy/schemas/user.py: -------------------------------------------------------------------------------- 1 | from typing import Annotated, Optional 2 | 3 | from pydantic import ConfigDict 4 | 5 | from fastapi_jsonapi.schema_base import BaseModel 6 | from fastapi_jsonapi.types_metadata import ClientCanSetId, RelationshipInfo 7 | 8 | from .computer import ComputerSchema 9 | from .post import PostSchema 10 | from .post_comment import PostCommentSchema 11 | from .user_bio import UserBioBaseSchema 12 | from .workplace import WorkplaceSchema 13 | 14 | 15 | class UserAttributesBaseSchema(BaseModel): 16 | model_config = ConfigDict( 17 | from_attributes=True, 18 | ) 19 | 20 | name: str 21 | 22 | age: Optional[int] = None 23 | email: Optional[str] = None 24 | 25 | 26 | class UserBaseSchema(UserAttributesBaseSchema): 27 | """User base schema.""" 28 | 29 | bio: Annotated[ 30 | Optional[UserBioBaseSchema], 31 | RelationshipInfo( 32 | resource_type="user_bio", 33 | ), 34 | ] = None 35 | comments: Annotated[ 36 | Optional[list[PostCommentSchema]], 37 | RelationshipInfo( 38 | resource_type="post_comment", 39 | many=True, 40 | ), 41 | ] = None 42 | computers: Annotated[ 43 | Optional[list[ComputerSchema]], 44 | RelationshipInfo( 45 | resource_type="computer", 46 | many=True, 47 | ), 48 | ] = None 49 | posts: Annotated[ 50 | Optional[list[PostSchema]], 51 | RelationshipInfo( 52 | resource_type="post", 53 | many=True, 54 | ), 55 | ] = None 56 | workplace: Annotated[ 57 | Optional[WorkplaceSchema], 58 | RelationshipInfo( 59 | resource_type="workplace", 60 | ), 61 | ] = None 62 | 63 | 64 | class UserPatchSchema(UserBaseSchema): 65 | """User PATCH schema.""" 66 | 67 | 68 | class UserInSchema(UserBaseSchema): 69 | """User input schema.""" 70 | 71 | 72 | class UserInSchemaAllowIdOnPost(UserBaseSchema): 73 | id: Annotated[str, ClientCanSetId()] 74 | 75 | 76 | class UserSchema(UserInSchema): 77 | """User item schema.""" 78 | 79 | id: int 80 | 81 | 82 | class CustomUserAttributesSchema(UserBaseSchema): 83 | spam: str 84 | eggs: str 85 | -------------------------------------------------------------------------------- /examples/api_for_sqlalchemy/schemas/user_bio.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import TYPE_CHECKING, Annotated, Optional 4 | 5 | from pydantic import ConfigDict 6 | 7 | from fastapi_jsonapi.schema_base import BaseModel 8 | from fastapi_jsonapi.types_metadata import RelationshipInfo 9 | 10 | if TYPE_CHECKING: 11 | from .user import UserSchema 12 | 13 | 14 | class UserBioAttributesBaseSchema(BaseModel): 15 | """UserBio base schema.""" 16 | 17 | model_config = ConfigDict( 18 | from_attributes=True, 19 | ) 20 | 21 | birth_city: str 22 | favourite_movies: str 23 | 24 | 25 | class UserBioBaseSchema(UserBioAttributesBaseSchema): 26 | """UserBio item schema.""" 27 | 28 | user: Annotated[ 29 | Optional[UserSchema], 30 | RelationshipInfo( 31 | resource_type="user", 32 | ), 33 | ] = None 34 | 35 | 36 | class UserBioPatchSchema(UserBioBaseSchema): 37 | """UserBio PATCH schema.""" 38 | 39 | 40 | class UserBioInSchema(UserBioBaseSchema): 41 | """UserBio input schema.""" 42 | 43 | 44 | class UserBioSchema(UserBioInSchema): 45 | """UserBio item schema.""" 46 | 47 | id: int 48 | -------------------------------------------------------------------------------- /examples/api_for_sqlalchemy/schemas/workplace.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import TYPE_CHECKING, Annotated, Optional 4 | 5 | from pydantic import ConfigDict 6 | 7 | from fastapi_jsonapi.schema_base import BaseModel 8 | from fastapi_jsonapi.types_metadata import RelationshipInfo 9 | 10 | if TYPE_CHECKING: 11 | from .user import UserSchema 12 | 13 | 14 | class WorkplaceBaseSchema(BaseModel): 15 | """Workplace base schema.""" 16 | 17 | model_config = ConfigDict( 18 | from_attributes=True, 19 | ) 20 | 21 | name: str 22 | 23 | user: Annotated[ 24 | Optional[UserSchema], 25 | RelationshipInfo( 26 | resource_type="user", 27 | ), 28 | ] = None 29 | 30 | 31 | class WorkplacePatchSchema(WorkplaceBaseSchema): 32 | """Workplace PATCH schema.""" 33 | 34 | 35 | class WorkplaceInSchema(WorkplaceBaseSchema): 36 | """Workplace input schema.""" 37 | 38 | 39 | class WorkplaceSchema(WorkplaceInSchema): 40 | """Workplace item schema.""" 41 | 42 | id: int 43 | -------------------------------------------------------------------------------- /examples/api_minimal.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from contextlib import asynccontextmanager 3 | from pathlib import Path 4 | from typing import Any, ClassVar, Optional 5 | 6 | import uvicorn 7 | from fastapi import Depends, FastAPI 8 | from fastapi.responses import ORJSONResponse as JSONResponse 9 | from pydantic import ConfigDict 10 | from sqlalchemy.engine import make_url 11 | from sqlalchemy.ext.asyncio import AsyncSession 12 | from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column 13 | 14 | from examples.api_for_sqlalchemy.models.db import DB 15 | from fastapi_jsonapi import ApplicationBuilder 16 | from fastapi_jsonapi.misc.sqla.generics.base import ViewBaseGeneric 17 | from fastapi_jsonapi.schema_base import BaseModel 18 | from fastapi_jsonapi.views import Operation, OperationConfig, ViewBase 19 | 20 | CURRENT_DIR = Path(__file__).resolve().parent 21 | sys.path.append(f"{CURRENT_DIR.parent.parent}") 22 | db = DB( 23 | url=make_url(f"sqlite+aiosqlite:///{CURRENT_DIR}/db.sqlite3"), 24 | ) 25 | 26 | 27 | class Base(DeclarativeBase): 28 | pass 29 | 30 | 31 | class User(Base): 32 | __tablename__ = "users" 33 | 34 | id: Mapped[int] = mapped_column(primary_key=True) 35 | name: Mapped[Optional[str]] 36 | 37 | 38 | class UserSchema(BaseModel): 39 | """User base schema.""" 40 | 41 | model_config = ConfigDict( 42 | from_attributes=True, 43 | ) 44 | 45 | name: str 46 | 47 | 48 | class SessionDependency(BaseModel): 49 | model_config = ConfigDict( 50 | arbitrary_types_allowed=True, 51 | ) 52 | 53 | session: AsyncSession = Depends(db.session) 54 | 55 | 56 | def session_dependency_handler(view: ViewBase, dto: SessionDependency) -> dict[str, Any]: 57 | return { 58 | "session": dto.session, 59 | } 60 | 61 | 62 | class UserView(ViewBaseGeneric): 63 | operation_dependencies: ClassVar = { 64 | Operation.ALL: OperationConfig( 65 | dependencies=SessionDependency, 66 | prepare_data_layer_kwargs=session_dependency_handler, 67 | ), 68 | } 69 | 70 | 71 | def add_routes(app: FastAPI): 72 | builder = ApplicationBuilder(app) 73 | builder.add_resource( 74 | path="/users", 75 | tags=["User"], 76 | view=UserView, 77 | schema=UserSchema, 78 | model=User, 79 | resource_type="user", 80 | ) 81 | builder.initialize() 82 | 83 | 84 | # noinspection PyUnusedLocal 85 | @asynccontextmanager 86 | async def lifespan(app: FastAPI): 87 | add_routes(app) 88 | 89 | async with db.engine.begin() as conn: 90 | await conn.run_sync(Base.metadata.create_all) 91 | 92 | yield 93 | 94 | await db.dispose() 95 | 96 | 97 | app = FastAPI( 98 | title="FastAPI and SQLAlchemy", 99 | lifespan=lifespan, 100 | debug=True, 101 | default_response_class=JSONResponse, 102 | docs_url="/docs", 103 | openapi_url="/openapi.json", 104 | ) 105 | 106 | 107 | if __name__ == "__main__": 108 | uvicorn.run( 109 | app, 110 | host="0.0.0.0", 111 | port=8080, 112 | ) 113 | -------------------------------------------------------------------------------- /examples/misc/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mts-ai/FastAPI-JSONAPI/a1a4fc5a868ab6bb563919c462ea248250e83b26/examples/misc/__init__.py -------------------------------------------------------------------------------- /examples/misc/custom_filter_example.py: -------------------------------------------------------------------------------- 1 | from typing import Annotated, Optional 2 | 3 | import orjson as json 4 | from pydantic import BaseModel, Field 5 | from pydantic.fields import FieldInfo 6 | from sqlalchemy.orm import InstrumentedAttribute 7 | from sqlalchemy.sql.expression import BinaryExpression 8 | 9 | from fastapi_jsonapi.exceptions import InvalidFilters 10 | from fastapi_jsonapi.types_metadata.custom_filter_sql import CustomFilterSQLA 11 | 12 | 13 | def _get_sqlite_json_ilike_expression( 14 | model_column: InstrumentedAttribute, 15 | value: list, 16 | operator: str, 17 | ) -> BinaryExpression: 18 | try: 19 | target_field, regex = value 20 | except ValueError: 21 | msg = f'The "value" field has to be list of two values for op `{operator}`' 22 | raise InvalidFilters(msg) 23 | 24 | if isinstance(regex, (list, dict)): 25 | regex = json.dumps(regex).decode() 26 | elif isinstance(regex, bool): 27 | return model_column.op("->>")(target_field).is_(regex) 28 | else: 29 | regex = f"{regex}" 30 | 31 | return model_column.op("->>")(target_field).ilike(regex) 32 | 33 | 34 | class SQLiteJSONIlikeFilterSQL(CustomFilterSQLA): 35 | def get_expression( 36 | self, 37 | schema_field: FieldInfo, 38 | model_column: InstrumentedAttribute, 39 | value: list[str], 40 | operator: str, 41 | ) -> BinaryExpression: 42 | return _get_sqlite_json_ilike_expression(model_column, value, operator) 43 | 44 | 45 | sql_filter_sqlite_json_ilike = SQLiteJSONIlikeFilterSQL(op="sqlite_json_ilike") 46 | 47 | 48 | class PictureSchema(BaseModel): 49 | """ 50 | Now you can use `jsonb_contains` sql filter for this resource 51 | """ 52 | 53 | name: str 54 | meta: Annotated[Optional[dict], sql_filter_sqlite_json_ilike] = Field( 55 | default_factory=dict, 56 | description="Any additional info in JSON format.", 57 | example={"location": "Moscow", "spam": "eggs"}, 58 | ) 59 | -------------------------------------------------------------------------------- /fastapi_jsonapi/VERSION: -------------------------------------------------------------------------------- 1 | 3.0.0 2 | -------------------------------------------------------------------------------- /fastapi_jsonapi/__init__.py: -------------------------------------------------------------------------------- 1 | """JSON API utils package.""" 2 | 3 | from pathlib import Path 4 | 5 | from fastapi_jsonapi.exceptions import BadRequest 6 | from fastapi_jsonapi.exceptions.json_api import HTTPException 7 | from fastapi_jsonapi.querystring import QueryStringManager 8 | 9 | from fastapi_jsonapi.api.application_builder import ApplicationBuilder # isort: skip 10 | 11 | __version__ = Path(__file__).parent.joinpath("VERSION").read_text().strip() 12 | 13 | __all__ = [ 14 | "ApplicationBuilder", 15 | "BadRequest", 16 | "HTTPException", 17 | "QueryStringManager", 18 | ] 19 | -------------------------------------------------------------------------------- /fastapi_jsonapi/api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mts-ai/FastAPI-JSONAPI/a1a4fc5a868ab6bb563919c462ea248250e83b26/fastapi_jsonapi/api/__init__.py -------------------------------------------------------------------------------- /fastapi_jsonapi/api/schemas.py: -------------------------------------------------------------------------------- 1 | from typing import Iterable, Optional, Type, Union 2 | 3 | from pydantic import BaseModel 4 | 5 | from fastapi_jsonapi.data_typing import TypeModel, TypeSchema 6 | from fastapi_jsonapi.views import Operation, ViewBase 7 | 8 | 9 | class ResourceData(BaseModel): 10 | path: Union[str, list[str]] 11 | tags: list[str] 12 | view: Type[ViewBase] 13 | model: Type[TypeModel] 14 | source_schema: Type[TypeSchema] 15 | schema_in_post: Optional[Type[BaseModel]] 16 | schema_in_post_data: Type[BaseModel] 17 | schema_in_patch: Optional[Type[BaseModel]] 18 | schema_in_patch_data: Type[BaseModel] 19 | detail_response_schema: Type[BaseModel] 20 | list_response_schema: Type[BaseModel] 21 | pagination_default_size: Optional[int] = 25 22 | pagination_default_number: Optional[int] = 1 23 | pagination_default_offset: Optional[int] = None 24 | pagination_default_limit: Optional[int] = None 25 | operations: Iterable[Operation] = () 26 | ending_slash: bool = True 27 | -------------------------------------------------------------------------------- /fastapi_jsonapi/atomic/__init__.py: -------------------------------------------------------------------------------- 1 | from .atomic import AtomicOperations 2 | from .atomic_handler import current_atomic_operation 3 | 4 | __all__ = ( 5 | "AtomicOperations", 6 | "current_atomic_operation", 7 | ) 8 | -------------------------------------------------------------------------------- /fastapi_jsonapi/atomic/atomic.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, Type 2 | 3 | from fastapi import APIRouter, Request, Response, status 4 | 5 | from fastapi_jsonapi.atomic.atomic_handler import AtomicViewHandler 6 | from fastapi_jsonapi.atomic.schemas import AtomicOperationRequest, AtomicResultResponse 7 | 8 | 9 | class AtomicOperations: 10 | atomic_handler: Type[AtomicViewHandler] = AtomicViewHandler 11 | 12 | def __init__( 13 | self, 14 | url_path: str = "/operations", 15 | router: Optional[APIRouter] = None, 16 | ): 17 | self.router = router or APIRouter(tags=["Atomic Operations"]) 18 | self.url_path = url_path 19 | self._register_view() 20 | 21 | async def view_atomic( 22 | self, 23 | request: Request, 24 | operations_request: AtomicOperationRequest, 25 | ): 26 | atomic_handler = self.atomic_handler( 27 | request=request, 28 | operations_request=operations_request, 29 | ) 30 | result = await atomic_handler.handle() 31 | if result: 32 | return result 33 | return Response(status_code=status.HTTP_204_NO_CONTENT) 34 | 35 | def _register_view(self) -> None: 36 | self.router.add_api_route( 37 | path=self.url_path, 38 | endpoint=self.view_atomic, 39 | response_model=AtomicResultResponse, 40 | methods=["Post"], 41 | summary="Atomic operations", 42 | description="""[https://jsonapi.org/ext/atomic/](https://jsonapi.org/ext/atomic/)""", 43 | ) 44 | -------------------------------------------------------------------------------- /fastapi_jsonapi/common.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | # noinspection PyProtectedMember 4 | from pydantic.fields import FieldInfo 5 | 6 | from fastapi_jsonapi.types_metadata import ClientCanSetId, CustomFilterSQL, CustomSortSQL, RelationshipInfo 7 | from fastapi_jsonapi.utils.metadata_instance_search import MetadataInstanceSearch 8 | 9 | search_client_can_set_id = MetadataInstanceSearch[ClientCanSetId](ClientCanSetId) 10 | search_relationship_info = MetadataInstanceSearch[RelationshipInfo](RelationshipInfo) 11 | search_custom_filter_sql = MetadataInstanceSearch[CustomFilterSQL](CustomFilterSQL) 12 | search_custom_sort_sql = MetadataInstanceSearch[CustomSortSQL](CustomSortSQL) 13 | 14 | 15 | def get_relationship_info_from_field_metadata( 16 | field: FieldInfo, 17 | ) -> Optional[RelationshipInfo]: 18 | return search_relationship_info.first(field) 19 | -------------------------------------------------------------------------------- /fastapi_jsonapi/data_layers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mts-ai/FastAPI-JSONAPI/a1a4fc5a868ab6bb563919c462ea248250e83b26/fastapi_jsonapi/data_layers/__init__.py -------------------------------------------------------------------------------- /fastapi_jsonapi/data_layers/fields/__init__.py: -------------------------------------------------------------------------------- 1 | """Fields package.""" 2 | -------------------------------------------------------------------------------- /fastapi_jsonapi/data_layers/fields/enums.py: -------------------------------------------------------------------------------- 1 | """Base enum module.""" 2 | 3 | from fastapi_jsonapi.data_layers.fields.mixins import MixinEnum 4 | 5 | 6 | class Enum(MixinEnum): 7 | """ 8 | Base enum class. 9 | 10 | All used non-integer enumerations must inherit from this class. 11 | """ 12 | -------------------------------------------------------------------------------- /fastapi_jsonapi/data_layers/fields/mixins.py: -------------------------------------------------------------------------------- 1 | """Enum mixin module.""" 2 | 3 | from enum import Enum 4 | 5 | 6 | class MixinEnum(Enum): 7 | """Extension over enum class from standard library.""" 8 | 9 | @classmethod 10 | def names(cls): 11 | """Get all field names.""" 12 | return ",".join(field.name for field in cls) 13 | 14 | @classmethod 15 | def values(cls): 16 | """Get all values from Enum.""" 17 | return [value for _, value in cls._member_map_.items()] 18 | 19 | @classmethod 20 | def keys(cls): 21 | """Get all field keys from Enum.""" 22 | return [key for key, _ in cls._member_map_.items()] 23 | 24 | @classmethod 25 | def inverse(cls): 26 | """Return all inverted items sequence.""" 27 | return {value: key for key, value in cls._member_map_.items()} 28 | 29 | @classmethod 30 | def value_to_enum(cls, value): 31 | """Convert value to enum.""" 32 | val_to_enum = {value.value: value for _, value in cls._member_map_.items()} 33 | return val_to_enum.get(value) 34 | -------------------------------------------------------------------------------- /fastapi_jsonapi/data_layers/sqla/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mts-ai/FastAPI-JSONAPI/a1a4fc5a868ab6bb563919c462ea248250e83b26/fastapi_jsonapi/data_layers/sqla/__init__.py -------------------------------------------------------------------------------- /fastapi_jsonapi/data_typing.py: -------------------------------------------------------------------------------- 1 | from typing import TypeVar 2 | 3 | from pydantic import BaseModel 4 | 5 | TypeModel = TypeVar("TypeModel") 6 | TypeSchema = TypeVar("TypeSchema", bound=BaseModel) 7 | -------------------------------------------------------------------------------- /fastapi_jsonapi/exceptions/__init__.py: -------------------------------------------------------------------------------- 1 | """Exceptions utils package. Contains exception schemas.""" 2 | 3 | from .base import ( 4 | ExceptionResponseSchema, 5 | ExceptionSchema, 6 | ExceptionSourceSchema, 7 | ) 8 | from .json_api import ( 9 | BadRequest, 10 | Forbidden, 11 | HTTPException, 12 | InternalServerError, 13 | InvalidField, 14 | InvalidFilters, 15 | InvalidInclude, 16 | InvalidSort, 17 | InvalidType, 18 | ObjectNotFound, 19 | RelatedObjectNotFound, 20 | RelationNotFound, 21 | ) 22 | 23 | __all__ = [ 24 | "BadRequest", 25 | "ExceptionResponseSchema", 26 | "ExceptionSchema", 27 | "ExceptionSourceSchema", 28 | "Forbidden", 29 | "HTTPException", 30 | "InternalServerError", 31 | "InvalidField", 32 | "InvalidFilters", 33 | "InvalidInclude", 34 | "InvalidSort", 35 | "InvalidType", 36 | "ObjectNotFound", 37 | "RelatedObjectNotFound", 38 | "RelationNotFound", 39 | ] 40 | -------------------------------------------------------------------------------- /fastapi_jsonapi/exceptions/base.py: -------------------------------------------------------------------------------- 1 | """Collection of useful http error for the Api.""" 2 | 3 | from typing import Any, Optional 4 | 5 | from pydantic import Field 6 | from pydantic.main import BaseModel 7 | 8 | 9 | class ExceptionSourceSchema(BaseModel): 10 | """Source exception schema.""" 11 | 12 | parameter: Optional[str] = None 13 | pointer: Optional[str] = None 14 | 15 | 16 | class ExceptionSchema(BaseModel): 17 | """Exception schema.""" 18 | 19 | status: str 20 | source: Optional[ExceptionSourceSchema] = None 21 | title: str 22 | detail: Any 23 | 24 | 25 | class ExceptionResponseSchema(BaseModel): 26 | """Exception response schema.""" 27 | 28 | errors: list[ExceptionSchema] 29 | jsonapi: dict[str, str] = Field(default={"version": "1.0"}) 30 | -------------------------------------------------------------------------------- /fastapi_jsonapi/exceptions/handlers.py: -------------------------------------------------------------------------------- 1 | from fastapi import Request 2 | from fastapi.responses import ORJSONResponse as JSONResponse 3 | 4 | from fastapi_jsonapi.exceptions import HTTPException 5 | 6 | 7 | async def base_exception_handler(request: Request, exc: HTTPException): 8 | return JSONResponse( 9 | status_code=exc.status_code, 10 | content={"errors": [exc.as_dict]}, 11 | ) 12 | -------------------------------------------------------------------------------- /fastapi_jsonapi/misc/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mts-ai/FastAPI-JSONAPI/a1a4fc5a868ab6bb563919c462ea248250e83b26/fastapi_jsonapi/misc/__init__.py -------------------------------------------------------------------------------- /fastapi_jsonapi/misc/sqla/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mts-ai/FastAPI-JSONAPI/a1a4fc5a868ab6bb563919c462ea248250e83b26/fastapi_jsonapi/misc/sqla/__init__.py -------------------------------------------------------------------------------- /fastapi_jsonapi/misc/sqla/generics/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mts-ai/FastAPI-JSONAPI/a1a4fc5a868ab6bb563919c462ea248250e83b26/fastapi_jsonapi/misc/sqla/generics/__init__.py -------------------------------------------------------------------------------- /fastapi_jsonapi/misc/sqla/generics/base.py: -------------------------------------------------------------------------------- 1 | from fastapi_jsonapi.data_layers.sqla.orm import SqlalchemyDataLayer 2 | from fastapi_jsonapi.views.view_base import ViewBase 3 | 4 | 5 | class ViewBaseGeneric(ViewBase): 6 | data_layer_cls = SqlalchemyDataLayer 7 | -------------------------------------------------------------------------------- /fastapi_jsonapi/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mts-ai/FastAPI-JSONAPI/a1a4fc5a868ab6bb563919c462ea248250e83b26/fastapi_jsonapi/py.typed -------------------------------------------------------------------------------- /fastapi_jsonapi/schema_base.py: -------------------------------------------------------------------------------- 1 | __all__ = ( 2 | "BaseModel", 3 | "Field", 4 | "registry", 5 | ) 6 | 7 | from pydantic import BaseModel as BaseModelGeneric 8 | from pydantic import Field 9 | 10 | 11 | class Registry: 12 | def __init__(self): 13 | self._known = {} 14 | 15 | def add(self, schema): 16 | self._known[schema.__name__] = schema 17 | 18 | def get(self, name: str): 19 | return self._known.get(name) 20 | 21 | @property 22 | def schemas(self): 23 | return dict(self._known) 24 | 25 | 26 | registry = Registry() 27 | 28 | 29 | class RegistryMeta(BaseModelGeneric): 30 | def __init_subclass__(cls, **kwargs): 31 | super().__init_subclass__(**kwargs) 32 | registry.add(cls) 33 | 34 | 35 | class BaseModel(RegistryMeta): 36 | pass 37 | -------------------------------------------------------------------------------- /fastapi_jsonapi/storages/__init__.py: -------------------------------------------------------------------------------- 1 | from fastapi_jsonapi.storages.models_storage import models_storage 2 | from fastapi_jsonapi.storages.schemas_storage import schemas_storage 3 | from fastapi_jsonapi.storages.views_storage import views_storage 4 | 5 | __all__ = [ 6 | "models_storage", 7 | "schemas_storage", 8 | "views_storage", 9 | ] 10 | -------------------------------------------------------------------------------- /fastapi_jsonapi/storages/views_storage.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import TYPE_CHECKING, Type 4 | 5 | from fastapi_jsonapi.exceptions import InternalServerError 6 | 7 | if TYPE_CHECKING: 8 | from fastapi_jsonapi.views import ViewBase 9 | 10 | 11 | class ViewStorage: 12 | def __init__(self): 13 | self._views: dict[str, Type[ViewBase]] = {} 14 | 15 | def add_view(self, resource_type: str, view: Type[ViewBase]): 16 | self._views[resource_type] = view 17 | 18 | def get_view(self, resource_type: str) -> Type[ViewBase]: 19 | try: 20 | return self._views[resource_type] 21 | except KeyError: 22 | raise InternalServerError( 23 | detail=f"Not found view for resource type {resource_type!r}", 24 | ) 25 | 26 | def has_view(self, resource_type: str) -> bool: 27 | return resource_type in self._views 28 | 29 | 30 | views_storage = ViewStorage() 31 | -------------------------------------------------------------------------------- /fastapi_jsonapi/types_metadata/__init__.py: -------------------------------------------------------------------------------- 1 | from .client_can_set_id import ClientCanSetId 2 | from .custom_filter_sql import CustomFilterSQL 3 | from .custom_sort_sql import CustomSortSQL 4 | from .relationship_info import RelationshipInfo 5 | 6 | __all__ = ( 7 | "ClientCanSetId", 8 | "CustomFilterSQL", 9 | "CustomSortSQL", 10 | "RelationshipInfo", 11 | ) 12 | -------------------------------------------------------------------------------- /fastapi_jsonapi/types_metadata/client_can_set_id.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import Any, Callable, Optional 3 | 4 | 5 | @dataclass(frozen=True) 6 | class ClientCanSetId: 7 | cast_type: Optional[Callable[[Any], Any]] = None 8 | -------------------------------------------------------------------------------- /fastapi_jsonapi/types_metadata/custom_sort_sql.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import Generic, TypeVar, Union 3 | 4 | # noinspection PyProtectedMember 5 | from pydantic.fields import FieldInfo 6 | from sqlalchemy import func 7 | from sqlalchemy.orm import InstrumentedAttribute 8 | from sqlalchemy.sql.expression import BinaryExpression, BooleanClauseList 9 | 10 | ColumnType = TypeVar("ColumnType") 11 | ExpressionType = TypeVar("ExpressionType") 12 | 13 | 14 | @dataclass(frozen=True) 15 | class CustomSortSQL(Generic[ColumnType, ExpressionType]): 16 | def get_expression( 17 | self, 18 | schema_field: FieldInfo, 19 | model_column: ColumnType, 20 | ) -> ExpressionType: 21 | raise NotImplementedError 22 | 23 | 24 | class CustomSortSQLA(CustomSortSQL[InstrumentedAttribute, Union[BinaryExpression, BooleanClauseList]]): 25 | """Base class for custom SQLAlchemy sorts""" 26 | 27 | 28 | class RegisterFreeStringSortSQL(CustomSortSQLA): 29 | def get_expression( 30 | self, 31 | schema_field: FieldInfo, 32 | model_column: InstrumentedAttribute, 33 | ) -> BinaryExpression: 34 | return func.lower(model_column) 35 | 36 | 37 | sql_register_free_sort = RegisterFreeStringSortSQL() 38 | -------------------------------------------------------------------------------- /fastapi_jsonapi/types_metadata/relationship_info.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | 4 | @dataclass(frozen=True) 5 | class RelationshipInfo: 6 | resource_type: str 7 | many: bool = False 8 | resource_id_example: str = "1" 9 | id_field_name: str = "id" 10 | -------------------------------------------------------------------------------- /fastapi_jsonapi/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mts-ai/FastAPI-JSONAPI/a1a4fc5a868ab6bb563919c462ea248250e83b26/fastapi_jsonapi/utils/__init__.py -------------------------------------------------------------------------------- /fastapi_jsonapi/utils/dependency_helper.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | from contextlib import AsyncExitStack 3 | from typing import Any, Awaitable, Callable, TypeVar, Union 4 | 5 | from fastapi import Request 6 | from fastapi.dependencies.models import Dependant 7 | from fastapi.dependencies.utils import get_dependant, solve_dependencies 8 | from fastapi.exceptions import RequestValidationError 9 | 10 | ReturnType = TypeVar("ReturnType") 11 | FuncReturnType = Union[Awaitable[ReturnType], ReturnType] 12 | 13 | 14 | class DependencyHelper: 15 | """ 16 | DependencyHelper for resolving dependencies. 17 | 18 | Use this helper to run a func with some FastAPI Dependencies 19 | """ 20 | 21 | def __init__(self, request: Request): 22 | self.request = request 23 | 24 | async def solve_dependencies_and_run(self, dependant: Dependant) -> ReturnType: 25 | body_data = await self.request.body() or None 26 | body = body_data and (await self.request.json()) 27 | async with AsyncExitStack() as async_exit_stack: 28 | solved_dependencies = await solve_dependencies( 29 | request=self.request, 30 | dependant=dependant, 31 | body=body, 32 | async_exit_stack=async_exit_stack, 33 | embed_body_fields=True, 34 | ) 35 | 36 | if solved_dependencies.errors: 37 | raise RequestValidationError(solved_dependencies.errors, body=body) 38 | 39 | orig_func: Callable[..., FuncReturnType[Any]] = dependant.call # type: ignore 40 | if inspect.iscoroutinefunction(orig_func): 41 | function_call_result = await orig_func(**solved_dependencies.values) 42 | else: 43 | function_call_result = orig_func(**solved_dependencies.values) 44 | 45 | return function_call_result 46 | 47 | async def run(self, func: Callable[..., FuncReturnType[Any]]) -> ReturnType: 48 | dependant = get_dependant( 49 | path=self.request.url.path, 50 | call=func, 51 | ) 52 | 53 | return await self.solve_dependencies_and_run(dependant) 54 | -------------------------------------------------------------------------------- /fastapi_jsonapi/utils/exceptions.py: -------------------------------------------------------------------------------- 1 | from functools import wraps 2 | 3 | from fastapi import HTTPException, status 4 | from pydantic import ValidationError 5 | 6 | 7 | def handle_validation_error(func): 8 | @wraps(func) 9 | def wrapper(*args, **kwargs): 10 | try: 11 | return func(*args, **kwargs) 12 | except ValidationError as ex: 13 | raise HTTPException( 14 | status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, 15 | detail=ex.errors(), 16 | ) 17 | 18 | return wrapper 19 | -------------------------------------------------------------------------------- /fastapi_jsonapi/utils/metadata_instance_search.py: -------------------------------------------------------------------------------- 1 | # noinspection PyProtectedMember 2 | from collections.abc import Generator 3 | from typing import Generic, Optional, TypeVar 4 | 5 | # noinspection PyProtectedMember 6 | from pydantic.fields import FieldInfo 7 | 8 | SearchType = TypeVar("SearchType") 9 | 10 | 11 | class MetadataInstanceSearch(Generic[SearchType]): 12 | def __init__(self, search_type: type[SearchType]): 13 | self.search_type = search_type 14 | 15 | def iterate(self, field: FieldInfo) -> Generator[SearchType, None, None]: 16 | for elem in field.metadata: 17 | if isinstance(elem, self.search_type): 18 | yield elem 19 | 20 | return None 21 | 22 | def first(self, field: FieldInfo) -> Optional[SearchType]: 23 | return next(self.iterate(field), None) 24 | -------------------------------------------------------------------------------- /fastapi_jsonapi/validation_utils.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import TYPE_CHECKING, Callable, Optional, Type 4 | 5 | from pydantic import BaseModel, field_validator, model_validator 6 | from pydantic._internal._decorators import PydanticDescriptorProxy 7 | 8 | if TYPE_CHECKING: 9 | # noinspection PyProtectedMember 10 | from pydantic._internal._decorators import DecoratorInfos 11 | 12 | 13 | def extract_validators( 14 | model: Type[BaseModel], 15 | include_for_field_names: Optional[set[str]] = None, 16 | exclude_for_field_names: Optional[set[str]] = None, 17 | ) -> tuple[dict[str, Callable], dict[str, PydanticDescriptorProxy]]: 18 | validators: DecoratorInfos = model.__pydantic_decorators__ 19 | 20 | exclude_for_field_names = exclude_for_field_names or set() 21 | if include_for_field_names and exclude_for_field_names: 22 | include_for_field_names = include_for_field_names.difference( 23 | exclude_for_field_names, 24 | ) 25 | 26 | field_validators, model_validators = {}, {} 27 | 28 | # field validators 29 | for name, validator in validators.field_validators.items(): 30 | for field_name in validator.info.fields: 31 | # exclude 32 | if field_name in exclude_for_field_names: 33 | continue 34 | # or include 35 | if include_for_field_names and field_name not in include_for_field_names: 36 | continue 37 | validator_config = field_validator(field_name, mode=validator.info.mode) 38 | 39 | func = validator.func.__func__ if hasattr(validator.func, "__func__") else validator.func 40 | 41 | field_validators[name] = validator_config(func) 42 | 43 | # model validators 44 | for name, validator in validators.model_validators.items(): 45 | validator_config = model_validator(mode=validator.info.mode) 46 | 47 | func = validator.func.__func__ if hasattr(validator.func, "__func__") else validator.func 48 | 49 | model_validators[name] = validator_config(func) 50 | 51 | return field_validators, model_validators 52 | -------------------------------------------------------------------------------- /fastapi_jsonapi/views/__init__.py: -------------------------------------------------------------------------------- 1 | from fastapi_jsonapi.views.enums import Operation 2 | from fastapi_jsonapi.views.schemas import OperationConfig, RelationshipRequestInfo 3 | from fastapi_jsonapi.views.view_base import ViewBase 4 | 5 | __all__ = [ 6 | "Operation", 7 | "OperationConfig", 8 | "RelationshipRequestInfo", 9 | "ViewBase", 10 | ] 11 | -------------------------------------------------------------------------------- /fastapi_jsonapi/views/enums.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from enum import Enum, auto 4 | 5 | 6 | class Operation(str, Enum): 7 | ALL = auto() 8 | CREATE = auto() 9 | DELETE = auto() 10 | DELETE_LIST = auto() 11 | GET = auto() 12 | GET_LIST = auto() 13 | UPDATE = auto() 14 | 15 | @staticmethod 16 | def real_operations() -> list[Operation]: 17 | return list(filter(lambda op: op != Operation.ALL, Operation)) 18 | 19 | def http_method(self) -> str: 20 | if self == Operation.ALL: 21 | msg = "HTTP method is not defined for 'ALL' operation." 22 | raise Exception(msg) 23 | 24 | operation_to_http_method = { 25 | Operation.GET: "GET", 26 | Operation.GET_LIST: "GET", 27 | Operation.UPDATE: "PATCH", 28 | Operation.CREATE: "POST", 29 | Operation.DELETE: "DELETE", 30 | Operation.DELETE_LIST: "DELETE", 31 | } 32 | return operation_to_http_method[self] 33 | -------------------------------------------------------------------------------- /fastapi_jsonapi/views/schemas.py: -------------------------------------------------------------------------------- 1 | from typing import Callable, Coroutine, Optional, Type, Union 2 | 3 | from pydantic import BaseModel, ConfigDict 4 | 5 | 6 | class OperationConfig(BaseModel): 7 | model_config = ConfigDict( 8 | arbitrary_types_allowed=True, 9 | ) 10 | 11 | dependencies: Optional[Type[BaseModel]] = None 12 | prepare_data_layer_kwargs: Optional[Union[Callable, Coroutine]] = None 13 | 14 | @property 15 | def handler(self) -> Optional[Union[Callable, Coroutine]]: 16 | return self.prepare_data_layer_kwargs 17 | 18 | 19 | class RelationshipRequestInfo(BaseModel): 20 | parent_obj_id: str 21 | parent_resource_type: str 22 | relationship_name: str 23 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mts-ai/FastAPI-JSONAPI/a1a4fc5a868ab6bb563919c462ea248250e83b26/tests/__init__.py -------------------------------------------------------------------------------- /tests/common.py: -------------------------------------------------------------------------------- 1 | from os import getenv 2 | from pathlib import Path 3 | 4 | from sqlalchemy import event 5 | from sqlalchemy.engine import Engine 6 | 7 | 8 | def sqla_uri(): 9 | testing_db_url = getenv("TESTING_DB_URL") 10 | if not testing_db_url: 11 | db_dir = Path(__file__).resolve().parent 12 | testing_db_url = f"sqlite+aiosqlite:///{db_dir}/db.sqlite3" 13 | return testing_db_url 14 | 15 | 16 | def is_postgres_tests() -> bool: 17 | return "postgres" in sqla_uri() 18 | 19 | 20 | def is_sqlite_tests() -> bool: 21 | return "sqlite" in sqla_uri() 22 | 23 | 24 | @event.listens_for(Engine, "connect") 25 | def set_sqlite_pragma(dbapi_connection, connection_record): 26 | """ 27 | https://docs.sqlalchemy.org/en/14/dialects/sqlite.html#foreign-key-support 28 | """ 29 | if is_sqlite_tests(): 30 | cursor = dbapi_connection.cursor() 31 | cursor.execute("PRAGMA foreign_keys=ON") 32 | cursor.close() 33 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import logging 3 | from collections import defaultdict 4 | from copy import copy 5 | 6 | import pytest 7 | from fastapi import FastAPI 8 | from httpx import AsyncClient 9 | from pytest import fixture # noqa PT013 10 | from pytest_asyncio import fixture as async_fixture 11 | 12 | from fastapi_jsonapi.atomic.prepared_atomic_operation import atomic_dependency_handlers 13 | from fastapi_jsonapi.data_layers.sqla.query_building import relationships_info_storage 14 | from tests.fixtures.app import ( # noqa 15 | app, 16 | app_plain, 17 | ) 18 | from tests.fixtures.db_connection import ( # noqa 19 | async_engine, 20 | async_session, 21 | refresh_db, 22 | ) 23 | from tests.fixtures.entities import ( # noqa 24 | child_1, 25 | child_2, 26 | child_3, 27 | child_4, 28 | computer_1, 29 | computer_2, 30 | computer_factory, 31 | p1_c1_association, 32 | p1_c2_association, 33 | p2_c1_association, 34 | p2_c2_association, 35 | p2_c3_association, 36 | parent_1, 37 | parent_2, 38 | parent_3, 39 | task_1, 40 | task_2, 41 | user_1, 42 | user_1_bio, 43 | user_1_comments_for_u2_posts, 44 | user_1_post, 45 | user_1_post_for_comments, 46 | user_1_posts, 47 | user_2, 48 | user_2_bio, 49 | user_2_comment_for_one_u1_post, 50 | user_2_posts, 51 | user_3, 52 | workplace_1, 53 | workplace_2, 54 | ) 55 | from tests.fixtures.user import ( # noqa 56 | user_attributes, 57 | user_attributes_factory, 58 | ) 59 | from tests.fixtures.views import ViewBaseGeneric # noqa 60 | 61 | 62 | def configure_logging(): 63 | logging.getLogger("faker.factory").setLevel(logging.INFO) 64 | logging.getLogger("aiosqlite").setLevel(logging.INFO) 65 | # logging.getLogger("sqlalchemy.engine").setLevel(logging.INFO) 66 | logging.basicConfig(level=logging.DEBUG) 67 | 68 | 69 | configure_logging() 70 | 71 | 72 | @pytest.fixture(scope="session") 73 | def event_loop(): 74 | """ 75 | Create an instance of the default event loop for each test case. 76 | 77 | Why: 78 | https://stackoverflow.com/questions/66054356/multiple-async-unit-tests-fail-but-running-them-one-by-one-will-pass 79 | """ 80 | loop = asyncio.get_event_loop_policy().new_event_loop() 81 | yield loop 82 | loop.close() 83 | 84 | 85 | @async_fixture() 86 | async def client(app: FastAPI) -> AsyncClient: # noqa 87 | async with AsyncClient(app=app, base_url="http://test") as ac: 88 | yield ac 89 | 90 | 91 | @pytest.fixture 92 | def clear_relationships_info_storage(): 93 | data = copy(relationships_info_storage._data) 94 | relationships_info_storage._data = defaultdict(dict) 95 | yield 96 | relationships_info_storage._data = data 97 | 98 | 99 | @pytest.fixture(autouse=True) 100 | def clear_atomic_dependency_handlers(): 101 | atomic_dependency_handlers.clear() 102 | -------------------------------------------------------------------------------- /tests/fixtures/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mts-ai/FastAPI-JSONAPI/a1a4fc5a868ab6bb563919c462ea248250e83b26/tests/fixtures/__init__.py -------------------------------------------------------------------------------- /tests/fixtures/db_connection.py: -------------------------------------------------------------------------------- 1 | from pytest_asyncio import fixture as async_fixture 2 | from sqlalchemy.engine import make_url 3 | 4 | from examples.api_for_sqlalchemy.models.base import Base 5 | from examples.api_for_sqlalchemy.models.db import DB 6 | from tests.common import sqla_uri 7 | 8 | db = DB( 9 | url=make_url(sqla_uri()), 10 | ) 11 | 12 | 13 | async def async_session_dependency(): 14 | async with db.session_maker() as session: 15 | yield session 16 | 17 | 18 | @async_fixture(scope="class") 19 | async def async_engine(): 20 | async with db.engine.begin() as conn: 21 | await conn.run_sync(Base.metadata.drop_all) 22 | await conn.run_sync(Base.metadata.create_all) 23 | 24 | 25 | @async_fixture(scope="class") 26 | async def async_session(async_engine): 27 | async with db.session_maker() as session: 28 | yield session 29 | 30 | 31 | @async_fixture(autouse=True) 32 | async def refresh_db(async_engine): # F811 33 | async with db.engine.begin() as connector: 34 | for table in reversed(Base.metadata.sorted_tables): 35 | await connector.execute(table.delete()) 36 | -------------------------------------------------------------------------------- /tests/fixtures/debug_app.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | import uvicorn 4 | 5 | from tests.fixtures.app import add_routers, build_app_plain 6 | 7 | CURRENT_FILE = Path(__file__).resolve() 8 | CURRENT_DIR = CURRENT_FILE.parent 9 | 10 | 11 | app = build_app_plain() 12 | add_routers(app) 13 | 14 | 15 | if __name__ == "__main__": 16 | uvicorn.run( 17 | "debug_app:app", 18 | host="0.0.0.0", 19 | port=8082, 20 | reload=True, 21 | app_dir=f"{CURRENT_DIR}", 22 | ) 23 | -------------------------------------------------------------------------------- /tests/fixtures/models/__init__.py: -------------------------------------------------------------------------------- 1 | from tests.fixtures.models.alpha import Alpha 2 | from tests.fixtures.models.beta import Beta 3 | from tests.fixtures.models.beta_delta_binding import BetaDeltaBinding 4 | from tests.fixtures.models.beta_gamma_binding import BetaGammaBinding 5 | from tests.fixtures.models.cascade_case import CascadeCase 6 | from tests.fixtures.models.contains_timestamp import ContainsTimestamp 7 | from tests.fixtures.models.custom_uuid_item import CustomUUIDItem 8 | from tests.fixtures.models.delta import Delta 9 | from tests.fixtures.models.gamma import Gamma 10 | from tests.fixtures.models.self_relationship import SelfRelationship 11 | from tests.fixtures.models.task import Task 12 | 13 | __all__ = ( 14 | "Alpha", 15 | "Beta", 16 | "BetaDeltaBinding", 17 | "BetaGammaBinding", 18 | "CascadeCase", 19 | "ContainsTimestamp", 20 | "CustomUUIDItem", 21 | "Delta", 22 | "Gamma", 23 | "SelfRelationship", 24 | "Task", 25 | ) 26 | -------------------------------------------------------------------------------- /tests/fixtures/models/alpha.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import TYPE_CHECKING 4 | 5 | from sqlalchemy import ForeignKey 6 | from sqlalchemy.orm import Mapped, mapped_column, relationship 7 | 8 | from examples.api_for_sqlalchemy.models.base import Base 9 | 10 | if TYPE_CHECKING: 11 | from .beta import Beta 12 | from .gamma import Gamma 13 | 14 | 15 | class Alpha(Base): 16 | __tablename__ = "alpha" 17 | 18 | beta_id: Mapped[int] = mapped_column(ForeignKey("beta.id"), index=True) 19 | beta: Mapped[Beta] = relationship(back_populates="alphas") 20 | gamma_id: Mapped[int] = mapped_column(ForeignKey("gamma.id")) 21 | gamma: Mapped[Gamma] = relationship("Gamma") 22 | -------------------------------------------------------------------------------- /tests/fixtures/models/beta.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.orm import Mapped, relationship 2 | 3 | from examples.api_for_sqlalchemy.models.base import Base 4 | 5 | from .alpha import Alpha 6 | from .delta import Delta 7 | from .gamma import Gamma 8 | 9 | 10 | class Beta(Base): 11 | __tablename__ = "beta" 12 | 13 | alphas: Mapped[Alpha] = relationship("Alpha") 14 | deltas: Mapped[list[Delta]] = relationship( 15 | "Delta", 16 | secondary="beta_delta_binding", 17 | lazy="noload", 18 | ) 19 | gammas: Mapped[list[Gamma]] = relationship( 20 | "Gamma", 21 | secondary="beta_gamma_binding", 22 | back_populates="betas", 23 | lazy="noload", 24 | ) 25 | -------------------------------------------------------------------------------- /tests/fixtures/models/beta_delta_binding.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import ForeignKey 2 | from sqlalchemy.orm import Mapped, mapped_column 3 | 4 | from examples.api_for_sqlalchemy.models.base import Base 5 | 6 | 7 | class BetaDeltaBinding(Base): 8 | __tablename__ = "beta_delta_binding" 9 | 10 | beta_id: Mapped[int] = mapped_column(ForeignKey("beta.id", ondelete="CASCADE")) 11 | delta_id: Mapped[int] = mapped_column(ForeignKey("delta.id", ondelete="CASCADE")) 12 | -------------------------------------------------------------------------------- /tests/fixtures/models/beta_gamma_binding.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import ForeignKey 2 | from sqlalchemy.orm import Mapped, mapped_column 3 | 4 | from examples.api_for_sqlalchemy.models.base import Base 5 | 6 | 7 | class BetaGammaBinding(Base): 8 | __tablename__ = "beta_gamma_binding" 9 | 10 | beta_id: Mapped[int] = mapped_column(ForeignKey("beta.id", ondelete="CASCADE")) 11 | gamma_id: Mapped[int] = mapped_column(ForeignKey("gamma.id", ondelete="CASCADE")) 12 | -------------------------------------------------------------------------------- /tests/fixtures/models/cascade_case.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import TYPE_CHECKING, Optional 4 | 5 | from sqlalchemy import ForeignKey 6 | from sqlalchemy.orm import Mapped, backref, mapped_column, relationship 7 | 8 | from examples.api_for_sqlalchemy.models.base import Base 9 | 10 | 11 | class CascadeCase(Base): 12 | __tablename__ = "cascade_case" 13 | 14 | parent_item_id: Mapped[Optional[int]] = mapped_column( 15 | ForeignKey( 16 | "cascade_case.id", 17 | onupdate="CASCADE", 18 | ondelete="CASCADE", 19 | ), 20 | ) 21 | sub_items: Mapped[list[CascadeCase]] = relationship( 22 | backref=backref("parent_item", remote_side="CascadeCase.id"), 23 | ) 24 | 25 | if TYPE_CHECKING: 26 | parent_item: Mapped[CascadeCase] 27 | -------------------------------------------------------------------------------- /tests/fixtures/models/contains_timestamp.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from sqlalchemy import DateTime 4 | from sqlalchemy.orm import Mapped, mapped_column 5 | 6 | from examples.api_for_sqlalchemy.models.base import Base 7 | 8 | 9 | class ContainsTimestamp(Base): 10 | __tablename__ = "contains_timestamp" 11 | 12 | timestamp: Mapped[datetime] = mapped_column(DateTime(timezone=True)) 13 | -------------------------------------------------------------------------------- /tests/fixtures/models/custom_uuid_item.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | from uuid import UUID 3 | 4 | from sqlalchemy.orm import Mapped, mapped_column 5 | from sqlalchemy.types import UUID as UUIDType 6 | 7 | from examples.api_for_sqlalchemy.models.base import Base 8 | 9 | 10 | class CustomUUIDItem(Base): 11 | __tablename__ = "custom_uuid_item" 12 | 13 | id: Mapped[UUID] = mapped_column(UUIDType(as_uuid=True), primary_key=True) 14 | extra_id: Mapped[Optional[UUID]] = mapped_column(UUIDType(as_uuid=True), unique=True) 15 | -------------------------------------------------------------------------------- /tests/fixtures/models/delta.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import TYPE_CHECKING 4 | 5 | from sqlalchemy.orm import Mapped, relationship 6 | 7 | from examples.api_for_sqlalchemy.models.base import Base 8 | 9 | if TYPE_CHECKING: 10 | from .beta import Beta 11 | from .gamma import Gamma 12 | 13 | 14 | class Delta(Base): 15 | __tablename__ = "delta" 16 | 17 | name: Mapped[str] 18 | 19 | gammas: Mapped[list[Gamma]] = relationship( 20 | "Gamma", 21 | back_populates="delta", 22 | lazy="noload", 23 | ) 24 | betas: Mapped[list[Beta]] = relationship( 25 | "Beta", 26 | secondary="beta_delta_binding", 27 | back_populates="deltas", 28 | lazy="noload", 29 | ) 30 | -------------------------------------------------------------------------------- /tests/fixtures/models/gamma.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import TYPE_CHECKING 4 | 5 | from sqlalchemy import ForeignKey 6 | from sqlalchemy.orm import Mapped, mapped_column, relationship 7 | 8 | from examples.api_for_sqlalchemy.models.base import Base 9 | 10 | if TYPE_CHECKING: 11 | from .alpha import Alpha 12 | from .beta import Beta 13 | from .delta import Delta 14 | 15 | 16 | class Gamma(Base): 17 | __tablename__ = "gamma" 18 | 19 | alpha: Mapped[Alpha] = relationship("Alpha") 20 | betas: Mapped[list[Beta]] = relationship( 21 | "Beta", 22 | secondary="beta_gamma_binding", 23 | back_populates="gammas", 24 | lazy="raise", 25 | ) 26 | delta_id: Mapped[int] = mapped_column( 27 | ForeignKey( 28 | "delta.id", 29 | ondelete="CASCADE", 30 | ), 31 | index=True, 32 | ) 33 | delta: Mapped[Delta] = relationship("Delta") 34 | -------------------------------------------------------------------------------- /tests/fixtures/models/self_relationship.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import TYPE_CHECKING, Optional 4 | 5 | from sqlalchemy import ForeignKey, String 6 | from sqlalchemy.orm import Mapped, backref, mapped_column, relationship 7 | 8 | from examples.api_for_sqlalchemy.models.base import Base 9 | 10 | 11 | class SelfRelationship(Base): 12 | __tablename__ = "selfrelationships" 13 | 14 | name: Mapped[str] = mapped_column(String) 15 | 16 | self_relationship_id: Mapped[Optional[int]] = mapped_column( 17 | ForeignKey( 18 | "selfrelationships.id", 19 | name="fk_self_relationship_id", 20 | ondelete="CASCADE", 21 | onupdate="CASCADE", 22 | ), 23 | ) 24 | children_objects: Mapped[list[SelfRelationship]] = relationship( 25 | backref=backref("parent_object", remote_side="SelfRelationship.id"), 26 | ) 27 | 28 | if TYPE_CHECKING: 29 | parent_object: Mapped[SelfRelationship] 30 | -------------------------------------------------------------------------------- /tests/fixtures/models/task.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from sqlalchemy import JSON 4 | from sqlalchemy.dialects.postgresql import JSONB 5 | from sqlalchemy.orm import Mapped, mapped_column 6 | 7 | from examples.api_for_sqlalchemy.models.base import Base 8 | from tests.common import is_postgres_tests 9 | 10 | 11 | class Task(Base): 12 | __tablename__ = "tasks" 13 | 14 | task_ids_dict_json: Mapped[Optional[dict]] = mapped_column(JSON, unique=False) 15 | task_ids_list_json: Mapped[Optional[list]] = mapped_column(JSON, unique=False) 16 | 17 | if is_postgres_tests(): 18 | task_ids_dict_jsonb: Mapped[Optional[dict]] = mapped_column(JSONB, unique=False) 19 | task_ids_list_jsonb: Mapped[Optional[list]] = mapped_column(JSONB, unique=False) 20 | -------------------------------------------------------------------------------- /tests/fixtures/schemas/__init__.py: -------------------------------------------------------------------------------- 1 | from .alpha import AlphaSchema 2 | from .beta import BetaSchema 3 | from .cascade_case import CascadeCaseSchema 4 | from .custom_uuid import ( 5 | CustomUUIDItemAttributesSchema, 6 | CustomUUIDItemSchema, 7 | ) 8 | from .delta import DeltaSchema 9 | from .gamma import GammaSchema 10 | from .self_relationship import SelfRelationshipAttributesSchema 11 | from .task import ( 12 | TaskBaseSchema, 13 | TaskInSchema, 14 | TaskPatchSchema, 15 | TaskSchema, 16 | ) 17 | 18 | __all__ = ( 19 | "AlphaSchema", 20 | "BetaSchema", 21 | "CascadeCaseSchema", 22 | "CustomUUIDItemAttributesSchema", 23 | "CustomUUIDItemSchema", 24 | "DeltaSchema", 25 | "GammaSchema", 26 | "SelfRelationshipAttributesSchema", 27 | "TaskBaseSchema", 28 | "TaskInSchema", 29 | "TaskPatchSchema", 30 | "TaskSchema", 31 | ) 32 | -------------------------------------------------------------------------------- /tests/fixtures/schemas/alpha.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import TYPE_CHECKING, Annotated, Optional 4 | 5 | from fastapi_jsonapi.schema_base import BaseModel 6 | from fastapi_jsonapi.types_metadata import RelationshipInfo 7 | 8 | if TYPE_CHECKING: 9 | from .beta import BetaSchema 10 | from .gamma import GammaSchema 11 | 12 | 13 | class AlphaSchema(BaseModel): 14 | beta: Annotated[ 15 | Optional[BetaSchema], 16 | RelationshipInfo( 17 | resource_type="beta", 18 | ), 19 | ] = None 20 | gamma: Annotated[ 21 | Optional[GammaSchema], 22 | RelationshipInfo( 23 | resource_type="gamma", 24 | ), 25 | ] = None 26 | -------------------------------------------------------------------------------- /tests/fixtures/schemas/beta.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import TYPE_CHECKING, Annotated, Optional 4 | 5 | from fastapi_jsonapi.schema_base import BaseModel 6 | from fastapi_jsonapi.types_metadata import RelationshipInfo 7 | 8 | if TYPE_CHECKING: 9 | from .alpha import AlphaSchema 10 | from .delta import DeltaSchema 11 | from .gamma import GammaSchema 12 | 13 | 14 | class BetaSchema(BaseModel): 15 | alphas: Annotated[ 16 | Optional[AlphaSchema], 17 | RelationshipInfo( 18 | resource_type="alpha", 19 | ), 20 | ] = None 21 | gammas: Annotated[ 22 | Optional[GammaSchema], 23 | RelationshipInfo( 24 | resource_type="gamma", 25 | many=True, 26 | ), 27 | ] = None 28 | deltas: Annotated[ 29 | Optional[DeltaSchema], 30 | RelationshipInfo( 31 | resource_type="delta", 32 | many=True, 33 | ), 34 | ] = None 35 | -------------------------------------------------------------------------------- /tests/fixtures/schemas/cascade_case.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import Annotated, Optional 4 | 5 | from fastapi_jsonapi.schema_base import BaseModel 6 | from fastapi_jsonapi.types_metadata import RelationshipInfo 7 | 8 | 9 | class CascadeCaseSchema(BaseModel): 10 | parent_item: Annotated[ 11 | Optional[CascadeCaseSchema], 12 | RelationshipInfo( 13 | resource_type="cascade_case", 14 | ), 15 | ] = None 16 | sub_items: Annotated[ 17 | Optional[list[CascadeCaseSchema]], 18 | RelationshipInfo( 19 | resource_type="cascade_case", 20 | many=True, 21 | ), 22 | ] = None 23 | -------------------------------------------------------------------------------- /tests/fixtures/schemas/custom_uuid.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import Annotated, Optional 4 | from uuid import UUID 5 | 6 | from pydantic import ConfigDict 7 | 8 | from fastapi_jsonapi.schema_base import BaseModel 9 | from fastapi_jsonapi.types_metadata import ClientCanSetId 10 | 11 | 12 | class CustomUUIDItemAttributesSchema(BaseModel): 13 | model_config = ConfigDict( 14 | from_attributes=True, 15 | ) 16 | 17 | extra_id: Optional[UUID] = None 18 | 19 | 20 | class CustomUUIDItemSchema(CustomUUIDItemAttributesSchema): 21 | id: Annotated[UUID, ClientCanSetId()] 22 | -------------------------------------------------------------------------------- /tests/fixtures/schemas/delta.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import TYPE_CHECKING, Annotated, Optional 4 | 5 | from fastapi_jsonapi.schema_base import BaseModel 6 | from fastapi_jsonapi.types_metadata import RelationshipInfo 7 | 8 | if TYPE_CHECKING: 9 | from .beta import BetaSchema 10 | from .gamma import GammaSchema 11 | 12 | 13 | class DeltaSchema(BaseModel): 14 | name: str 15 | 16 | gammas: Annotated[ 17 | Optional[GammaSchema], 18 | RelationshipInfo( 19 | resource_type="gamma", 20 | many=True, 21 | ), 22 | ] = None 23 | betas: Annotated[ 24 | Optional[BetaSchema], 25 | RelationshipInfo( 26 | resource_type="beta", 27 | many=True, 28 | ), 29 | ] = None 30 | -------------------------------------------------------------------------------- /tests/fixtures/schemas/gamma.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import TYPE_CHECKING, Annotated, Optional 4 | 5 | from fastapi_jsonapi.schema_base import BaseModel 6 | from fastapi_jsonapi.types_metadata import RelationshipInfo 7 | 8 | if TYPE_CHECKING: 9 | from .beta import BetaSchema 10 | from .delta import DeltaSchema 11 | 12 | 13 | class GammaSchema(BaseModel): 14 | betas: Annotated[ 15 | Optional[BetaSchema], 16 | RelationshipInfo( 17 | resource_type="beta", 18 | many=True, 19 | ), 20 | ] = None 21 | delta: Annotated[ 22 | Optional[DeltaSchema], 23 | RelationshipInfo( 24 | resource_type="delta", 25 | ), 26 | ] = None 27 | -------------------------------------------------------------------------------- /tests/fixtures/schemas/self_relationship.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import Annotated, Optional 4 | 5 | from fastapi_jsonapi.schema_base import BaseModel 6 | from fastapi_jsonapi.types_metadata import RelationshipInfo 7 | 8 | 9 | class SelfRelationshipAttributesSchema(BaseModel): 10 | name: str 11 | 12 | parent_object: Annotated[ 13 | Optional[SelfRelationshipAttributesSchema], 14 | RelationshipInfo( 15 | resource_type="self_relationship", 16 | ), 17 | ] = None 18 | children_objects: Annotated[ 19 | Optional[list[SelfRelationshipAttributesSchema]], 20 | RelationshipInfo( 21 | resource_type="self_relationship", 22 | many=True, 23 | ), 24 | ] = None 25 | -------------------------------------------------------------------------------- /tests/fixtures/schemas/task.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import Annotated, Optional 4 | 5 | from pydantic import ConfigDict, field_validator 6 | 7 | from fastapi_jsonapi.schema_base import BaseModel 8 | from fastapi_jsonapi.types_metadata.custom_filter_sql import ( 9 | sql_filter_pg_json_contains, 10 | sql_filter_pg_json_ilike, 11 | sql_filter_pg_jsonb_contains, 12 | sql_filter_pg_jsonb_ilike, 13 | sql_filter_sqlite_json_contains, 14 | sql_filter_sqlite_json_ilike, 15 | ) 16 | from tests.common import is_postgres_tests 17 | 18 | 19 | class TaskBaseSchema(BaseModel): 20 | model_config = ConfigDict( 21 | from_attributes=True, 22 | ) 23 | 24 | if is_postgres_tests(): 25 | task_ids_dict_json: Annotated[Optional[dict], sql_filter_pg_json_ilike] 26 | task_ids_list_json: Annotated[Optional[list], sql_filter_pg_json_contains] 27 | else: 28 | task_ids_dict_json: Annotated[Optional[dict], sql_filter_sqlite_json_ilike] 29 | task_ids_list_json: Annotated[Optional[list], sql_filter_sqlite_json_contains] 30 | 31 | # noinspection PyMethodParameters 32 | @field_validator("task_ids_dict_json", mode="before", check_fields=False) 33 | @classmethod 34 | def task_ids_dict_json_validator(cls, value: Optional[dict]): 35 | """ 36 | return `{}`, if value is None both on get and on create 37 | """ 38 | return value or {} 39 | 40 | # noinspection PyMethodParameters 41 | @field_validator("task_ids_list_json", mode="before", check_fields=False) 42 | @classmethod 43 | def task_ids_list_json_validator(cls, value: Optional[list]): 44 | """ 45 | return `[]`, if value is None both on get and on create 46 | """ 47 | return value or [] 48 | 49 | if is_postgres_tests(): 50 | task_ids_dict_jsonb: Annotated[Optional[dict], sql_filter_pg_jsonb_ilike] 51 | task_ids_list_jsonb: Annotated[Optional[list], sql_filter_pg_jsonb_contains] 52 | 53 | # noinspection PyMethodParameters 54 | @field_validator("task_ids_dict_jsonb", mode="before", check_fields=False) 55 | @classmethod 56 | def task_ids_dict_jsonb_validator(cls, value: Optional[dict]): 57 | """ 58 | return `{}`, if value is None both on get and on create 59 | """ 60 | return value or {} 61 | 62 | # noinspection PyMethodParameters 63 | @field_validator("task_ids_list_jsonb", mode="before", check_fields=False) 64 | @classmethod 65 | def task_ids_list_jsonb_validator(cls, value: Optional[list]): 66 | """ 67 | return `[]`, if value is None both on get and on create 68 | """ 69 | return value or [] 70 | 71 | 72 | class TaskPatchSchema(TaskBaseSchema): 73 | """Task PATCH schema.""" 74 | 75 | 76 | class TaskInSchema(TaskBaseSchema): 77 | """Task create schema.""" 78 | 79 | 80 | class TaskSchema(TaskBaseSchema): 81 | """Task item schema.""" 82 | 83 | id: int 84 | -------------------------------------------------------------------------------- /tests/fixtures/user.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from examples.api_for_sqlalchemy.schemas import UserAttributesBaseSchema 4 | from tests.misc.utils import fake 5 | 6 | 7 | @pytest.fixture 8 | def user_attributes_factory(): 9 | def factory(): 10 | user_attributes = UserAttributesBaseSchema( 11 | name=fake.name(), 12 | age=fake.pyint(min_value=13, max_value=99), 13 | email=fake.email(), 14 | ) 15 | return user_attributes 16 | 17 | return factory 18 | 19 | 20 | @pytest.fixture 21 | def user_attributes(user_attributes_factory): 22 | return user_attributes_factory() 23 | -------------------------------------------------------------------------------- /tests/fixtures/views.py: -------------------------------------------------------------------------------- 1 | from typing import ClassVar 2 | 3 | from fastapi import Depends 4 | from pydantic import BaseModel, ConfigDict 5 | from sqlalchemy.ext.asyncio import AsyncSession 6 | 7 | from fastapi_jsonapi.misc.sqla.generics.base import ViewBaseGeneric as ViewBaseGenericHelper 8 | from fastapi_jsonapi.views import Operation, OperationConfig, ViewBase 9 | from tests.fixtures.db_connection import async_session_dependency 10 | 11 | 12 | class ArbitraryModelBase(BaseModel): 13 | model_config = ConfigDict( 14 | arbitrary_types_allowed=True, 15 | ) 16 | 17 | 18 | class SessionDependency(ArbitraryModelBase): 19 | session: AsyncSession = Depends(async_session_dependency) 20 | 21 | 22 | def common_handler(view: ViewBase, dto: SessionDependency) -> dict: 23 | return { 24 | "session": dto.session, 25 | } 26 | 27 | 28 | class ViewBaseGeneric(ViewBaseGenericHelper): 29 | operation_dependencies: ClassVar = { 30 | Operation.ALL: OperationConfig( 31 | dependencies=SessionDependency, 32 | prepare_data_layer_kwargs=common_handler, 33 | ), 34 | } 35 | -------------------------------------------------------------------------------- /tests/misc/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mts-ai/FastAPI-JSONAPI/a1a4fc5a868ab6bb563919c462ea248250e83b26/tests/misc/__init__.py -------------------------------------------------------------------------------- /tests/misc/utils.py: -------------------------------------------------------------------------------- 1 | # fmt: off 2 | __all__ = ( 3 | "fake", 4 | ) 5 | # fmt: on 6 | 7 | from faker import Faker 8 | 9 | fake = Faker() 10 | 11 | Faker.seed("some-qwerty-seed-to-keep-persistent-abc-mts-ai-fastapi-jsonapi") 12 | -------------------------------------------------------------------------------- /tests/test_api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mts-ai/FastAPI-JSONAPI/a1a4fc5a868ab6bb563919c462ea248250e83b26/tests/test_api/__init__.py -------------------------------------------------------------------------------- /tests/test_atomic/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mts-ai/FastAPI-JSONAPI/a1a4fc5a868ab6bb563919c462ea248250e83b26/tests/test_atomic/__init__.py -------------------------------------------------------------------------------- /tests/test_atomic/conftest.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Sequence 2 | 3 | import pytest 4 | 5 | from fastapi_jsonapi.atomic.schemas import AtomicOperationAction 6 | 7 | 8 | @pytest.fixture 9 | def allowed_atomic_actions_list() -> list[str]: 10 | return [op.value for op in AtomicOperationAction] 11 | 12 | 13 | def options_as_pydantic_choices_string(options: Sequence[str]) -> str: 14 | if len(options) == 1: 15 | return repr(options[0]) 16 | return " or ".join( 17 | ( 18 | ", ".join(repr(op) for op in options[:-1]), 19 | repr(options[-1]), 20 | ), 21 | ) 22 | 23 | 24 | @pytest.fixture 25 | def allowed_atomic_actions_as_string(allowed_atomic_actions_list) -> str: 26 | return options_as_pydantic_choices_string(allowed_atomic_actions_list) 27 | -------------------------------------------------------------------------------- /tests/test_atomic/test_delete_objects.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from typing import Awaitable, Callable 3 | 4 | from fastapi import status 5 | from httpx import AsyncClient 6 | from sqlalchemy import select 7 | from sqlalchemy.ext.asyncio import AsyncSession 8 | from sqlalchemy.sql.functions import count 9 | 10 | from examples.api_for_sqlalchemy.models import Computer 11 | from fastapi_jsonapi.atomic.schemas import AtomicOperationAction 12 | 13 | logging.basicConfig(level=logging.DEBUG) 14 | 15 | 16 | class TestAtomicDeleteObjects: 17 | async def test_delete_two_objects( 18 | self, 19 | client: AsyncClient, 20 | async_session: AsyncSession, 21 | computer_factory: Callable[..., Awaitable[Computer]], 22 | ): 23 | computer_1 = await computer_factory() 24 | computer_2 = await computer_factory() 25 | 26 | computers_ids = [ 27 | computer_1.id, 28 | computer_2.id, 29 | ] 30 | stmt_computers = select(count(Computer.id)).where( 31 | Computer.id.in_(computers_ids), 32 | ) 33 | computers_count = await async_session.scalar(stmt_computers) 34 | assert computers_count == len(computers_ids) 35 | 36 | data_atomic_request = { 37 | "atomic:operations": [ 38 | { 39 | "op": "remove", 40 | "ref": { 41 | "id": f"{computer_1.id}", 42 | "type": "computer", 43 | }, 44 | }, 45 | { 46 | "op": "remove", 47 | "ref": { 48 | "id": f"{computer_2.id}", 49 | "type": "computer", 50 | }, 51 | }, 52 | ], 53 | } 54 | response = await client.post("/operations", json=data_atomic_request) 55 | assert response.status_code == status.HTTP_204_NO_CONTENT, response.text 56 | assert response.content == b"" 57 | 58 | computers_count = await async_session.scalar(stmt_computers) 59 | assert computers_count == 0 60 | 61 | async def test_delete_no_ref( 62 | self, 63 | client: AsyncClient, 64 | ): 65 | data_atomic_request = { 66 | "atomic:operations": [ 67 | { 68 | "op": "remove", 69 | "data": { 70 | "id": "0", 71 | "type": "computer", 72 | }, 73 | }, 74 | ], 75 | } 76 | response = await client.post("/operations", json=data_atomic_request) 77 | assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY, response.text 78 | response_data = response.json() 79 | detail, *_ = response_data["detail"] 80 | assert detail["loc"] == ["body", "atomic:operations", 0] 81 | assert detail["msg"] == f"Value error, ref should be present for action {AtomicOperationAction.remove.value!r}" 82 | -------------------------------------------------------------------------------- /tests/test_atomic/test_response.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from fastapi_jsonapi.atomic.schemas import AtomicResultResponse 4 | 5 | 6 | class TestAtomicResultResponse: 7 | @pytest.mark.parametrize( 8 | "operation_response", 9 | [ 10 | { 11 | "atomic:results": [ 12 | { 13 | "data": { 14 | "links": { 15 | "self": "https://example.com/blogPosts/13", 16 | }, 17 | "type": "articles", 18 | "id": "13", 19 | "attributes": { 20 | "title": "JSON API paints my bikeshed!", 21 | }, 22 | }, 23 | }, 24 | ], 25 | }, 26 | { 27 | "atomic:results": [ 28 | { 29 | "data": { 30 | "links": { 31 | "self": "https://example.com/user/acb2ebd6-ed30-4877-80ce-52a14d77d470", 32 | }, 33 | "type": "users", 34 | "id": "acb2ebd6-ed30-4877-80ce-52a14d77d470", 35 | "attributes": {"name": "dgeb"}, 36 | }, 37 | }, 38 | { 39 | "data": { 40 | "links": { 41 | "self": "https://example.com/articles/bb3ad581-806f-4237-b748-f2ea0261845c", 42 | }, 43 | "type": "articles", 44 | "id": "bb3ad581-806f-4237-b748-f2ea0261845c", 45 | "attributes": { 46 | "title": "JSON API paints my bikeshed!", 47 | }, 48 | "relationships": { 49 | "user": { 50 | "links": { 51 | "self": "https://example.com/articles/bb3ad581-806f-4237-b748-f2ea0261845c/relationships/user", 52 | "related": "https://example.com/articles/bb3ad581-806f-4237-b748-f2ea0261845c/user", 53 | }, 54 | }, 55 | }, 56 | }, 57 | }, 58 | ], 59 | }, 60 | ], 61 | ) 62 | def test_response_data(self, operation_response: dict): 63 | validated = AtomicResultResponse.model_validate(operation_response) 64 | assert validated.model_dump(exclude_unset=True, by_alias=True) == operation_response 65 | -------------------------------------------------------------------------------- /tests/test_data_layers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mts-ai/FastAPI-JSONAPI/a1a4fc5a868ab6bb563919c462ea248250e83b26/tests/test_data_layers/__init__.py -------------------------------------------------------------------------------- /tests/test_data_layers/test_filtering/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mts-ai/FastAPI-JSONAPI/a1a4fc5a868ab6bb563919c462ea248250e83b26/tests/test_data_layers/test_filtering/__init__.py -------------------------------------------------------------------------------- /tests/test_data_layers/test_filtering/test_sqlalchemy.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | from unittest.mock import MagicMock, Mock 3 | 4 | import pytest 5 | from fastapi import status 6 | from pydantic import BaseModel, ConfigDict 7 | 8 | from fastapi_jsonapi.data_layers.sqla.query_building import build_filter_expression 9 | from fastapi_jsonapi.exceptions import InvalidType 10 | 11 | 12 | class TestFilteringFuncs: 13 | def test_user_type_cast_success(self): 14 | class UserType: 15 | def __init__(self, *args, **kwargs): 16 | """This method is needed to handle incoming arguments""" 17 | 18 | class ModelSchema(BaseModel): 19 | model_config = ConfigDict( 20 | arbitrary_types_allowed=True, 21 | ) 22 | 23 | value: UserType 24 | 25 | model_column_mock = MagicMock() 26 | 27 | build_filter_expression( 28 | schema_field=ModelSchema.model_fields["value"], 29 | model_column=model_column_mock, 30 | operator="__eq__", 31 | value=Any, 32 | ) 33 | 34 | model_column_mock.__eq__.assert_called_once() 35 | 36 | call_arg = model_column_mock.__eq__.call_args[0] 37 | isinstance(call_arg, UserType) 38 | 39 | def test_user_type_cast_fail(self): 40 | class UserType: 41 | def __init__(self, *args, **kwargs): 42 | msg = "Cast failed" 43 | raise ValueError(msg) 44 | 45 | class ModelSchema(BaseModel): 46 | model_config = ConfigDict( 47 | arbitrary_types_allowed=True, 48 | ) 49 | 50 | user_type: UserType 51 | 52 | with pytest.raises(InvalidType) as exc_info: 53 | build_filter_expression( 54 | schema_field=ModelSchema.model_fields["user_type"], 55 | model_column=Mock(), 56 | operator=Mock(), 57 | value=Any, 58 | ) 59 | 60 | assert exc_info.value.as_dict == { 61 | "detail": "Can't cast filter value `typing.Any` to arbitrary type.", 62 | "meta": [ 63 | { 64 | "detail": "Cast failed", 65 | "source": {"pointer": ""}, 66 | "status_code": status.HTTP_409_CONFLICT, 67 | "title": "Conflict", 68 | }, 69 | ], 70 | "status_code": status.HTTP_409_CONFLICT, 71 | "title": "Invalid type.", 72 | } 73 | -------------------------------------------------------------------------------- /tests/test_fastapi_jsonapi/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mts-ai/FastAPI-JSONAPI/a1a4fc5a868ab6bb563919c462ea248250e83b26/tests/test_fastapi_jsonapi/__init__.py -------------------------------------------------------------------------------- /tests/test_utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mts-ai/FastAPI-JSONAPI/a1a4fc5a868ab6bb563919c462ea248250e83b26/tests/test_utils/__init__.py -------------------------------------------------------------------------------- /tests/test_utils/test_dependency_helper.py: -------------------------------------------------------------------------------- 1 | from random import choices 2 | from string import ascii_letters 3 | from unittest.mock import AsyncMock 4 | 5 | from fastapi import Depends, Request 6 | 7 | from fastapi_jsonapi.utils.dependency_helper import DependencyHelper 8 | 9 | 10 | class TestDependencyHelper: 11 | async def test_dependency_helper(self): 12 | header_key = "".join(choices(ascii_letters, k=10)) 13 | header_value = "".join(choices(ascii_letters, k=12)) 14 | data_1 = object() 15 | data_2 = object() 16 | data_3 = object() 17 | 18 | def sub_dependency(): 19 | return data_1 20 | 21 | def some_dependency(sub_dep_1=Depends(sub_dependency)): 22 | return sub_dep_1 23 | 24 | async def some_async_dependency(): 25 | return data_3 26 | 27 | def some_function( 28 | req: Request, 29 | d1_as_dep=Depends(some_dependency), 30 | d3_as_dep=Depends(some_async_dependency), 31 | ): 32 | return d1_as_dep, data_2, d3_as_dep, req.headers.get(header_key) 33 | 34 | request = Request( 35 | { 36 | "type": "http", 37 | "path": "/foo/bar", 38 | "headers": [(header_key.lower().encode("latin-1"), header_value.encode("latin-1"))], 39 | "query_string": "", 40 | "fastapi_astack": AsyncMock(), 41 | }, 42 | ) 43 | # dirty 44 | request._body = b"" 45 | 46 | # prepare dependency helper 47 | dep_helper = DependencyHelper(request) 48 | # run a function with dependencies 49 | d1, d2, d3, h_value = await dep_helper.run(some_function) 50 | assert d1 is data_1 51 | assert d2 is data_2 52 | assert d3 is data_3 53 | assert h_value == header_value 54 | --------------------------------------------------------------------------------