pax_global_header00006660000000000000000000000064150240514540014512gustar00rootroot0000000000000052 comment=6a0005d1eeb9bb62316aa21b4c90c83e18051a17 strawberry-graphql-django-0.62.0/000077500000000000000000000000001502405145400166775ustar00rootroot00000000000000strawberry-graphql-django-0.62.0/.alexrc.yaml000066400000000000000000000002531502405145400211170ustar00rootroot00000000000000allow: - black - color - colors - execute - executed - executes - execution - failure - hook - hooks - invalid" - period - primitive - special strawberry-graphql-django-0.62.0/.coveragerc000066400000000000000000000005031502405145400210160ustar00rootroot00000000000000[run] source = strawberry_django_plus omit = .venv/**,examples/** [report] precision = 2 exclude_lines = pragma: nocover pragma:nocover pragma: no cover pragma:no cover if TYPE_CHECKING: @overload @abstractmethod @abc.abstractmethod assert_never omit = */migrations/* */tests/* strawberry-graphql-django-0.62.0/.github/000077500000000000000000000000001502405145400202375ustar00rootroot00000000000000strawberry-graphql-django-0.62.0/.github/actionlint-matcher.json000066400000000000000000000010351502405145400247160ustar00rootroot00000000000000{ "problemMatcher": [ { "owner": "actionlint", "pattern": [ { "regexp": "^(?:\\x1b\\[\\d+m)?(.+?)(?:\\x1b\\[\\d+m)*:(?:\\x1b\\[\\d+m)*(\\d+)(?:\\x1b\\[\\d+m)*:(?:\\x1b\\[\\d+m)*(\\d+)(?:\\x1b\\[\\d+m)*: (?:\\x1b\\[\\d+m)*(.+?)(?:\\x1b\\[\\d+m)* \\[(.+?)\\]$", "file": 1, "line": 2, "column": 3, "message": 4, "code": 5 } ] } ] } strawberry-graphql-django-0.62.0/.github/workflows/000077500000000000000000000000001502405145400222745ustar00rootroot00000000000000strawberry-graphql-django-0.62.0/.github/workflows/actionlint.yml000066400000000000000000000007661502405145400251740ustar00rootroot00000000000000--- name: Action Lint # yamllint disable-line rule:truthy on: pull_request: {} push: branches: - main jobs: actionlint: name: Check runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Check workflow files run: | echo "::add-matcher::.github/actionlint-matcher.json" bash <(curl https://raw.githubusercontent.com/rhysd/actionlint/main/scripts/download-actionlint.bash) ./actionlint -color shell: bash strawberry-graphql-django-0.62.0/.github/workflows/tests.yml000066400000000000000000000057311502405145400241670ustar00rootroot00000000000000--- name: Tests # yamllint disable-line rule:truthy on: push: branches: - main - v* pull_request: branches: - main - v* release: types: - released jobs: typing: name: Typing runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Install poetry run: pipx install poetry - name: Set up Python uses: actions/setup-python@v5 with: cache: poetry - name: Install Deps run: poetry install - run: echo "$(poetry env info --path)/bin" >> $"GITHUB_PATH" - id: venv run: echo "python-path=$(poetry env info --path)/bin/python3" >> "$GITHUB_OUTPUT" - name: Check for pyright errors uses: jakebailey/pyright-action@v2 with: python-path: ${{ steps.venv.outputs.python-path }} tests: runs-on: ubuntu-latest strategy: fail-fast: false matrix: django-version: - 4.2.* - 5.0.* - 5.1.* - 5.2.* python-version: - '3.9' - '3.10' - '3.11' - '3.12' - '3.13' mode: - std - geos include: # Django 4.2 only supports python 3.8-3.12 - django-version: 4.2.* python-version: '3.8' mode: std - django-version: 4.2.* python-version: '3.8' mode: geos exclude: # Django 4.2 only supports python 3.8-3.12 - django-version: 4.2.* python-version: '3.13' # Django 5.0 only supports python 3.10-3.12 - django-version: 5.0.* python-version: '3.9' - django-version: 5.0.* python-version: '3.13' # Django 5.1 only supports python 3.10+ - django-version: 5.1.* python-version: '3.9' # Django 5.2 only supports python 3.10+ - django-version: 5.2.* python-version: '3.9' steps: - name: Checkout uses: actions/checkout@v4 - name: Install OS Dependencies if: ${{ matrix.mode == 'geos' }} uses: daaku/gh-action-apt-install@v4 with: packages: binutils gdal-bin libproj-dev libsqlite3-mod-spatialite - name: Install Poetry run: pipx install poetry - name: Set up Python ${{ matrix.python-version }} id: setup-python uses: actions/setup-python@v5 with: cache: poetry python-version: ${{ matrix.python-version }} - name: Install Deps run: poetry install - name: Install Django ${{ matrix.django-version }} run: poetry run pip install "django==${{ matrix.django-version }}" - name: Test with pytest run: poetry run pytest --showlocals -vvv --cov-report=xml - name: Upload coverage to Codecov uses: codecov/codecov-action@v5 env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} strawberry-graphql-django-0.62.0/.gitignore000066400000000000000000000047021502405145400206720ustar00rootroot00000000000000# Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ share/python-wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .nox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover *.py,cover .hypothesis/ .pytest_cache/ cover/ .tmp_upload/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 db.sqlite3-journal # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder .pybuilder/ target/ # Jupyter Notebook .ipynb_checkpoints # IPython profile_default/ ipython_config.py # pyenv # For a library or package, you might want to ignore these files since the code is # intended to run in multiple environments; otherwise, check them in: # .python-version # pipenv # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. # However, in case of collaboration, if having platform-specific dependencies or dependencies # having no cross-platform support, pipenv may install dependencies that don't work, or not # install all needed dependencies. #Pipfile.lock # poetry # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. # This is especially recommended for binary packages to ensure reproducibility, and is more # commonly ignored for libraries. # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control #poetry.lock # PEP 582; used by e.g. github.com/David-OConnor/pyflow __pypackages__/ # intelejea .idea # Celery stuff celerybeat-schedule celerybeat.pid # SageMath parsed files *.sage.py # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Direnv config .envrc # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ .dmypy.json dmypy.json # Pyre type checker .pyre/ # pytype static type analyzer .pytype/ # Cython debug symbols cython_debug/ # Pyenv version .python-version # MacOS files .DS_Store strawberry-graphql-django-0.62.0/.pre-commit-config.yaml000066400000000000000000000016501502405145400231620ustar00rootroot00000000000000--- repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v5.0.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer exclude: snapshots - id: check-added-large-files args: - --maxkb=1024 - id: check-docstring-first - id: check-merge-conflict - id: check-yaml exclude: mkdocs.yml - id: check-toml - id: check-json - id: check-xml - id: check-symlinks - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.11.13 hooks: - id: ruff-format - id: ruff args: - --fix - repo: https://github.com/patrick91/pre-commit-alex rev: aa5da9e54b92ab7284feddeaf52edf14b1690de3 hooks: - id: alex exclude: CHANGELOG.md - repo: https://github.com/pre-commit/mirrors-prettier rev: v4.0.0-alpha.8 hooks: - id: prettier files: ^docs/.*\.mdx?$ strawberry-graphql-django-0.62.0/CHANGELOG.md000066400000000000000000000073471502405145400205230ustar00rootroot00000000000000# Changelog ## v0.3+ For newer versions, check the release notes at: https://github.com/strawberry-graphql/strawberry-django/releases ## v0.3rc1 This release adds couple of new features and contains few improvements. This also cleans deprecated API like `fields` parameter from `straberry.django.type`, `TypeRegister` and few other items which have been deprecated in v0.2.0. New features * add user registration mutation (#45, @NeoLight1010) * add permissions to django mutations (#53, @wellzenon) Improvements * fix detecting `auto` annotations when postponed evaluation is used (#73, @illia-v) * updated the way event loop is detected in `is_async` (#72, @g-as) * fix return type of field (#64) Fixes * fix a bug related to creating users with unhashed passwords (#62, @illia-v) * fix filtering in `get_queryset` of types with enabled pagination (#60, @illia-v) Cleanup * Clean deprecated API (#69) ## v0.2.5 This release adds support for latest `strawberry-graphql` Dependencies * update minimum required `strawberry-graphql` to v0.69.0 ## v0.2.4 Dependencies * Use `strawberry-graphql` earlier than v0.69, which introduced many backward incompatible changes. ## v0.2.3 Improvements * fix error moessage of NotImplementedError exception (@star2000) * remove pk argument on model relations (#40, @g-as) Fixes * fix breaking changes in `strawberry-graphql` (#42) Dependencies * update minimum required `strawberry-graphql` to v0.68.2 ## v0.2.2 New features * add `strawberry.django.mutation` type * add support for model properties and methods * add support for `strawberry.django` namespace Improvements * improve error message of FieldDoesNotExist exception, list all available fields * update README.md examples to use django namespace * integrate to `strawberry-graphql` v0.64.4 Dependencies * update minimum required `strawberry-graphql` to v0.64.4 ## v0.2.1 Fixes * fix relation and reverse relation field name resolution (#32) ## v0.2.0 This release adds new class oriented API where all fields are defined in class body. This release also adds basic support for filtering, ordering and pagination. See more information about new API from docs folder. Example above shows how the new API looks like. ```python from strawberry import auto import strawberry_django from . import models @strawberry_django.type(models.Color) class Color: name: auto @strawberry_django.type(models.Fruit) class Fruit: name: auto color: Color ``` Old API is deprecated and it will removed in v0.3. The biggest breaking change is `fields` parameter which is deprecated. `TypeRegister` is not used anymore in new API. Types and relationships are annotated and defined directly in class body of output and input types. ## v0.1.5 Bug fixes: * fix m2m relationship setting (#28) ## v0.1.4 Fix the AttributeError in the projects which do not have django-filter package installed. ## v0.1.3 Add support for django-filter. Now it is possible to convert FilterSet class to input type and apply filters to queryset following way. ```python @strawberry_django.filter class UserFilter(django_filters.FilterSet): class Meta: model = models.User fields = ["id", "name"] def resolver(filters: UserFilter): queryset = models.User.objects.all() return strawberry_django.filters.apply(filters, queryset) ``` ## v0.1.2 Changes: * rename `is_update` parameter to `partial` in `strawberry_django.input`. Old parameter`is_update` has been deprecated and it will be removed in v0.2. * update minimum supported `strawberry-graphql` version to v0.53 * fix example Django App * add LICENSE file * internal code cleanup ## v0.1.1 This release fixes * type and default value overriding (#14) * foreign key handling in partial update (#16) ## v0.1.0 First release strawberry-graphql-django-0.62.0/LICENSE000066400000000000000000000020571502405145400177100ustar00rootroot00000000000000MIT License Copyright (c) 2020 Lauri Hintsala Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. strawberry-graphql-django-0.62.0/Makefile000066400000000000000000000002331502405145400203350ustar00rootroot00000000000000.PHONY : install test lint POETRY := $(shell command -v poetry 2> /dev/null) all: install test install: ${POETRY} install test: ${POETRY} run pytest strawberry-graphql-django-0.62.0/README.md000066400000000000000000000064301502405145400201610ustar00rootroot00000000000000# Strawberry GraphQL Django integration [![CI](https://github.com/strawberry-graphql/strawberry-django/actions/workflows/tests.yml/badge.svg)](https://github.com/strawberry-graphql/strawberry-django/actions/workflows/tests.yml) [![Coverage](https://codecov.io/gh/strawberry-graphql/strawberry-django/branch/main/graph/badge.svg?token=JNH6PUYh3e)](https://codecov.io/gh/strawberry-graphql/strawberry-django) [![PyPI](https://img.shields.io/pypi/v/strawberry-graphql-django)](https://pypi.org/project/strawberry-graphql-django/) [![Downloads](https://pepy.tech/badge/strawberry-graphql-django)](https://pepy.tech/project/strawberry-graphql-django) ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/strawberry-graphql-django) [**Docs**](https://strawberry.rocks/docs/django) | [**Discord**](https://strawberry.rocks/discord) This package provides powerful tools to generate GraphQL types, queries, mutations and resolvers from Django models. Installing `strawberry-graphql-django` package from the python package repository. ```shell pip install strawberry-graphql-django ``` ## Supported Features - [x] GraphQL type generation from models - [x] Filtering, pagination and ordering - [x] Basic create, retrieve, update and delete (CRUD) types and mutations - [x] Basic Django auth support, current user query, login and logout mutations - [x] Django sync and async views - [x] Permission extension using django's permissioning system - [x] Relay support with automatic resolvers generation - [x] Query optimization to improve performance and avoid common pitfalls (e.g n+1) - [x] Debug Toolbar integration with graphiql to display metrics like SQL queries - [x] Unit test integration ## Basic Usage ```python # models.py from django.db import models class Fruit(models.Model): """A tasty treat""" name = models.CharField( max_length=20, ) color = models.ForeignKey( "Color", on_delete=models.CASCADE, related_name="fruits", blank=True, null=True, ) class Color(models.Model): name = models.CharField( max_length=20, help_text="field description", ) ``` ```python # types.py import strawberry_django from strawberry import auto from . import models @strawberry_django.type(models.Fruit) class Fruit: id: auto name: auto color: 'Color' @strawberry_django.type(models.Color) class Color: id: auto name: auto fruits: list[Fruit] ``` ```python # schema.py import strawberry import strawberry_django from strawberry_django.optimizer import DjangoOptimizerExtension from .types import Fruit @strawberry.type class Query: fruits: list[Fruit] = strawberry_django.field() schema = strawberry.Schema( query=Query, extensions=[ DjangoOptimizerExtension, # not required, but highly recommended ], ) ``` ```python # urls.py from django.urls import include, path from strawberry.django.views import AsyncGraphQLView from .schema import schema urlpatterns = [ path('graphql', AsyncGraphQLView.as_view(schema=schema)), ] ``` Code above generates following schema. ```graphql """ A tasty treat """ type Fruit { id: ID! name: String! color: Color } type Color { id: ID! """ field description """ name: String! fruits: [Fruit!] } type Query { fruits: [Fruit!]! } ``` strawberry-graphql-django-0.62.0/docs/000077500000000000000000000000001502405145400176275ustar00rootroot00000000000000strawberry-graphql-django-0.62.0/docs/README.md000066400000000000000000000017731502405145400211160ustar00rootroot00000000000000--- title: Strawberry Django docs --- # Strawberry Django docs ## Docs - [Getting Started](./index.md) ## General - [Types](./guide/types.md) - [Fields](./guide/fields.md) - [Views](./guide/views.md) - [Filters](./guide/filters.md) - [Ordering](./guide/ordering.md) - [Pagination](./guide/pagination.md) - [Queries](./guide/queries.md) - [Mutations](./guide/mutations.md) - [Subscriptions](./guide/subscriptions.md) - [Settings](./guide/settings.md) - [FAQ](./faq.md) ## Guide - [Query Optimizer](./guide/optimizer.md) - [Permissions](./guide/permissions.md) - [Relay](./guide/relay.md) - [Authentication](./guide/authentication.md) - [Export Schema](./guide/export-schema.md) - [Resolvers](./guide/resolvers.md) - [Unit Testing](./guide/unit-testing.md) ## Integrations - [Debug Toolbar](./integrations/debug-toolbar.md) - [Channels](./integrations/channels.md) - [Choices Field](./integrations/choices-field.md) - [Django Guardian](./integrations/guardian.md) - [Community Projects](./community-projects.md) strawberry-graphql-django-0.62.0/docs/community-projects.md000066400000000000000000000017721502405145400240330ustar00rootroot00000000000000--- title: Community Projects --- # Community Projects Those are some community maintained projects worth mentioning: | Project | Description | | :-------------------------------------------------------------------------------: | :--------------------------------------------------------------------------------------------------: | | [🐙 strawberry-django-auth](https://github.com/nrbnlulu/strawberry-django-auth) | Authentication System for Django using Strawberry. | | [🐙 strawberry-django-extras](https://github.com/m4riok/strawberry-django-extras) | JWT Authentication, Input validation and permissions, mutation hooks and deeply nested CUD mutations | If you want your integration to be listed here, send us a [Pull Request](https://github.com/strawberry-graphql/strawberry-django/pulls) strawberry-graphql-django-0.62.0/docs/faq.md000066400000000000000000000020071502405145400207170ustar00rootroot00000000000000--- title: Frequently Asked Questions --- # Frequently Asked Questions (FAQ) ## How to access Django request object in resolvers? The request object is accessible via the `get_request` method. ```python from strawberry_django.utils.requests import get_request def resolver(root, info: Info): request = get_request(info) ``` ## How to access the current user object in resolvers? The current user object is accessible via the `get_current_user` method. ```python from strawberry_django.auth.utils import get_current_user def resolver(root, info: Info): current_user = get_current_user(info) ``` ## Autocompletion with editors Some editors like VSCode may not be able to resolve symbols and types without explicit `strawberry.django` import. Adding following line to code fixes that problem. ```python import strawberry.django ``` ## Example project? See complete Django project from github repository folder [examples/django](https://github.com/strawberry-graphql/strawberry-django/tree/main/examples/django). strawberry-graphql-django-0.62.0/docs/guide/000077500000000000000000000000001502405145400207245ustar00rootroot00000000000000strawberry-graphql-django-0.62.0/docs/guide/authentication.md000066400000000000000000000023641502405145400242720ustar00rootroot00000000000000--- title: Authentication --- # Authentication > [!WARNING] > This solution is enough for web browsers, but will not work for clients that > doesn't have a way to store cookies in it (e.g. mobile apps). For those it is > recommended to use token authentication methods. JWT can be used with > [strawberry-django-auth](https://github.com/nrbnlulu/strawberry-django-auth) > lib. `strawberry_django` provides mutations to get authentication going right away. The `auth.register` mutation performs password validation using Django's `validate_password` method. ```python title="types.py" import strawberry_django from strawberry import auto from django.contrib.auth import get_user_model @strawberry_django.type(get_user_model()) class User: username: auto email: auto @strawberry_django.input(get_user_model()) class UserInput: username: auto password: auto ``` ```python title="schema.py" import strawberry import strawberry_django from .types import User, UserInput @strawberry.type class Query: me: User = strawberry_django.auth.current_user() @strawberry.type class Mutation: login: User = strawberry_django.auth.login() logout = strawberry_django.auth.logout() register: User = strawberry_django.auth.register(UserInput) ``` strawberry-graphql-django-0.62.0/docs/guide/export-schema.md000066400000000000000000000033711502405145400240310ustar00rootroot00000000000000--- title: Export Schema --- # Export Schema > [!INFO] > The `export_schema` management command provided here is specifically designed for use with `strawberry_django`. The [default Strawberry export command](https://strawberry.rocks/docs/guides/schema-export) won't work with `strawberry_django` schemas because `strawberry_django` extends the base functionality of Strawberry to integrate with Django models and queries. This command ensures proper schema export functionality. The `export_schema` management command allows you to export a GraphQL schema defined using the `strawberry_django` library. This command converts the schema definition to GraphQL schema definition language (SDL), which can then be saved to a file or printed to the console. ## Usage To use the `export_schema` command, you need to specify the schema location(e.g., myapp.schema). Optionally, you can provide a file path to save the schema. If no path is provided, the schema will be printed to the console. ```sh python manage.py export_schema --path ``` ### Arguments - ``: The location of the schema module. This should be a dot-separated Python path (e.g., myapp.schema). For example, if your schema is located in the `schemas` directory in the `myapp` django app, you would use `myapp.schemas`. ### Options - `--path `: An optional argument specifying the file path where the schema should be saved. If not provided, the schema will be printed to standard output. ## Example Here's an example of how to use the export_schema command: ```sh python manage.py export_schema myapp.schema --path=output/schema.graphql ``` In this example, the schema located at `myapp.schema` will be exported to the file `output/schema.graphql`. strawberry-graphql-django-0.62.0/docs/guide/fields.md000066400000000000000000000137761502405145400225320ustar00rootroot00000000000000--- title: Defining Fields --- # Defining Fields > [!TIP] > It is highly recommended to enable the [Query Optimizer Extension](optimizer.md) > for improved performance and avoid some common pitfalls (e.g. the `n+1` issue) Fields can be defined manually or `auto` type can be used for automatic type resolution. All basic field types and relation fields are supported out of the box. If you use a library that defines a custom field you will need to define an equivalent type such as `str`, `float`, `bool`, `int` or `id`. ```python title="types.py" import strawberry_django from strawberry import auto @strawberry_django.type(models.Fruit) class Fruit: id: auto name: auto # equivalent type, inferred by `strawberry` @strawberry_django.type(models.Fruit) class Fruit2: id: strawberry.ID name: str ``` > [!TIP] > For choices using > [Django's TextChoices/IntegerChoices](https://docs.djangoproject.com/en/4.2/ref/models/fields/#enumeration-types) > it is recommented using the [django-choices-field](../integrations/choices-field.md) integration > enum handling. ## Relationships All one-to-one, one-to-many, many-to-one and many-to-many relationship types are supported, and the many-to-many relation is described using the `typing.List` annotation. The default resolver of `strawberry_django.fields()` resolves the relationship based on given type information. ```python title="types.py" @strawberry_django.type(models.Fruit) class Fruit: id: auto name: auto color: "Color" @strawberry_django.type(models.Color) class Color: id: auto name: auto fruits: list[Fruit] ``` Note that all relations can naturally trigger the n+1 problem. To avoid that, you can either enable the [Optimizer Extension](./optimizer.md) which will automatically solve some general issues for you, or even use [Data Loaders](https://strawberry.rocks/docs/guides/dataloaders) for more complex situations. ## Field customization All Django types are encoded using the `strawberry_django.field()` field type by default. Fields can be customized with various parameters. ```python title="types.py" @strawberry_django.type(models.Color) class Color: another_name: auto = strawberry_django.field(field_name='name') internal_name: auto = strawberry_django.field( name='fruits', field_name='fruit_set', filters=FruitFilter, order=FruitOrder, pagination=True, description="A list of fruits with this color" ) ``` ## Defining types for auto fields When using `strawberry.auto` to resolve a field's type, Strawberry Django uses a dict that maps each django field field type to its proper type. e.g.: ```python { models.CharField: str, models.IntegerField: int, ..., } ``` If you are using a custom django field that is not part of the default library, or you want to use a different type for a field, you can do that by overriding its value in the map, like: ```python from typing import NewType from django.db import models import strawberry import strawberry_django from strawberry_django.fields.types import field_type_map Slug = strawberry.scalar( NewType("Slug", str), serialize=lambda v: v, parse_value=lambda v: v, ) @strawberry_django.type class MyCustomFileType: ... field_type_map.update({ models.SlugField: Slug, models.FileField: MyCustomFileType, }) ``` ## Including / excluding Django model fields by name > [!WARNING] > These new keywords should be used with caution, as they may inadvertently lead to exposure of unwanted data. Especially with `fields="__all__"` or `exclude`, sensitive model attributes may be included and made available in the schema without your awareness. `strawberry_django.type` includes two optional keyword fields to help you populate fields from the Django model, `fields` and `exclude`. Valid values for `fields` are: - `__all__` to assign `strawberry.auto` as the field type for all model fields. - `[]` to assign `strawberry.auto` as the field type for the enumerated fields. These can be combined with manual type annotations if needed. ```python title="All Fields" @strawberry_django.type(models.Fruit, fields="__all__") class FruitType: pass ``` ```python title="Enumerated Fields" @strawberry_django.type(models.Fruit, fields=["name", "color"]) class FruitType: pass ``` ```python title="Overriden Fields" @strawberry_django.type(models.Fruit, fields=["color"]) class FruitType: name: str ``` Valid values for `exclude` are: - `[]` to exclude from the fields list. All other Django model fields will included and have `strawberry.auto` as the field type. These can also be overriden if another field type should be assigned. An empty list is ignored. ```python title="Exclude Fields" @strawberry_django.type(models.Fruit, exclude=["name"]) class FruitType: pass ``` ```python title="Overriden Exclude Fields" @strawberry_django.type(models.Fruit, exclude=["name"]) class FruitType: color: int ``` Note that `fields` has precedence over `exclude`, so if both are provided, then `exclude` is ignored. ## Overriding the field class (advanced) If in your project, you want to change/add some of the standard `strawberry_django.field()` behaviour, it is possible to use your own custom field class when decorating a `strawberry_django.type` with the `field_cls` argument, e.g. ```python title="types.py" class CustomStrawberryDjangoField(StrawberryDjangoField): """Your custom behaviour goes here.""" @strawberry_django.type(User, field_cls=CustomStrawberryDjangoField) class UserType: # Each of these fields will be an instance of `CustomStrawberryDjangoField`. id: int name: auto @strawberry.type class UserQuery: # You can directly create your custom field class on a plain strawberry type user: UserType = CustomStrawberryDjangoField() ``` In this example, each of the fields of the `UserType` will be automatically created by `CustomStrawberryDjangoField`, which may implement anything from custom pagination of relationships to altering the field permissions. strawberry-graphql-django-0.62.0/docs/guide/filters.md000066400000000000000000000310151502405145400227160ustar00rootroot00000000000000--- title: Filtering --- # Filtering It is possible to define filters for Django types, which will be converted into `.filter(...)` queries for the ORM: ```python title="types.py" import strawberry_django from strawberry import auto @strawberry_django.filter_type(models.Fruit) class FruitFilter: id: auto name: auto @strawberry_django.type(models.Fruit, filters=FruitFilter) class Fruit: ... ``` > [!TIP] > In most cases filter fields should have `Optional` annotations and default value `strawberry.UNSET` like so: > `foo: Optional[SomeType] = strawberry.UNSET` > Above `auto` annotation is wrapped in `Optional` automatically. > `UNSET` is automatically used for fields without `field` or with `strawberry_django.filter_field`. The code above would generate following schema: ```graphql title="schema.graphql" input FruitFilter { id: ID name: String AND: FruitFilter OR: FruitFilter NOT: FruitFilter DISTINCT: Boolean } ``` > [!TIP] > If you are using the [relay integration](relay.md) and working with types inheriting > from `relay.Node` and `GlobalID` for identifying objects, you might want to set > `MAP_AUTO_ID_AS_GLOBAL_ID=True` in your [strawberry django settings](./settings.md) > to make sure `auto` fields gets mapped to `GlobalID` on types and filters. ## AND, OR, NOT, DISTINCT ... To every filter `AND`, `OR`, `NOT` & `DISTINCT` fields are added to allow more complex filtering ```graphql { fruits( filters: { name: "kebab" OR: { name: "raspberry" } } ) { ... } } ``` ## List-based AND/OR/NOT Filters The `AND`, `OR`, and `NOT` operators can also be declared as lists, allowing for more complex combinations of conditions. This is particularly useful when you need to combine multiple conditions in a single operation. ```python title="types.py" @strawberry_django.filter_type(models.Vegetable, lookups=True) class VegetableFilter: id: auto name: auto AND: Optional[list[Self]] = strawberry.UNSET OR: Optional[list[Self]] = strawberry.UNSET NOT: Optional[list[Self]] = strawberry.UNSET ``` This enables queries like: ```graphql { vegetables( filters: { AND: [{ name: { contains: "blue" } }, { name: { contains: "squash" } }] } ) { id } } ``` The list-based filtering system differs from the single object filter in a few ways: 1. It allows combining multiple conditions in a single `AND`, `OR`, or `NOT` operation 2. The conditions in a list are evaluated together as a group 3. When using `AND`, all conditions in the list must be satisfied 4. When using `OR`, any condition in the list can be satisfied 5. When using `NOT`, none of the conditions in the list should be satisfied This is particularly useful for complex queries where you need to have multiple conditions against the same field. ## Lookups Lookups can be added to all fields with `lookups=True`, which will add more options to resolve each type. For example: ```python title="types.py" @strawberry_django.filter_type(models.Fruit, lookups=True) class FruitFilter: id: auto name: auto ``` The code above would generate the following schema: ```graphql title="schema.graphql" input IDBaseFilterLookup { exact: ID isNull: Boolean inList: [String!] } input StrFilterLookup { exact: ID isNull: Boolean inList: [String!] iExact: String contains: String iContains: String startsWith: String iStartsWith: String endsWith: String iEndsWith: String regex: String iRegex: String } input FruitFilter { id: IDFilterLookup name: StrFilterLookup AND: FruitFilter OR: FruitFilter NOT: FruitFilter DISTINCT: Boolean } ``` Single-field lookup can be annotated with the `FilterLookup` generic type. ```python title="types.py" from strawberry_django import FilterLookup @strawberry_django.filter(models.Fruit) class FruitFilter: name: FilterLookup[str] ``` ## Filtering over relationships ```python title="types.py" @strawberry_django.filter(models.Color) class ColorFilter: id: auto name: auto @strawberry_django.filter(models.Fruit) class FruitFilter: id: auto name: auto color: ColorFilter | None ``` The code above would generate following schema: ```graphql title="schema.graphql" input ColorFilter { id: ID name: String AND: ColorFilter OR: ColorFilter NOT: ColorFilter } input FruitFilter { id: ID name: String color: ColorFilter AND: FruitFilter OR: FruitFilter NOT: FruitFilter } ``` ## Custom filter methods You can define custom filter method by defining your own resolver. ```python title="types.py" @strawberry_django.filter(models.Fruit) class FruitFilter: name: auto last_name: auto @strawberry_django.filter_field def simple(self, value: str, prefix) -> Q: return Q(**{f"{prefix}name": value}) @strawberry_django.filter_field def full_name( self, queryset: QuerySet, value: str, prefix: str ) -> tuple[QuerySet, Q]: queryset = queryset.alias( _fullname=Concat( f"{prefix}name", Value(" "), f"{prefix}last_name" ) ) return queryset, Q(**{"_fullname": value}) @strawberry_django.filter_field def full_name_lookups( self, info: Info, queryset: QuerySet, value: strawberry_django.FilterLookup[str], prefix: str ) -> tuple[QuerySet, Q]: queryset = queryset.alias( _fullname=Concat( f"{prefix}name", Value(" "), f"{prefix}last_name" ) ) return strawberry_django.process_filters( filters=value, queryset=queryset, info=info, prefix=f"{prefix}_fullname" ) ``` > [!WARNING] > It is discouraged to use `queryset.filter()` directly. When using more > complex filtering via `NOT`, `OR` & `AND` this might lead to undesired behaviour. > [!TIP] > > #### process_filters > > As seen above `strawberry_django.process_filters` function is exposed and can be > reused in custom methods. Above it's used to resolve fields lookups > > #### null values > > By default `null` value is ignored for all filters & lookups. This applies to custom > filter methods as well. Those won't even be called (you don't have to check for `None`). > This can be modified using > `strawberry_django.filter_field(filter_none=True)` > > This also means that built in `exact` & `iExact` lookups cannot be used to filter for `None` > and `isNull` have to be used explicitly. > > #### value resolution > > - `value` parameter of type `relay.GlobalID` is resolved to its `node_id` attribute > - `value` parameter of type `Enum` is resolved to is's value > - above types are converted in `lists` as well > > resolution can modified via `strawberry_django.filter_field(resolve_value=...)` > > - True - always resolve > - False - never resolve > - UNSET (default) - resolves for filters without custom method only The code above generates the following schema: ```graphql title="schema.graphql" input FruitFilter { name: String lastName: String simple: str fullName: str fullNameLookups: StrFilterLookup } ``` #### Resolver arguments - `prefix` - represents the current path or position - **Required** - Important for nested filtering - In code bellow custom filter `name` ends up filtering `Fruit` instead of `Color` without applying `prefix` ```python title="Why prefix?" @strawberry_django.filter(models.Fruit) class FruitFilter: name: auto color: ColorFilter | None @strawberry_django.filter(models.Color) class ColorFilter: @strawberry_django.filter_field def name(self, value: str, prefix: str): # prefix is "fruit_set__" if unused root object is filtered instead if value: return Q(name=value) return Q() ``` ```graphql { fruits( filters: {color: name: "blue"} ) { ... } } ``` - `value` - represents graphql field type - **Required**, but forbidden for default `filter` method - _must_ be annotated - used instead of field's return type - `queryset` - can be used for more complex filtering - Optional, but **Required** for default `filter` method - usually used to `annotate` `QuerySet` #### Resolver return For custom field methods two return values are supported - django's `Q` object - tuple with `QuerySet` and django's `Q` object -> `tuple[QuerySet, Q]` For default `filter` method only second variant is supported. ### What about nulls? By default `null` values are ignored. This can be toggled as such `@strawberry_django.filter_field(filter_none=True)` ## Overriding the default `filter` method Works similar to field filter method, but: - is responsible for resolution of filtering for entire object - _must_ be named `filter` - argument `queryset` is **Required** - argument `value` is **Forbidden** ```python title="types.py" @strawberry_django.filter(models.Fruit) class FruitFilter: def ordered( self, value: int, prefix: str, queryset: QuerySet, ): queryset = queryset.alias( _ordered_num=Count(f"{prefix}orders__id") ) return queryset, Q(**{f"{prefix}_ordered_num": value}) @strawberry_django.order_field def filter( self, info: Info, queryset: QuerySet, prefix: str, ) -> tuple[QuerySet, list[Q]]: queryset = queryset.filter( ... # Do some query modification ) return strawberry_django.process_filters( self, info=info, queryset=queryset, prefix=prefix, skip_object_order_method=True ) ``` > [!TIP] > As seen above `strawberry_django.process_filters` function is exposed and can be > reused in custom methods. > For filter method `filter` `skip_object_order_method` was used to avoid endless recursion. ## Adding filters to types All fields and CUD mutations inherit filters from the underlying type by default. So, if you have a field like this: ```python title="types.py" @strawberry_django.type(models.Fruit, filters=FruitFilter) class Fruit: ... @strawberry.type class Query: fruits: list[Fruit] = strawberry_django.field() ``` The `fruits` field will inherit the `filters` of the type in the same way as if it was passed to the field. ## Adding filters directly into a field Filters added into a field override the default filters of this type. ```python title="schema.py" @strawberry.type class Query: fruits: list[Fruit] = strawberry_django.field(filters=FruitFilter) ``` ## Generic Lookup reference There is 7 already defined Generic Lookup `strawberry.input` classes importable from `strawberry_django` #### `BaseFilterLookup` - contains `exact`, `isNull` & `inList` - used for `ID` & `bool` fields #### `RangeLookup` - used for `range` or `BETWEEN` filtering #### `ComparisonFilterLookup` - inherits `BaseFilterLookup` - additionaly contains `gt`, `gte`, `lt`, `lte`, & `range` - used for Numberical fields #### `FilterLookup` - inherits `BaseFilterLookup` - additionally contains `iExact`, `contains`, `iContains`, `startsWith`, `iStartsWith`, `endsWith`, `iEndsWith`, `regex` & `iRegex` - used for string based fields and as default #### `DateFilterLookup` - inherits `ComparisonFilterLookup` - additionally contains `year`,`month`,`day`,`weekDay`,`isoWeekDay`,`week`,`isoYear` & `quarter` - used for date based fields #### `TimeFilterLookup` - inherits `ComparisonFilterLookup` - additionally contains `hour`,`minute`,`second`,`date` & `time` - used for time based fields #### `DatetimeFilterLookup` - inherits `DateFilterLookup` & `TimeFilterLookup` - used for timedate based fields ## Legacy filtering The previous version of filters can be enabled via [**USE_DEPRECATED_FILTERS**](settings.md#strawberry_django) > [!WARNING] > If **USE_DEPRECATED_FILTERS** is not set to `True` legacy custom filtering > methods will be _not_ be called. When using legacy filters it is important to use legacy `strawberry_django.filters.FilterLookup` lookups as well. The correct version is applied for `auto` annotated filter field (given `lookups=True` being set). Mixing old and new lookups might lead to error `DuplicatedTypeName: Type StrFilterLookup is defined multiple times in the schema`. While legacy filtering is enabled new filtering custom methods are fully functional including default `filter` method. Migration process could be composed of these steps: - enable **USE_DEPRECATED_FILTERS** - gradually transform custom filter field methods to new version (do not forget to use old FilterLookup if applicable) - gradually transform default `filter` methods - disable **USE_DEPRECATED_FILTERS** - **_This is breaking change_** strawberry-graphql-django-0.62.0/docs/guide/legacy-ordering.md000066400000000000000000000154541502405145400243320ustar00rootroot00000000000000--- title: Ordering --- > [!WARNING] > The legacy `@strawberry_django.order` implementation is only provided for backwards compatibility. > You should prefer [Ordering](ordering) instead. # Order (Legacy) `@strawberry_django.order` allows sorting by multiple fields only by specifying the object keys in the order input in the desired order. This is not always feasible and contradicts the way objects are supposed to be used. ```python title="types.py" @strawberry_django.order(models.Color) class ColorOrder: name: auto @strawberry_django.order(models.Fruit) class FruitOrder: name: auto color: ColorOrder | None ``` > [!TIP] > In most cases order fields should have `Optional` annotations and default value `strawberry.UNSET`. > Above `auto` annotation is wrapped in `Optional` automatically. > `UNSET` is automatically used for fields without `field` or with `strawberry_django.order_field`. The code above generates the following schema: ```graphql title="schema.graphql" enum Ordering { ASC ASC_NULLS_FIRST ASC_NULLS_LAST DESC DESC_NULLS_FIRST DESC_NULLS_LAST } input ColorOrder { name: Ordering } input FruitOrder { name: Ordering color: ColorOrder } ``` ## Custom order methods You can define custom order method by defining your own resolver. ```python title="types.py" @strawberry_django.order(models.Fruit) class FruitOrder: name: auto @strawberry_django.order_field def discovered_by(self, value: bool, prefix: str) -> list[str]: if not value: return [] return [f"{prefix}discover_by__name", f"{prefix}name"] @strawberry_django.order_field def order_number( self, info: Info, queryset: QuerySet, value: strawberry_django.Ordering, # `auto` can be used instead prefix: str, sequence: dict[str, strawberry_django.Ordering] | None ) -> tuple[QuerySet, list[str]] | list[str]: queryset = queryset.alias( _ordered_num=Count(f"{prefix}orders__id") ) ordering = value.resolve(f"{prefix}_ordered_num") return queryset, [ordering] ``` > [!WARNING] > Do not use `queryset.order_by()` directly. Due to `order_by` not being chainable > operation, changes applied this way would be overriden later. > [!TIP] > The `strawberry_django.Ordering` type has convenient method `resolve` that can be used to > convert field's name to appropriate `F` object with correctly applied `asc()`, `desc()` method > with `nulls_first` and `nulls_last` arguments. The code above generates the following schema: ```graphql title="schema.graphql" enum Ordering { ASC ASC_NULLS_FIRST ASC_NULLS_LAST DESC DESC_NULLS_FIRST DESC_NULLS_LAST } input FruitOrder { name: Ordering discoveredBy: bool orderNumber: Ordering } ``` #### Resolver arguments - `prefix` - represents the current path or position - **Required** - Important for nested ordering - In code bellow custom order `name` ends up ordering `Fruit` instead of `Color` without applying `prefix` ```python title="Why prefix?" @strawberry_django.order(models.Fruit) class FruitOrder: name: auto color: ColorOrder | None @strawberry_django.order(models.Color) class ColorOrder: @strawberry_django.order_field def name(self, value: bool, prefix: str): # prefix is "fruit_set__" if unused root object is ordered instead if value: return ["name"] return [] ``` ```graphql { fruits( order: {color: name: ASC} ) { ... } } ``` - `value` - represents graphql field type - **Required**, but forbidden for default `order` method - _must_ be annotated - used instead of field's return type - Using `auto` is the same as `strawberry_django.Ordering`. - `queryset` - can be used for more complex ordering - Optional, but **Required** for default `order` method - usually used to `annotate` `QuerySet` - `sequence` - used to order values on the same level - elements in graphql object are not quaranteed to keep their order as defined by user thus this argument should be used in those cases [GraphQL Spec](https://spec.graphql.org/October2021/#sec-Language.Arguments) - usually for custom order field methods does not have to be used - for advanced usage, look at `strawberry_django.process_order` function #### Resolver return For custom field methods two return values are supported - iterable of values acceptable by `QuerySet.order_by` -> `Collection[F | str]` - tuple with `QuerySet` and iterable of values acceptable by `QuerySet.order_by` -> `tuple[QuerySet, Collection[F | str]]` For default `order` method only second variant is supported. ### What about nulls? By default `null` values are ignored. This can be toggled as such `@strawberry_django.order_field(order_none=True)` ## Overriding the default `order` method Works similar to field order method, but: - is responsible for resolution of ordering for entire object - _must_ be named `order` - argument `queryset` is **Required** - argument `value` is **Forbidden** - should probaly use `sequence` ```python title="types.py" @strawberry_django.order(models.Fruit) class FruitOrder: name: auto @strawberry_django.order_field def ordered( self, info: Info, queryset: QuerySet, value: strawberry_django.Ordering, prefix: str ) -> tuple[QuerySet, list[str]] | list[str]: queryset = queryset.alias( _ordered_num=Count(f"{prefix}orders__id") ) return queryset, [value.resolve(f"{prefix}_ordered_num") ] @strawberry_django.order_field def order( self, info: Info, queryset: QuerySet, prefix: str, sequence: dict[str, strawberry_django.Ordering] | None ) -> tuple[QuerySet, list[str]]: queryset = queryset.filter( ... # Do some query modification ) return strawberry_django.process_order( self, info=info, queryset=queryset, sequence=sequence, prefix=prefix, skip_object_order_method=True ) ``` > [!TIP] > As seen above `strawberry_django.process_order` function is exposed and can be > reused in custom methods. > For order method `order` `skip_object_order_method` was used to avoid endless recursion. ## Adding orderings to types All fields and mutations inherit orderings from the underlying type by default. So, if you have a field like this: ```python title="types.py" @strawberry_django.type(models.Fruit, order=FruitOrder) class Fruit: ... ``` The `fruits` field will inherit the `order` of the type same same way as if it was passed to the field. ## Adding orderings directly into a field Orderings added into a field override the default order of this type. ```python title="schema.py" @strawberry.type class Query: fruit: Fruit = strawberry_django.field(order=FruitOrder) ``` strawberry-graphql-django-0.62.0/docs/guide/mutations.md000066400000000000000000000143601502405145400232750ustar00rootroot00000000000000--- title: Mutations --- # Mutations ## Getting started Mutations can be defined the same way as [strawberry's mutations](https://strawberry.rocks/docs/general/mutations), but instead of using `@strawberry.mutation`, use `@strawberry_django.mutation`. Here are the differences between those: - Strawberry Django's mutation will be sure that the mutation is executed in an async safe environment, meaning that if you are running ASGI and you define a `sync` resolver, it will automatically be wrapped in a `sync_to_async` call. - It will better integrate with the [permission integration](./permissions.md) - It has an option to automatically handle common django errors and return them in a standardized way (more on that below) ## Django errors handling When defining a mutation you can pass `handle_django_errors=True` to make it handle common django errors, such as `ValidationError`, `PermissionDenied` and `ObjectDoesNotExist`: ```python title="types.py" @strawberry.type class Mutation: @strawberry_django.mutation(handle_django_errors=True) def create_fruit(self, name: str, color: str) -> Fruit: if not is_valid_color(color): raise ValidationError("The color is not valid") # Creation can also raise ValidationError, if the `name` is # larger than its allowed `max_length` for example. fruit = models.Fruit.objects.create(name=name) return cast(Fruit, fruit) ``` The code above would generate following schema: ```graphql title="schema.graphql" enum OperationMessageKind { INFO WARNING ERROR PERMISSION VALIDATION } type OperationInfo { """List of messages returned by the operation.""" messages: [OperationMessage!]! } type OperationMessage { """The kind of this message.""" kind: OperationMessageKind! """The error message.""" message: String! """ The field that caused the error, or `null` if it isn't associated with any particular field. """ field: String """The error code, or `null` if no error code was set.""" code: String } type Fruit { name: String! color: String! } union CreateFruitPayload = Fruit | OperationInfo mutation { createFruit( name: String! color: String! ): CreateFruitPayload! } ``` > [!TIP] > If all or most of your mutations use this behaviour, you can change the > default behaviour for `handle_django_errors` by setting > `MUTATIONS_DEFAULT_HANDLE_ERRORS=True` in your [strawberry django settings](./settings.md) ## Input mutations Those are defined using `@strawberry_django.input_mutation` and act the same way as the `@strawberry_django.mutation`, the only difference being that it injects an [InputMutationExtension](https://strawberry.rocks/docs/general/mutations#the-input-mutation-extension) in the field, which converts its arguments in a new type (check the extension's docs for more information). ## CUD mutations The following CUD mutations are provided by this lib: - `strawberry_django.mutations.create`: Will create the model using the data from the given input - `strawberry_django.mutations.update`: Will update the model using the data from the given input - `strawberry_django.mutations.delete`: Will delete the model using the id from the given input A basic example would be: ```python title="types.py" from strawberry import auto from strawberry_django import mutations, NodeInput from strawberry.relay import Node @strawberry_django.type(SomeModel) class SomeModelType(Node): name: auto @strawberry_django.input(SomeModel) class SomeModelInput: name: auto @strawberry_django.partial(SomeModel) class SomeModelInputPartial(NodeInput): name: auto @strawberry.type class Mutation: create_model: SomeModelType = mutations.create(SomeModelInput) update_model: SomeModelType = mutations.update(SomeModelInputPartial) delete_model: SomeModelType = mutations.delete(NodeInput) ``` Some things to note here: - Those CUD mutations accept the same arguments as `@strawberry_django.mutation` accepts. This allows you to pass `handle_django_errors=True` to it for example. - The mutation will receive the type in an argument named `"data"` by default. To change it to `"info"` for example, you can change it by passing `argument_name="info"` to the mutation, or set `MUTATIONS_DEFAULT_ARGUMENT_NAME="info"` in your [strawberry django settings](./settings.md) to make it the default when not provided. - Take note that inputs using `partial` will _not_ automatically mark non-auto fields optional and instead will respect explicit type annotations; see [partial input types](./types.md#input-types) documentation for examples. - It's also possible to update or delete a model using a unique identifier other than `id` by passing a `key_attr` argument: ```python @strawberry_django.partial(SomeModel) class SomeModelInputPartial: unique_field: strawberry.auto @strawberry.type class Mutation: update_model: SomeModelType = mutations.update( SomeModelInputPartial, key_attr="unique_field", ) delete_model: SomeModelType = mutations.delete( SomeModelInputPartial, key_attr="unique_field", ) ``` ## Filtering > [!CAUTION] > Filtering on mutations is discouraged as it can potentially alter your entire model collection if there are issues with the filters. Filters can be added to update and delete mutations. More information in the [filtering](filters.md) section. ```python title="schema.py" import strawberry from strawberry_django import mutations @strawberry.type class Mutation: updateFruits: list[Fruit] = mutations.update(FruitPartialInput, filters=FruitFilter) deleteFruits: list[Fruit] = mutations.delete(filters=FruitFilter) schema = strawberry.Schema(mutation=Mutation) ``` ## Batching If you need to make multiple creates, updates, or deletes as part of one atomic mutation you can use batching. Batching has a similar syntax except that the mutations take and return a list. ```python title="schema.py" import strawberry from strawberry_django import mutations @strawberry.type class Mutation: createFruits: list[Fruit] = mutations.create(list[FruitPartialInput]) updateFruits: list[Fruit] = mutations.update(list[FruitPartialInput]) deleteFruits: list[Fruit] = mutations.delete(list[FruitPartialInput]) schema = strawberry.Schema(mutation=Mutation) ``` strawberry-graphql-django-0.62.0/docs/guide/optimizer.md000066400000000000000000000336761502405145400233070ustar00rootroot00000000000000--- title: Query Optimizer --- # Query Optimizer ## Features The query optimizer is a must-have extension for improved performance of your schema. What it does: 1. Call [QuerySet.select_related()](https://docs.djangoproject.com/en/4.2/ref/models/querysets/#select-related) on all selected foreign key relations by the query to avoid requiring an extra query to retrieve those 2. Call [QuerySet.prefetch_related()](https://docs.djangoproject.com/en/4.2/ref/models/querysets/#prefetch-related) on all selected many-to-one/many-to-many relations by the query to avoid requiring an extra query to retrieve those. 3. Call [QuerySet.only()](https://docs.djangoproject.com/en/4.2/ref/models/querysets/#only) on all selected fields to reduce the database payload and only requesting what is actually being selected 4. Call [QuerySet.annotate()](https://docs.djangoproject.com/en/4.2/ref/models/querysets/#annotate) to support any passed annotations of [Query Expressions](https://docs.djangoproject.com/en/4.2/ref/models/expressions/). Those are specially useful to avoid some common GraphQL pitfalls, like the famous `n+1` issue. ## Enabling the extension The automatic optimization can be enabled by adding the `DjangoOptimizerExtension` to your strawberry's schema config. ```python title="schema.py" import strawberry from strawberry_django.optimizer import DjangoOptimizerExtension schema = strawberry.Schema( Query, extensions=[ # other extensions... DjangoOptimizerExtension, ] ) ``` ## Usage The optimizer will try to optimize all types automatically by introspecting it. Consider the following example: ```python title="models.py" class Artist(models.Model): name = models.CharField() class Album(models.Model): name = models.CharField() release_date = models.DateTimeField() artist = models.ForeignKey("Artist", related_name="albums") class Song(models.Model): name = model.CharField() duration = models.DecimalField() album = models.ForeignKey("Album", related_name="songs") ``` ```python title="types.py" from strawberry import auto import strawberry_django @strawberry_django.type(Artist) class ArtistType: name: auto albums: list["AlbumType"] albums_count: int = strawberry_django.field(annotate=Count("albums")) @strawberry_django.type(Album) class AlbumType: name: auto release_date: auto artist: ArtistType songs: list["SongType"] @strawberry_django.type(Song) class SongType: name: auto duration: auto album_type: AlbumType @strawberry.type class Query: artist: Artist = strawberry_django.field() songs: list[SongType] = strawberry_django.field() ``` Querying for `artist` and `songs` like this: ```graphql title="schema.graphql" query { artist { id name albums { id name songs { id name } } albumsCount } song { id album { id name artist { id name albums { id name release_date } } } } } ``` Would produce an ORM query like this: ```python # For "artist" query Artist.objects.all().only("id", "name").prefetch_related( Prefetch( "albums", queryset=Album.objects.all().only("id", "name").prefetch_related( Prefetch( "songs", Song.objects.all().only("id", "name"), ) ) ), ).annotate( albums_count=Count("albums") ) # For "songs" query Song.objects.all().only( "id", "album", "album__id", "album__name", "album__release_date", # Note about this below "album__artist", "album__artist__id", ).select_related( "album", "album__artist", ).prefetch_related( Prefetch( "album__artist__albums", Album.objects.all().only("id", "name", "release_date"), ) ) ``` > [!NOTE] > Even though `album__release_date` field was not selected here, it got selected > in the prefetch query later. Since Django caches known objects, we have to select it here or > else it would trigger extra queries latter. ## Optimization hints Sometimes you will have a custom resolver which cannot be automatically optimized by the extension. Take this for example: ```python title="models.py" class OrderItem(models.Model): price = models.DecimalField() quantity = models.IntegerField() @property def total(self) -> decimal.Decimal: return self.price * self.quantity ``` ```python title="types.py" from strawberry import auto import strawberry_django @strawberry_django.type(models.OrderItem) class OrderItem: price: auto quantity: auto total: auto ``` In this case, if only `total` is requested it would trigger an extra query for both `price` and `quantity` because both had their value retrievals [defered](https://docs.djangoproject.com/en/4.2/ref/models/querysets/#django.db.models.query.QuerySet.defer) by the optimizer. A solution in this case would be to "tell the optimizer" how to optimize that field: ```python title="types.py" from strawberry import auto import strawberry_django @strawberry_django.type(models.OrderItem) class OrderItem: price: auto quantity: auto total: auto = strawberry_django.field( only=["price", "quantity"], ) ``` Or if you are using a custom resolver: ```python title="types.py" import decimal from strawberry import auto import strawberry_django @strawberry_django.type(models.OrderItem) class OrderItem: price: auto quantity: auto @strawberry_django.field(only=["price", "quantity"]) def total(self, root: models.OrderItem) -> decimal.Decimal: return root.price * root.quantity # or root.total directly ``` The following options are accepted for optimizer hints: - `only`: a list of fields in the same format as accepted by [QuerySet.only()](https://docs.djangoproject.com/en/4.2/ref/models/querysets/#only) - `select_related`: a list of relations to join using [QuerySet.select_related()](https://docs.djangoproject.com/en/4.2/ref/models/querysets/#select-related) - `prefetch_related`: a list of relations to prefetch using [QuerySet.prefetch_related()](https://docs.djangoproject.com/en/4.2/ref/models/querysets/#prefetch-related). The options here are strings or a callable in the format of `Callable[[Info], Prefetch]` (e.g. `prefetch_related=[lambda info: Prefetch(...)]`) - `annotate`: a dict of expressions to annotate using [QuerySet.annotate()](https://docs.djangoproject.com/en/4.2/ref/models/querysets/#annotate). The keys of this dict are strings, and each value is a [Query Expression](https://docs.djangoproject.com/en/4.2/ref/models/expressions/) or a callable in the format of `Callable[[Info], BaseExpression]` (e.g. `annotate={"total": lambda info: Sum(...)}`) ## Optimization hints on model (ModelProperty) It is also possible to include type hints directly in the models' `@property` to allow it to be resolved with `auto`, while the GraphQL schema doesn't have to worry about its internal logic. For that this integration provides 2 decorators that can be used: - `strawberry_django.model_property`: similar to `@property` but accepts optimization hints - `strawberry_django.cached_model_property`: similar to `@cached_property` but accepts optimization hints The example in the previous section could be written using `@model_property` like this: ```python title="models.py" from strawberry_django.descriptors import model_property class OrderItem(models.Model): price = models.DecimalField() quantity = models.IntegerField() @model_property(only=["price", "quantity"]) def total(self) -> decimal.Decimal: return self.price * self.quantity ``` ```python title="types.py" from strawberry import auto import strawberry_django @strawberry_django.type(models.OrderItem) class OrderItem: price: auto quantity: auto total: auto ``` `total` now will be properly optimized since it points to a `@model_property` decorated attribute, which contains the required information for optimizing it. ## Optimizing polymorphic queries The optimizer has dedicated support for polymorphic queries, that is, fields which return an interface. The optimizer will handle optimizing any subtypes of the interface as necessary. This is supported on top level queries as well as relations between models. See the following sections for how this interacts with your models. ### Using Django Polymorphic If you are already using the [Django Polymorphic](https://django-polymorphic.readthedocs.io/en/stable/) library, polymorphic queries work out of the box. ```python title="models.py" from django.db import models from polymorphic.models import PolymorphicModel class Project(PolymorphicModel): topic = models.CharField(max_length=255) class ResearchProject(Project): supervisor = models.CharField(max_length=30) class ArtProject(Project): artist = models.CharField(max_length=30) ``` ```python title="types.py" import strawberry import strawberry_django from . import models @strawberry_django.interface(models.Project) class ProjectType: topic: strawberry.auto @strawberry_django.type(models.ResearchProject) class ResearchProjectType(ProjectType): supervisor: strawberry.auto @strawberry_django.type(models.ArtProject) class ArtProjectType(ProjectType): artist: strawberry.auto @strawberry.type class Query: projects: list[ProjectType] = strawberry_django.field() ``` The `projects` field will return either ResearchProjectType or ArtProjectType, matching on whether it is a ResearchProject or ArtProject. The optimizer will make sure to only select those fields from subclasses which are requested in the GraphQL query in the same way that it does normally. > [!WARNING] > The optimizer does not filter your QuerySet and Django will return > all instances of your model, regardless of whether their type exists in your GraphQL schema or not. > Make sure you have a corresponding type for every model subclass or add a `get_queryset` method to your > GraphQL interface type to filter out unwanted subtypes. > Otherwise you might receive an error like > `Abstract type 'ProjectType' must resolve to an Object type at runtime for field 'Query.projects'.` ### Using Model-Utils InheritanceManager Models using `InheritanceManager` from [django-model-utils](https://django-model-utils.readthedocs.io/en/latest/) are also supported. ```python title="models.py" from django.db import models from model_utils.managers import InheritanceManager class Project(models.Model): topic = models.CharField(max_length=255) objects = InheritanceManager() class ResearchProject(Project): supervisor = models.CharField(max_length=30) class ArtProject(Project): artist = models.CharField(max_length=30) ``` ```python title="types.py" import strawberry import strawberry_django from . import models @strawberry_django.interface(models.Project) class ProjectType: topic: strawberry.auto @strawberry_django.type(models.ResearchProject) class ResearchProjectType(ProjectType): supervisor: strawberry.auto @strawberry_django.type(models.ArtProject) class ArtProjectType(ProjectType): artist: strawberry.auto @strawberry.type class Query: projects: list[ProjectType] = strawberry_django.field() ``` The `projects` field will return either ResearchProjectType or ArtProjectType, matching on whether it is a ResearchProject or ArtProject. The optimizer automatically calls `select_subclasses`, passing in any subtypes present in your schema. > [!WARNING] > The optimizer does not filter your QuerySet and Django will return > all instances of your model, regardless of whether their type exists in your GraphQL schema or not. > Make sure you have a corresponding type for every model subclass or add a `get_queryset` method to your > GraphQL interface type to filter out unwanted subtypes. > Otherwise you might receive an error like > `Abstract type 'ProjectType' must resolve to an Object type at runtime for field 'Query.projects'.` > [!NOTE] > If you have polymorphic relations (as in: a field that points to a model with subclasses), you need to make sure > the manager being used to look up the related model is an `InheritanceManager`. > Strawberry Django uses the model's [base manager](https://docs.djangoproject.com/en/5.1/topics/db/managers/#base-managers) > by default, which is different from the standard `objects`. > Either change your base manager to also be an `InheritanceManager` or set Strawberry Django to use the default > manager: `DjangoOptimizerExtension(prefetch_custom_queryset=True)`. ### Custom polymorphic solution The optimizer also supports polymorphism even if your models are not polymorphic. `resolve_type` in the GraphQL interface type is used to tell GraphQL the actual type that should be used. ```python title="models.py" from django.db import models class Project(models.Model): topic = models.CharField(max_length=255) supervisor = models.CharField(max_length=30) artist = models.CharField(max_length=30) ``` ```python title="types.py" import strawberry import strawberry_django from . import models @strawberry_django.interface(models.Project) class ProjectType: topic: strawberry.auto @classmethod def resolve_type(cls, value, info, parent_type) -> str: if not isinstance(value, models.Project): raise TypeError() if value.artist: return 'ArtProjectType' if value.supervisor: return 'ResearchProjectType' raise TypeError() @classmethod def get_queryset(cls, qs, info): return qs @strawberry_django.type(models.ResearchProject) class ResearchProjectType(ProjectType): supervisor: strawberry.auto @strawberry_django.type(models.ArtProject) class ArtProjectType(ProjectType): artist: strawberry.auto @strawberry.type class Query: projects: list[ProjectType] = strawberry_django.field() ``` > [!WARNING] > Make sure to add `get_queryset` to your interface type, to force the optimizer to use > `prefetch_related`, otherwise this technique will not work for relation fields. strawberry-graphql-django-0.62.0/docs/guide/ordering.md000066400000000000000000000145221502405145400230630ustar00rootroot00000000000000--- title: Ordering --- # Ordering `@strawberry_django.ordering_type` is an upgrade from the previous `@strawberry_django.order` implementation and allows sorting by multiple fields. ```python title="types.py" @strawberry_django.order_type(models.Color) class ColorOrder: name: auto @strawberry_django.order_type(models.Fruit) class FruitOrder: name: auto color: ColorOrder | None ``` > [!TIP] > In most cases ordering fields should have `Optional` annotations and default value `strawberry.UNSET`. > Above `auto` annotation is wrapped in `Optional` automatically. > `UNSET` is automatically used for fields without `field` or with `strawberry_django.order_field`. The code above generates the following schema: ```graphql title="schema.graphql" enum Ordering { ASC ASC_NULLS_FIRST ASC_NULLS_LAST DESC DESC_NULLS_FIRST DESC_NULLS_LAST } input ColorOrder @oneOf { name: Ordering } input FruitOrder @oneOf { name: Ordering color: ColorOrder } ``` As you can see, every input is automatically annotated with `@oneOf`. To express ordering by multiple fields, a list is passed. ## Custom order methods You can define custom order method by defining your own resolver. ```python title="types.py" @strawberry_django.order_type(models.Fruit) class FruitOrder: name: auto @strawberry_django.order_field def discovered_by(self, value: bool, prefix: str) -> list[str]: if not value: return [] return [f"{prefix}discover_by__name", f"{prefix}name"] @strawberry_django.order_field def order_number( self, info: Info, queryset: QuerySet, value: strawberry_django.Ordering, # `auto` can be used instead prefix: str, ) -> tuple[QuerySet, list[str]] | list[str]: queryset = queryset.alias( _ordered_num=Count(f"{prefix}orders__id") ) ordering = value.resolve(f"{prefix}_ordered_num") return queryset, [ordering] ``` > [!WARNING] > Do not use `queryset.order_by()` directly. Due to `order_by` not being chainable > operation, changes applied this way would be overridden later. > [!TIP] > The `strawberry_django.Ordering` type has convenient method `resolve` that can be used to > convert field's name to appropriate `F` object with correctly applied `asc()`, `desc()` method > with `nulls_first` and `nulls_last` arguments. The code above generates the following schema: ```graphql title="schema.graphql" enum Ordering { ASC ASC_NULLS_FIRST ASC_NULLS_LAST DESC DESC_NULLS_FIRST DESC_NULLS_LAST } input FruitOrder @oneOf { name: Ordering discoveredBy: bool orderNumber: Ordering } ``` #### Resolver arguments - `prefix` - represents the current path or position - **Required** - Important for nested ordering - In code below custom order `name` ends up ordering `Fruit` instead of `Color` without applying `prefix` ```python title="Why prefix?" @strawberry_django.order_type(models.Fruit) class FruitOrder: name: auto color: ColorOrder | None @strawberry_django.order_type(models.Color) class ColorOrder: @strawberry_django.order_field def name(self, value: bool, prefix: str): # prefix is "fruit_set__" if unused root object is ordered instead if value: return ["name"] return [] ``` ```graphql { fruits( ordering: [{color: name: ASC}] ) { ... } } ``` - `value` - represents graphql field type - **Required**, but forbidden for default `order` method - _must_ be annotated - used instead of field's return type - Using `auto` is the same as `strawberry_django.Ordering`. - `queryset` - can be used for more complex ordering - Optional, but **Required** for default `order` method - usually used to `annotate` `QuerySet` #### Resolver return For custom field methods two return values are supported - iterable of values acceptable by `QuerySet.order_by` -> `Collection[F | str]` - tuple with `QuerySet` and iterable of values acceptable by `QuerySet.order_by` -> `tuple[QuerySet, Collection[F | str]]` For default `order` method only second variant is supported. ### What about nulls? By default `null` values are ignored. This can be toggled as such `@strawberry_django.order_field(order_none=True)` ## Overriding the default `order` method Works similar to field order method, but: - is responsible for resolution of ordering for entire object - _must_ be named `order` - argument `queryset` is **Required** - argument `value` is **Forbidden** ```python title="types.py" @strawberry_django.order_type(models.Fruit) class FruitOrder: name: auto @strawberry_django.order_field def ordered( self, info: Info, queryset: QuerySet, value: strawberry_django.Ordering, prefix: str ) -> tuple[QuerySet, list[str]] | list[str]: queryset = queryset.alias( _ordered_num=Count(f"{prefix}orders__id") ) return queryset, [value.resolve(f"{prefix}_ordered_num")] @strawberry_django.order_field def order( self, info: Info, queryset: QuerySet, prefix: str, ) -> tuple[QuerySet, list[str]]: queryset = queryset.filter( ... # Do some query modification ) return strawberry_django.ordering.process_ordering_default( self, info=info, queryset=queryset, prefix=prefix, ) ``` > [!TIP] > As seen above `strawberry_django.ordering.process_ordering_default` function is exposed and can be > reused in custom methods. This provides the default ordering implementation. ## Adding orderings to types All fields and mutations inherit orderings from the underlying type by default. So, if you have a field like this: ```python title="types.py" @strawberry_django.type(models.Fruit, ordering=FruitOrder) class Fruit: ... ``` The `fruits` field will inherit the `ordering` of the type the same way as if it was passed to the field. ## Adding orderings directly into a field Orderings added into a field override the default order of this type. ```python title="schema.py" @strawberry.type class Query: fruit: Fruit = strawberry_django.field(ordering=FruitOrder) ``` ## Legacy Order The previous implementation (`@strawberry_django.order`) is still available, but deprecated and only provided to allow backwards-compatible schemas. It can be used together with `@strawberry_django.ordering.ordering`, however clients may only specify one or the other. You can still read the [documentation for it](legacy-ordering). strawberry-graphql-django-0.62.0/docs/guide/pagination.md000066400000000000000000000161621502405145400234050ustar00rootroot00000000000000--- title: Pagination --- # Pagination ## Default pagination An interface for limit/offset pagination can be use for basic pagination needs: ```python title="types.py" @strawberry_django.type(models.Fruit, pagination=True) class Fruit: name: auto @strawberry.type class Query: fruits: list[Fruit] = strawberry_django.field() ``` Would produce the following schema: ```graphql title="schema.graphql" type Fruit { name: String! } input OffsetPaginationInput { offset: Int! = 0 limit: Int = null } type Query { fruits(pagination: OffsetPaginationInput): [Fruit!]! } ``` And can be queried like: ```graphql title="schema.graphql" query { fruits(pagination: { offset: 0, limit: 2 }) { name } } ``` The `pagination` argument can be given to the type, which will enforce the pagination argument every time the field is annotated as a list, but you can also give it directly to the field for more control, like: ```python title="types.py" @strawberry_django.type(models.Fruit) class Fruit: name: auto @strawberry.type class Query: fruits: list[Fruit] = strawberry_django.field(pagination=True) ``` Which will produce the exact same schema. ### Default limit for pagination The default limit for pagination is set to `100`. This can be changed in the [strawberry django settings](./settings.md) to increase or decrease that number, or even set to `None` to set it to unlimited. To configure it on a per field basis, you can define your own `OffsetPaginationInput` subclass and modify its default value, like: ```python @strawberry.input def MyOffsetPaginationInput(OffsetPaginationInput): limit: int = 250 # Pass it to the pagination argument when defining the type @strawberry_django.type(models.Fruit, pagination=MyOffsetPaginationInput) class Fruit: ... @strawberry.type class Query: # Or pass it to the pagination argument when defining the field fruits: list[Fruit] = strawberry_django.field(pagination=MyOffsetPaginationInput) ``` ## OffsetPaginated Generic For more complex pagination needs, you can use the `OffsetPaginated` generic, which alongside the `pagination` argument, will wrap the results in an object that contains the results and the pagination information, together with the `totalCount` of elements excluding pagination. ```python title="types.py" from strawberry_django.pagination import OffsetPaginated @strawberry_django.type(models.Fruit) class Fruit: name: auto @strawberry.type class Query: fruits: OffsetPaginated[Fruit] = strawberry_django.offset_paginated() ``` Would produce the following schema: ```graphql title="schema.graphql" type Fruit { name: String! } type PaginationInfo { limit: Int = null offset: Int! } type FruitOffsetPaginated { pageInfo: PaginationInfo! totalCount: Int! results: [Fruit]! } input OffsetPaginationInput { offset: Int! = 0 limit: Int = null } type Query { fruits(pagination: OffsetPaginationInput): [FruitOffsetPaginated!]! } ``` Which can be queried like: ```graphql title="schema.graphql" query { fruits(pagination: { offset: 0, limit: 2 }) { totalCount pageInfo { limit offset } results { name } } } ``` > [!NOTE] > OffsetPaginated follow the same rules for the default pagination limit, and can be configured > in the same way as explained above. ### Customizing queryset resolver It is possible to define a custom resolver for the queryset to either provide a custom queryset for it, or even to receive extra arguments alongside the pagination arguments. Suppose we want to pre-filter a queryset of fruits for only available ones, while also adding [ordering](./ordering.md) to it. This can be achieved with: ```python title="types.py" @strawberry_django.type(models.Fruit) class Fruit: name: auto price: auto @strawberry_django.order(models.Fruit) class FruitOrder: name: auto price: auto @strawberry.type class Query: @strawberry_django.offset_paginated(OffsetPaginated[Fruit], order=order) def fruits(self, only_available: bool = True) -> QuerySet[Fruit]: queryset = models.Fruit.objects.all() if only_available: queryset = queryset.filter(available=True) return queryset ``` This would produce the following schema: ```graphql title="schema.graphql" type Fruit { name: String! price: Decimal! } type FruitOrder { name: Ordering price: Ordering } type PaginationInfo { limit: Int! offset: Int! } type FruitOffsetPaginated { pageInfo: PaginationInfo! totalCount: Int! results: [Fruit]! } input OffsetPaginationInput { offset: Int! = 0 limit: Int = null } type Query { fruits( onlyAvailable: Boolean! = true pagination: OffsetPaginationInput order: FruitOrder ): [FruitOffsetPaginated!]! } ``` ### Customizing the pagination Like other generics, `OffsetPaginated` can be customized to modify its behavior or to add extra functionality in it. For example, suppose we want to add the average price of the fruits in the pagination: ```python title="types.py" from strawberry_django.pagination import OffsetPaginated @strawberry_django.type(models.Fruit) class Fruit: name: auto price: auto @strawberry.type class FruitOffsetPaginated(OffsetPaginated[Fruit]): @strawberry_django.field def average_price(self) -> Decimal: if self.queryset is None: return Decimal(0) return self.queryset.aggregate(Avg("price"))["price__avg"] @strawberry_django.field def paginated_average_price(self) -> Decimal: paginated_queryset = self.get_paginated_queryset() if paginated_queryset is None: return Decimal(0) return paginated_queryset.aggregate(Avg("price"))["price__avg"] @strawberry.type class Query: fruits: FruitOffsetPaginated = strawberry_django.offset_paginated() ``` Would produce the following schema: ```graphql title="schema.graphql" type Fruit { name: String! } type PaginationInfo { limit: Int = null offset: Int! } type FruitOffsetPaginated { pageInfo: PaginationInfo! totalCount: Int! results: [Fruit]! averagePrice: Decimal! paginatedAveragePrice: Decimal! } input OffsetPaginationInput { offset: Int! = 0 limit: Int = null } type Query { fruits(pagination: OffsetPaginationInput): [FruitOffsetPaginated!]! } ``` The following attributes/methods can be accessed in the `OffsetPaginated` class: - `queryset`: The queryset original queryset with any filters/ordering applied, but not paginated yet - `pagination`: The `OffsetPaginationInput` object, with the `offset` and `limit` for pagination - `get_total_count()`: Returns the total count of elements in the queryset without pagination - `get_paginated_queryset()`: Returns the queryset with pagination applied - `resolve_paginated(queryset, *, info, pagination, **kwargs)`: The classmethod that strawberry-django calls to create an instance of the `OffsetPaginated` class/subclass. ## Cursor pagination (aka Relay style pagination) Another option for pagination is to use a [relay style cursor pagination](https://graphql.org/learn/pagination). For this, you can leverage the [relay integration](./relay.md) provided by strawberry to create a relay connection. strawberry-graphql-django-0.62.0/docs/guide/permissions.md000066400000000000000000000074541502405145400236330ustar00rootroot00000000000000--- title: Permissions --- # Permissions This integration exposes a field extension to extend fields into using the [Django's Permissioning System](https://docs.djangoproject.com/en/4.2/topics/auth/default/) for checking for permissions. It supports protecting any field for cases like: - The user is authenticated - The user is a superuser - The user or a group they belongs to has a given permission - The user or the group they belongs has a given permission to the resolved value - The user or the group they belongs has a given permission to the parent of the field - etc ## How it works ```mermaid graph TD A[Extension Check for Permissions] --> B; B[User Passes Checks] -->|Yes| BF[Return Resolved Value]; B -->|No| C; C[Can return 'OperationInfo'?] -->|Yes| CF[Return 'OperationInfo']; C -->|No| D; D[Field is Optional] -->|Yes| DF[Return 'None']; D -->|No| E; E[Field is a 'List'] -->|Yes| EF[Return an empty 'List']; E -->|No| F; F[Field is a relay 'Connection'] -->|Yes| FF[Return an empty relay 'Connection']; F -->|No| GF[Raises 'PermissionDenied' error]; ``` ## Example ```python title="types.py" import strawberry_django from strawberry_django.permissions import ( IsAuthenticated, HasPerm, HasRetvalPerm, ) @strawberry_django.type class SomeType: login_required_field: RetType = strawberry_django.field( # will check if the user is authenticated extensions=[IsAuthenticated()], ) perm_required_field: OtherType = strawberry_django.field( # will check if the user has `"some_app.some_perm"` permission extensions=[HasPerm("some_app.some_perm")], ) obj_perm_required_field: OtherType = strawberry_django.field( # will check the permission for the resolved value extensions=[HasRetvalPerm("some_app.some_perm")], ) ``` ## Available Options Available options are: - `IsAuthenticated`: Checks if the user is authenticated (i.e. `user.is_autenticated`) - `IsStaff`: Checks if the user is a staff member (i.e. `user.is_staff`) - `IsSuperuser`: Checks if the user is a superuser (i.e. `user.is_superuser`) - `HasPerm(perms: str | list[str], any_perm: bool = True)`: Checks if the user has any or all of the given permissions (i.e. `user.has_perm(perm)`) - `HasSourcePerm(perms: str | list[str], any: bool = True)`: Checks if the user has any or all of the given permissions for the root of that field (i.e. `user.has_perm(perm, root)`) - `HasRetvalPerm(perms: str | list[str], any: bool = True)`: Resolves the retval and then checks if the user has any or all of the given permissions for that specific value (i.e. `user.has_perm(perm, retval)`). If the return value is a list, this extension will filter the return value, removing objects that fails the check (check below for more information regarding other possibilities). > [!NOTE] > The `HasSourcePerm` and `HasRetvalPerm` require having an > [authentication backend](https://docs.djangoproject.com/en/4.2/topics/auth/customizing/) > which supports resolving object permissions. This lib works out of the box with > [django-guardian](https://django-guardian.readthedocs.io/en/stable/), so if you are > using it you don't need to do anything else. ## No Permission Handling When the condition fails, the following will be returned on the field (following this priority): 1. `OperationInfo`/`OperationMessage` if those types are allowed at the return type 2. `null` in case the field is not mandatory (e.g. `String` or `[String]`) 3. An empty list in case the field is a list (e.g. `[String]!`) 4. An empty `Connection` in case the return type is a relay connection 5. Otherwise, an error will be raised ## Custom Permissions Checking You can create your own permission checking extension by subclassing `DjangoPermissionExtension` and implementing your own `resolve_for_user` method. strawberry-graphql-django-0.62.0/docs/guide/queries.md000066400000000000000000000015161502405145400227260ustar00rootroot00000000000000--- title: Queries --- # Queries Queries can be written using `strawberry_django.field()` to load the fields defined in the `types.py` file. ```python #schema.py import strawberry import strawberry_django from .types import Fruit @strawberry.type class Query: fruit: Fruit = strawberry_django.field() fruits: list[Fruit] = strawberry_django.field() schema = strawberry.Schema(query=Query) ``` > [!TIP] > You must name your query class "Query" or decorate it with `@strawberry.type(name="Query")` for the single query default primary filter to work For the single queries (like `Fruit` above), Strawberry comes with a default primary key search filter in the GraphiQL interface. The query `Fruits` gets all the objects in the Fruits by default. To query specific sets of objects a filter need to be added in the `types.py` file. strawberry-graphql-django-0.62.0/docs/guide/relay.md000066400000000000000000000107501502405145400223650ustar00rootroot00000000000000--- title: Relay --- # Relay Support You can use the [official strawberry relay integration](https://strawberry.rocks/docs/guides/relay) directly with django types like this: ```python title="types.py" import strawberry import strawberry_django from strawberry_django.relay import DjangoListConnection class Fruit(models.Model): ... @strawberry_django.type(Fruit) class FruitType(relay.Node): ... @strawberry.type class Query: # Option 1: Default relay without totalCount # This is the default strawberry relay behaviour. # NOTE: you need to use strawberry_django.connection() - not the default strawberry.relay.connection() fruit: strawberry.relay.ListConnection[FruitType] = strawberry_django.connection() # Option 2: Strawberry django also comes with DjangoListConnection # this will allow you to get total-count on your query. fruit_with_total_count: DjangoListConnection[ FruitType ] = strawberry_django.connection() # Option 3: You can manually create resolver by your method manually. @strawberry_django.connection(DjangoListConnection[FruitType]) def fruit_with_custom_resolver(self) -> list[SomeModel]: return Fruit.objects.all() ``` Behind the scenes this extension is doing the following for you: - Automatically resolve the `relay.NodeID` field using the [model's pk](https://docs.djangoproject.com/en/4.2/ref/models/fields/#django.db.models.Field.primary_key) - Automatically generate resolves for connections that doesn't define one. For example, `some_model_conn` and `some_model_conn_with_total_count` will both define a custom resolver automatically that returns `SomeModel.objects.all()`. - Integrate connection resolution with all other features available in this lib. For example, [filters](filters.md), [ordering](ordering.md) and [permissions](permissions.md) can be used together with connections defined by strawberry django. You can also define your own `relay.NodeID` field and your resolve, in the same way as `some_model_conn_with_resolver` is doing. In those cases, they will not be overridden. > [!TIP] > If you are only working with types inheriting from `relay.Node` and `GlobalID` > for identifying objects, you might want to set `MAP_AUTO_ID_AS_GLOBAL_ID=True` > in your [strawberry django settings](./settings.md) to make sure `auto` fields gets > mapped to `GlobalID` on types and filters. Also, this lib exposes a `strawberry_django.relay.DjangoListConnection`, which works the same way as `strawberry.relay.ListConnection` does, but also exposes a `totalCount` attribute in the connection. For more customization options, like changing the pagination algorithm, adding extra fields to the `Connection`/`Edge` type, take a look at the [official strawberry relay integration](https://strawberry.rocks/docs/guides/relay) as those are properly explained there. ## Cursor based connections As an alternative to the default `ListConnection`, `DjangoCursorConnection` is also available. It supports pagination through a Django `QuerySet` via "true" cursors. `ListConnection` uses slicing to achieve pagination, which can negatively affect performance for huge datasets, because large page numbers require a large `OFFSET` in SQL. Instead, `DjangoCursorConnection` uses range queries such as `Q(due_date__gte=...)` for pagination. In combination with an Index, this makes for more efficient queries. `DjangoCursorConnection` requires a _strictly_ ordered `QuerySet`, that is, no two entries in the `QuerySet` must be considered equal by its ordering. `order_by('due_date')` for example is not strictly ordered, because two items could have the same due date. `DjangoCursorConnection` will automatically resolve such situations by also ordering by the primary key. When the order for the connection is configurable by the user (for example via [`@strawberry_django.order`](./ordering.md)) then cursors created by `DjangoCursorConnection` will not be compatible between different orders. The drawback of cursor based pagination is that users cannot jump to a particular page immediately. Therefor cursor based pagination is better suited for special use-cases like an infinitely scrollable list. Otherwise `DjangoCursorConnection` behaves like other connection classes: ```python @strawberry.type class Query: fruit: DjangoCursorConnection[FruitType] = strawberry_django.connection() @strawberry_django.connection(DjangoCursorConnection[FruitType]) def fruit_with_custom_resolver(self) -> list[Fruit]: return Fruit.objects.all() ``` strawberry-graphql-django-0.62.0/docs/guide/resolvers.md000066400000000000000000000051341502405145400232750ustar00rootroot00000000000000--- title: Resolvers --- # Custom Resolvers Basic resolvers are generated automatically once the types are declared. However it is possible to override them with custom resolvers. ## Sync resolvers Sync resolvers can be used in both ASGI/WSGI and will be automatically wrapped in `sync_to_async` when running async. ```python title="types.py" import strawberry_django from strawberry import auto from . import models @strawberry_django.type(models.Color) class Color: id: auto name: auto @strawberry_django.field def fruits(self) -> list[Fruit]: return self.fruits.objects.filter(...) ``` ## Async resolvers Async resolvers can be used when running using ASGI. ```python title="types.py" import strawberry_django from strawberry import auto from . import models from asgiref.sync import sync_to_async @strawberry_django.type(models.Color) class Color: id: auto name: auto @strawberry_django.field async def fruits(self) -> list[Fruit]: return sync_to_async(list)(self.fruits.objects.filter(...)) ``` ## Optimizing resolvers When using custom resolvers together with the [Query Optimizer Extension](optimizer.md) you might need to give it a "hint" on how to optimize that field Take a look at the [optimization hints](optimizer.md#optimization-hints) docs for more information about this topic. ## Issues with Resolvers It is important to note that overriding resolvers also removes default capabilities (e.g. `Pagination`, `Filter`), exception for [relay connections](relay.md). You can however still add those by hand and resolve them: ```python title="types.py" import strawberry from strawberry import auto from strawberry.types import Info import strawberry_django from . import models @strawberry_django.filter(models.Fruit, lookups=True) class FruitFilter: id: auto name: auto @strawberry_django.type(models.Fruit, order=FruitOrder) class Fruit: id: auto name: auto @strawberry_django.type(models.Fruit, is_interface=True) class Fruit: id: auto name: auto @strawberry.type class Query: @strawberry_django.field def fruits( self, info: Info filters: FruitFilter | None = strawberry.UNSET, order: FruitOrder | None = strawberry.UNSET ) -> list[Fruit]: qs = models.fruit.objects.all() # apply filters if defined if filters is not strawberry.UNSET: qs = strawberry_django.filters.apply(filters, qs, info) # apply ordering if defined if order is not strawberry.UNSET: qs = strawberry_django.ordering.apply(filters, qs) return qs ``` strawberry-graphql-django-0.62.0/docs/guide/settings.md000066400000000000000000000071211502405145400231070ustar00rootroot00000000000000--- title: Settings --- # Django Settings Certain features of this library are configured using custom [Django settings](https://docs.djangoproject.com/en/4.2/topics/settings/). ## STRAWBERRY_DJANGO A dictionary with the following optional keys: - **`FIELD_DESCRIPTION_FROM_HELP_TEXT`** (default: `False`) If True, [GraphQL field's description](https://spec.graphql.org/draft/#sec-Descriptions) will be fetched from the corresponding Django model field's [`help_text` attribute](https://docs.djangoproject.com/en/4.1/ref/models/fields/#help-text). If a description is provided using [field customization](fields.md#field-customization), that description will be used instead. - **`TYPE_DESCRIPTION_FROM_MODEL_DOCSTRING`** (default: `False`) If True, [GraphQL type descriptions](https://spec.graphql.org/draft/#sec-Descriptions) will be fetched from the corresponding Django model's [docstring](https://docs.python.org/3/glossary.html#term-docstring). If a description is provided using the [`strawberry_django.type` decorator](types.md#types-from-django-models), that description will be used instead. - **`MUTATIONS_DEFAULT_ARGUMENT_NAME`** (default: `"data"`) Change the [CUD mutations'](mutations.md#cud-mutations) default argument name when no option is passed (e.g. to `"input"`) - **`MUTATIONS_DEFAULT_HANDLE_ERRORS`** (default: `False`) Set the default behaviour of the [Django Errors Handling](mutations.md#django-errors-handling) when no option is passed. - **`GENERATE_ENUMS_FROM_CHOICES`** (default: `False`) If True, fields with `choices` will have automatically generate an enum of possibilities instead of being exposed as `String`. A better option is to use [Django's TextChoices/IntegerChoices](https://docs.djangoproject.com/en/4.2/ref/models/fields/#enumeration-types) with the [django-choices-field](../integrations/choices-field.md) integration. - **`MAP_AUTO_ID_AS_GLOBAL_ID`** (default: `False`) If True, `auto` fields that refer to model ids will be mapped to `relay.GlobalID` instead of `strawberry.ID`. This is mostly useful if all your model types inherit from `relay.Node` and you want to work only with `GlobalID`. - **`DEFAULT_PK_FIELD_NAME`** (default: `"pk"`) Change the [CRUD mutations'](mutations.md#cud-mutations) default primary key field. - **`USE_DEPRECATED_FILTERS`** (default: `False`) If True, [legacy filters](filters.md#legacy-filtering) are enabled. This is usefull for migrating from previous version. - **`PAGINATION_DEFAULT_LIMIT`** (default: `100`) Default limit for [pagination](pagination.md) when one is not provided by the client. Can be set to `None` to set it to unlimited. - **`ALLOW_MUTATIONS_WITHOUT_FILTERS`** (default: `False`) If True, [CUD mutations](mutations.md#cud-mutations) will not require a filter to be specified. This is useful for cases where you want to allow mutations without any filtering, but it can lead to unintended side effects if not used carefully. These features can be enabled by adding this code to your `settings.py` file, like: ```python title="settings.py" STRAWBERRY_DJANGO = { "FIELD_DESCRIPTION_FROM_HELP_TEXT": True, "TYPE_DESCRIPTION_FROM_MODEL_DOCSTRING": True, "MUTATIONS_DEFAULT_ARGUMENT_NAME": "input", "MUTATIONS_DEFAULT_HANDLE_ERRORS": True, "GENERATE_ENUMS_FROM_CHOICES": False, "MAP_AUTO_ID_AS_GLOBAL_ID": True, "DEFAULT_PK_FIELD_NAME": "id", "PAGINATION_DEFAULT_LIMIT": 250, "ALLOW_MUTATIONS_WITHOUT_FILTERS": True, } ``` strawberry-graphql-django-0.62.0/docs/guide/subscriptions.md000066400000000000000000000074571502405145400241720ustar00rootroot00000000000000--- title: Subscriptions --- # Subscriptions Subscriptions are supported using the [Strawberry Django Channels](https://strawberry.rocks/docs/integrations/channels) integration. This guide will give you a minimal working example to get you going. There are 3 parts to this guide: 1. Making Django compatible 2. Setup local testing 3. Creating your first subscription ## Making Django compatible It's important to realise that Django doesn't support websockets out of the box. To resolve this, we can help the platform along a little. This implementation is based on Django Channels - this means that should you wish - there is a lot more websockets fun to be had. If you're interested, head over to [Django Channels](https://channels.readthedocs.io). To add the base compatibility, go to your `MyProject.asgi.py` file and replace it with the following content. Ensure that you replace the relevant code with your setup. ```python # MyProject.asgi.py import os from django.core.asgi import get_asgi_application from strawberry_django.routers import AuthGraphQLProtocolTypeRouter os.environ.setdefault("DJANGO_SETTINGS_MODULE", "MyProject.settings") # CHANGE the project name django_asgi_app = get_asgi_application() # Import your Strawberry schema after creating the django ASGI application # This ensures django.setup() has been called before any ORM models are imported # for the schema. from .schema import schema # CHANGE path to where you housed your schema file. application = AuthGraphQLProtocolTypeRouter( schema, django_application=django_asgi_app, ) ``` Note, django-channels allows for a lot more complexity. Here we merely cover the basic framework to get subscriptions to run on Django with minimal effort. Should you be interested in discovering the far more advanced capabilities of Django channels, head over to [channels docs](https://channels.readthedocs.io) ## Setup local testing The classic `./manage.py runserver` will not support subscriptions as it runs on WSGI mode. However, Django has ASGI server support out of the box through Daphne, which will override the runserver command to support our desired ASGI support. There are other asgi servers available, such as Uvicorn and Hypercorn. For the sake of simplicity we'll use Daphne as it comes with the runserver override. [Django Docs](https://docs.djangoproject.com/en/4.2/howto/deployment/asgi/daphne/) This shouldn't stop you from using any of the other ASGI flavours in production or local testing like Uvicorn or Hypercorn To get started: Firstly, we need install Daphne to handle the workload, so let's install it: ```bash pip install daphne ``` Secondly, we need to add `daphne` to your settings.py file before 'django.contrib.staticfiles' ```python INSTALLED_APPS = [ ... 'daphne', 'django.contrib.staticfiles', ... ] ``` and add your `ASGI_APPLICATION` setting in your settings.py ```python # settings.py ... ASGI_APPLICATION = 'MyProject.asgi.application' ... ``` Now you can run your test-server like as usual, but with ASGI support: ```bash ./manage.py runserver ``` ## Creating your first subscription Once you've taken care of those 2 setup steps, your first subscription is a breeze. Go and edit your schema-file and add: ```python import asyncio import strawberry @strawberry.type class Subscription: @strawberry.subscription async def count(self, target: int = 100) -> int: for i in range(target): yield i await asyncio.sleep(0.5) ``` That's pretty much it for this basic start. See for yourself by running your test server `./manange.py runserver` and opening `http://127.0.0.1:8000/graphql/` in your browser. Now run: ```graphql subscription { count(target: 10) } ``` You should see something like (where the count changes every .5s to a max of 9) ```json { "data": { "count": 9 } } ``` strawberry-graphql-django-0.62.0/docs/guide/types.md000066400000000000000000000106461502405145400224210ustar00rootroot00000000000000--- title: Defining Types --- # Defining Types ## Output types > [!NOTE] > It is highly recommended to enable the [Query Optimizer Extension](optimizer.md) > for improved performance and avoid some common pitfalls (e.g. the `n+1` issue) Output types are generated from models. The `auto` type is used for field type auto resolution. Relational fields are described by referencing to other types generated from Django models. A many-to-many relation is described with the `typing.List` type annotation. `strawberry_django` will automatically generate resolvers for relational fields. More information about that can be read from [resolvers](resolvers.md) page. ```python title="types.py" import strawberry_django from strawberry import auto @strawberry_django.type(models.Fruit) class Fruit: id: auto name: auto color: "Color" @strawberry_django.type(models.Color) class Color: id: auto name: auto fruits: list[Fruit] ``` ## Input types Input types can be generated from Django models using the `strawberry_django.input` decorator. The first parameter is the model which the type is derived from. ```python title="types.py" @strawberry_django.input(models.Fruit) class FruitInput: id: auto name: auto color: "ColorInput" ``` A partial input type, in which all `auto`-typed fields are optional, is generated by setting the `partial` keyword argument in `input` to `True`. Partial input types can be generated from existing input types through class inheritance. Non-`auto` type annotations will be respected—and therefore required—unless explicitly marked `Optional[]`. ```python title="types.py" @strawberry_django.input(models.Color, partial=True) class FruitPartialInput(FruitInput): color: list["ColorPartialInput"] # Auto fields are optional @strawberry_django.input(models.Color, partial=True) class ColorPartialInput: id: auto name: auto fruits: list[FruitPartialInput] # Alternate input; "name" field will be required @strawberry_django.input(models.Color, partial=True) class ColorNameRequiredPartialInput: id: auto name: str fruits: list[FruitPartialInput] ``` ## Types from Django models Django models can be converted to `strawberry` Types with the `strawberry_django.type` decorator. Custom descriptions can be added using the `description` keyword argument (See: [`strawberry.type` decorator API](https://strawberry.rocks/docs/types/object-types#api)). ```python title="types.py" import strawberry_django @strawberry_django.type(models.Fruit, description="A tasty snack") class Fruit: ... ``` ### Adding fields to the type By default, no fields are implemented on the new type. Check the documentation on [How to define Fields](fields.md) for that. ### Customizing the returned `QuerySet` > [!WARNING] > By doing this you are modifying all automatic `QuerySet` generation for any field > that returns this type. Ideally you will want to define your own [resolver](resolvers.md) > instead, which gives you more control over it. By default, a `strawberry_django` type will get data from the default manager for its Django Model. You can implement a custom `get_queryset` classmethod to your type to do some extra processing to the default queryset, like filtering it further. ```python title="types.py" @strawberry_django.type(models.Fruit) class Berry: @classmethod def get_queryset(cls, queryset, info, **kwargs): return queryset.filter(name__contains="berry") ``` The `get_queryset` classmethod is given a `QuerySet` to filter and a `strawberry` `Info` object containing details about the request. You can use that `info` parameter to, for example, limit access to results based on the current user in the request: ```python title="types.py" from strawberry_django.auth.utils import get_current_user @strawberry_django.type(models.Fruit) class Berry: @classmethod def get_queryset(cls, queryset, info, **kwargs): user = get_current_user(info) if not user.is_staff: # Restrict access to top secret berries if the user is not a staff member queryset = queryset.filter(is_top_secret=False) return queryset.filter(name__contains="berry") ``` > [!NOTE] > Another way of limiting this is by using the [PermissionExtension](permissions.md) > provided by this lib. The `kwargs` dictionary can include other parameters that were added in a `@strawberry.django.type` definition like [filters](filters.md) or [pagination](pagination.md). strawberry-graphql-django-0.62.0/docs/guide/unit-testing.md000066400000000000000000000032261502405145400237030ustar00rootroot00000000000000--- title: Unit Testing --- # Unit testing Unit testing can be done by following the [strawberry's testing docs](https://strawberry.rocks/docs/operations/testing) reference. This lib also provides a `TestClient` and an `AsyncTestClient` that makes it easier to run tests by mimicing a call to your API. For example, suppose you have a `me` query which returns the currently logged in user or `None` in case it is not authenticated. You could test it like this: ```python from strawberry_django.test.client import TestClient def test_me_unauthenticated(db): client = TestClient("/graphql") res = client.query(""" query TestQuery { me { pk email firstName lastName } } """) assert res.errors is None assert res.data == {"me": None} def test_me_authenticated(db): user = User.objects.create(...) client = TestClient("/graphql") with client.login(user): res = client.query(""" query TestQuery { me { pk email firstName lastName } } """) assert res.errors is None assert res.data == { "me": { "pk": user.pk, "email": user.email, "firstName": user.first_name, "lastName": user.last_name, }, } ``` For more information how to apply these tests, take a look at the [source](https://github.com/strawberry-graphql/strawberry-django/blob/main/strawberry_django/test/client.py) and [this example](https://github.com/strawberry-graphql/strawberry-django/blob/main/tests/test_permissions.py#L49) strawberry-graphql-django-0.62.0/docs/guide/views.md000066400000000000000000000020571502405145400224070ustar00rootroot00000000000000--- title: Views --- # Serving the API Strawberry works both with ASGI (async) and WSGI (sync). This integration supports both ways of serving django. ASGI is the best way to enjoy everything that strawberry has to offer and is highly recommended unless you can't for some reason. By using WSGI you will be missing support for some interesting features, such as [Data Loaders](https://strawberry.rocks/docs/guides/dataloaders). # Serving as ASGI (async) Expose the strawberry API when using ASGI by setting your urls.py like this: ```python title="urls.py" from django.urls import path from strawberry.django.views import AsyncGraphQLView from .schema import schema urlpatterns = [ path('graphql', AsyncGraphQLView.as_view(schema=schema)), ] ``` # Serving WSGI (sync) Expose the strawberry API when using WSGI by setting your urls.py like this: ```python title="urls.py" from django.urls import path from strawberry.django.views import GraphQLView from .schema import schema urlpatterns = [ path('graphql', GraphQLView.as_view(schema=schema)), ] ``` strawberry-graphql-django-0.62.0/docs/images/000077500000000000000000000000001502405145400210745ustar00rootroot00000000000000strawberry-graphql-django-0.62.0/docs/images/graphiql-with-fruit.png000066400000000000000000002636251502405145400255270ustar00rootroot00000000000000PNG  IHDR5kޣ\iCCPICC Profile(u=KBqaPHDCAD[}z})z#jji [4 p  "۹Y ?styU(nQzeÅ>&Zň$ f]RWS_pXz,x?}*Wpr22e)} m>v8bLRxP[S @_ƥ7F3ǂ 695 1169 Screenshot @IDATxE3E1O=pٿN=sn !i)@M     8-(2n4Ɋi%qG*VBZvtK8ɬ+iUy~x/tAFG!#ΎHHHHH@_d1)!!A{(L!P ;b;Pj1|[$@$@$@$@$@VHNNm„ r 'koWclx}|OVaxtbf,!Z6Q1F4Bee>+/dmUz~+)CПY+x W@Bh1= HHHH7^z ^|I@oa &ںuФPM6oܸѾ֭[&u;UWへ43fȱ=ZhR0SBnnvzhѢ>U|\<,Q:5ϖYu;Vrn^-(̑ IHHHH &OE7kWEok1 d:rw_N;]ZN}<澜 }eŴrnG H?,X@%1LMMqI׮]Bޙ:?Kv8CD^ m@D$3pqС}v"ad ~HzRH{ T|gYg%x3g͚_u6EnɢE_u['P'N?t9sLd(ඉm ppѣʚ5SeU=2\r|wyf!9D/?|e6 &۷^   h x3@vݙo+c f a΅s.݌wj޶cPCMXʔ)Gv] $S|ܹ:zvsʐye2o3&7x#vͼ/=^o(<\D4^!$a`cZ3-oJ@„ݝ}]w D#x{v#WKs=6oI&IJJJHD)S5"R>s@fϞ])zb\YQ棏>n@>}iF3?!͛7e^ȦkF| 8P9 0;{>"HJJr8 HHHBH7oJ \4j-VLVj3._満Tt_tdڰo~k]yeȑZPB_s.++K.]*ZiS .AHvQGf|7&/xfHHH#@@YQ/2s>lp ~i *ū̐tI/8^8Fn$x(!?ۧ "22SdT2wC~NUU!a̓mۮ=r~^]:"]wE$ڍ"Cbw, ̜~ ?% A HH<2#U$Wm;xĉr|p w)pićBृ?aX;<-o'pՕ.a+ȇ~h 7رcYNN Ń>x'{XBP4/ւB6lؠ N vviz<@wGj$瞓~Zo=z#<_wҽ{w=Yy?߿jO>ڕtǎ/zwȠA䪫cgpx Ἱ>O 9]Ёl`&95# 23Wmk 3}Ne0|?ѡ__},^X#T`ֹK ǓO>Y{ .;h?3f̐5kH˖-SN/XE'0`rƄJG7:0D0a M۶mmN |8# _1xAt'@0K>|G'B!ܭ^Z AkB bڃ-E^C~p~Z`x`8W 4V&T2`ʌxeu̫t5M>yKB됣&]{b!$asl푄2M,K]X=LPϖ_.|CM>Q[OgZ6OW|SkOsUl"ZgmS2 w4 ܯ`ꩧ8hxFFM^6Px ~ :2,ySЂ1 @@ڽ{P5cyu!Z 9ptgkA 1'j xC؁!jVykKce!v0kc+}$ShΜ9v%s*(?O( Wl"yB㬆oc C=Aرrs5<{g P7u4x=En=n `_$8:plx>HHH#"A<8cqT_ ye"#P:3uiG}i i,f :0c1/|<QI""2B~TUV Eܼ<=U._%'=Fkm~bBDFDj_ȷl_ģn_RzvÎ͜J5k~\~^Lf>cU?.ɜ`"=^0؞Fu݈u쪮T’@ 3\g gCQwXPǝ9实cqL5xufR5ȕIڟuX0ӿiuL 4TF4HCF0t4CU-W煯s/ W14k*")V9H/18|I q HkjG8LMN#*6Vi{ϴԹ}yk0wUyফֿ\$\ql۽Wx9IPb bԉclOL{VaU˜u~g_dl^W΃w ?#,"&$![Hr$B) AX '`|(7OL} Q^}y|c|(c2b@A(BΈ:qiO7~!&<&>\k-R֭m<\bAo_lsK$@$@$@ {bq f󾌈e@j>oۘ/}{akv]lMW"Mw=@'Yc0'S `G^'3SkbmsAKIN{RJN>A.9sϥHUN#$]{ɺ'?Z.>cܥ+tf|$/[;:8d8ֹc}]o5I IC@2|@7qi)|ԩuga?V/#=B$Jʝh[ϻX!\٧~*X a4JoIdG<9*u` u L_ ~IZ-8wٶlXq bAԂj xa9.]/$!V&Ahʐ*;~#oB8!\  +g0A袋t&?ʆaܫ16;.1i<޿x?:qV9r38CB)o p/ IzVO.WYF$@$@$@ ;ȕ46aZnyr֗vuu0^>~Bu{BY#=Yٍy(0sD^2̭7xNH+! s*%Fd_]񏊌%=ɴSOWcfßϓWT84=_2"ߥB喇, $es<mٱ[ !3?6AruWf~,#-ʉh~HnB9@-leBXkaediE*dݬm62w[Kiʔ):2ʱ) Tu]u] g |Ktغ';N?xA_`X 7`{gs-VyXʺYU u "A {衇>7޿Xc'1 oSO_|ph{5:;!(A삸o @2 ͵-lw1> >!DY\tNXa9sCBpWm~BƌU`\.MlM☵IHHH1߬H2"") $cW@϶'RU -l`r< Y~Ej! _#s@ #o#!sfm:ʂ%_Vٹ³o͒oFVƭ;{zץM[ekNSDnE2{uH>-rMWcK>g[dioesF8k>` xxڠG(5l30R}۫f+)R\Gi˴i lփ|7c>#wcOO O[x@42s]U?+W܎7>V:>)KrpEM-U{p>񠢥6 }/}Gf<[<2P g!xȸ  >ք$sO-ڬlL"x xBöLH(s'ԕue<3 -Y- 8BX!< C" uxy20W@B`}Ήo;! dcHpo%wr$@$@$@$@O5 ol~ R:V +/*M%khzɘza HCB(A\2x-e6Gh;=KHZ-wn?Ep{"D$go$4xTW|(BO%    h AD?=F2w2UV*  ,M&ȉqi-(5Sɴ G麰%p&<0ED$?#HMM['    AuA}vD$$ږV%!~tUS!&%˚ A]7Ht96BqeHj$wyAmPr81 xC_p#)[47v.,Ya=Zz"A #W !!!"’IdUPD~ςW &$ H{ ֶ+]mΰ"RyK7Px\ I(8D$h$@$@$@$@$@$WB/ v;tCYւBdpg@@ s $@$@$@$@$@'3,HKz"ԕpE<\%nP7 4HH$Ҡ5mK*|J8*lHP]#JHX6"nSL:\O @"_lcKk| `Y4ɊiE1#xYWҪ< Smd ɳMl$FnHHHHHH_X'""¾xfu t;/:] R$2QWF4=!RbkʕQ(o\}0Q6C,9WBh7M$@$@$@$@$@A#an\}4↙X?\ E@d;$@$@$@$@$@$@$@$Ј PDjF$@$@$@$@$@$@$@"@)P$ 4b孑 @D%-C$@$@$@$@$@$@$@$H Do]Homef+, &p6HHHHHHHH$@ND@$@$@$@$@$@$@$@$@        : PD+ PD{HHHHHHHHND $@$@$@$@$@$@$@$@        E: E$HHHHHHHH$@ND@$@$@$@$@$@$@$@$@        : PD+ PD{HHHHHHHHND $@$@$@$@$@$@$@$@        E: E$HHHHHHHH$]g V  H **J"񊈔Hx2 5jFjFJBeBE F$@$@$@$@$@j#'jTYYtA"H2hQF$@$@$@$@$@MHǫZy(U@L HAFIBAQLL,=B} =̑b**Z**ʵA\Dj׾ ;f`-u,e> @$&xe     L晔 :sm\D`tGjQ>Jvl&;wlZV&hZbx5 {="2B* $x*>D$ x"\3U$ @<֪.//r5֟:$@$`'$ Hv!     `T}-(_ã_:O{217R}-'E9| aX겺ji\Px7$@$@!42Ҷ W V6B$@$f0WHȇGs#K.p5oR9`>!P$Z,$ E_2w^W"$@$@ UKv ד!qGV5I~ JJKK6XO&vyqEɎ;!&?5,cYHH$`J$&Fy;x ~gD$FӀ/:)r)S )lB$@~w HH ̳sHN>ush@~AjWvU=V"F+e$́T'' EEE{Fz$@$@$oQHD{w ^MɐݔnJ'2o !9qqnβHHo[wio X-)9~TV\f?6;ͤ[^Po/M*UR:Hl uo`ڭȚ?~+}{z#C.Ңet?em9%%Uzk?_P/!a#snO]?uNDD uKKgNٸaUjN4x!}HH&ԺB 4*w2 Ԩf?qtwgd%^sݭ2#ӡl߾mf4LwEjQbՎ:\Üz5"~(Utqٳ{?psޟEl"1jG>DS&&?YUFy SmVO?& CC ҋ| ͨ @c'=S @!y/*&ƫ/=[{%#K\"gZo̸?;));S.ʝU@rUkoNTK@ҵyߛqhDFT]]  8H{V  E &&FRRS 0ͼwYY6JPc>S2|u8Gk{Ρx? =zɥ_:: n穞s/es/x&ycqrCk'xR^!F$@$@$=;H TuE9F9 LԩRw?jxs^hug/*M0/ջ=x}i֬}hq r,;̻:Yp^ylb7';oڳ'Fpݜ/digdqRVVP|D>Yx Ҥg C٢/u{ܣgo*%hx5YdRithG\uuǍ2w;`0Ǒm.u:fXK˔ AK.Z}2p r.!ϒ's՟<|xe"$@$@&=&H:=ɵ"M+PڅŁه S=/.UͅM*jgdigISڎA]v߷6#G99Xͼ7E$+i۾۵ "႗^xJnnQ*l첿\[+{=W;G%wԧ!D# Y++CMHrYmú5vZ{<ַ þMJF5`9# 2lk[@TԟC#<      FDm}c`b5 O˫/='3g!Ej%XlL oe;pxT ygg@ڢ^|Fޟ5]**mX7p7z" yἠ ~e7` +?G s!`Zjcv=nyu` *|b]=epPp|?z<&     B`3da: 5U?PstY=F/<;ACa돵kQ_kr㵗4f#EVw߬UkAA~ԢeWMj][lB ;7UDjޢn˭iV +Ɂ)HHH x'ɱcGH>EfP,;;GV^+8_f}mZ# 8ڴigL "7_}#́!/tzgD4E`& 9JreXXk塅pHz m\PKn^+wySoήrVV!QDj)CPkY]d~ Q>|YץyaBQWKiQd=7[6IB($2 hhDcߦm;hc5x OPF@bbcͮ7@1Sr:H*PwUվC'r:4,Fy YŪIMl?"50S2"ۮ8 UJ2VQD2O*_՜EfeIu/k=}P8swPIεZ엖8\܄SMl"gABَ[يlI~1E$| Zy Ka>_$`- BY*߄[BҊ'I7#&VcCs쏃/ }YZAܬ6e*-nah.5I3 @9ƌf'DٳToe|(7:s9읷^ɧNɲ:-a鼋\U/U`` 8Z/Z!y~EzlټɡZ9e-[ upWȭw+\~) h#aSHZ@ @hcƊ1c4 hJ@f ٜˍl>]' s\f~y'r!@̷]6O ?Ā;D̠l-/IG(꙱KFq 2zq⼒Bސ{Kyq&Ie?l_+1a4StkHHH*lHHa '$WC1ִ= r1U5 @$It.0xH[2jeYf櫋@կ eRBug^$F#"]q/gKLLL8qb-˹J ?9eiu{`tYe槑 @>D>XWa;\I}#'wJӏR' :A^ei=vG۶86]Cd}6ʁ/gk9Z{E\a-b|f*`9N+'r9舙x;ZM#h߾Qp)S\r#\ )Xd}hZ+z9W^t(7!#zn_^g.& ]&.-UY(-Aƕ}CDWuPr=ɚի\5s2ݮ] r}I H>$@@3ø'ȉL @ hLdga%bzjz d&  csٟ/75VŕT̜?w U ?֪\{ZmΘImEz{3޴+YPuCILnP|;Hg"]ОJj7sps[ w`Gtq{fVfz7H+2#Sz'V\&I nVz#hms6{,h2!cO'k6Oi `]݈ܝb9 "0S{@#@غnȍo(j:j7P+=~t9﮼w㼐H04ʆy1P 6ȴ7K$U Ef͛kg`'UoXP !tQjuSe} Ĺ|W1ͼ ٽK E 燲KE^Xt/swm$-DT&KYמwIKh^ G/=Jª<[j`_:**Z_/㥬Nh_E61{4XH<<>t@IDATo!|拕|/[B[wΜҲRAQ8Ep~: @hߡ7P ,  $%#*=@V.y'.5qRghXɾJ'JIzE}`H9:[#   /xM<(YK0'R}8'h!btH}L|m312ھiaکNL2R2J6C@U䋾XrU>p+niww v}iZF-0JZ#oM#   8DP×K, { BW{HFeT+ɰcFIL":BF&eJE3#oB~,+ytM:Y*oSMz8*:Ai5X'(%%r% ))66v"rM>:%C0Z^-/d/I M*l;,׵Sdk;Ro+OW/;XoQfĤΗq'IHf-ٗM?xSR~Z Ef3_*Fi_~$SIѰ1Rޡ_t􅽽ySϗoߦ>P(ԴcBߒW-u!ezIܦu:vݲAT") 0{@׫lFBB>$EfÊ6z~yZ@ݺI4{Mi{պzsAJUN EJ/9@@%||xreUn }j%o +1*GZu"ұɭuwiQ:*uTN1aojն#w͓ZD*BڔT8͓nvfxאX`I  &}r s&<;$   ϐ Giʖ'-&J>jN۷ n1f1`#좐x@ĝ;%άe 9C(z[;&1ST[w6`;iQ~w<|H*= wư [tMQ2+sf£Z}ѕ:RG'SE PHl_vN=O/XeP걻pH[% YpH8T_qs=_~nӡ-ˁu%>KHQgRx   M c|5hGJ|n@ )ˊM:$KVU(bJ`ZDdoUapu%(ociZǕԬ}byr{*yrΧv SŅȡSu@jn&(Ki<즼*ڴ;jjrW;.vGS[lrUCY =k*~i{ujzē'˖S !4ȆC|jj3DV>*[jŷ; _>wN8#OIWܶWIxV}P7G*9vN59HՍݾReT[4{uN%C^jTu)?}#hVRO豰=#B  $HɶXW% 4%on Jpxڒ-Z*J݆~? ţpe$@$@$'H~e}#RѶZRw|~"%l{:ˊ. sLFꥸ$vVI䶋,"  hh|>jϊc$ hR("5OQKL ɩ*$ O:M(ߒY L mʂeK{Ks׹BSQHHHxt@$@$@x?c DKoðzR,&B-I܉jE;?~4ݡbUJuPAaZ@'mY"JeX{KշJ[*E4W+/Un/PXu\|Wd  h(5{' h@("aED#mS ݕ1&YnV{l|ycR0}t*olZ:JQh5H&vQqz.ĶNIiU;xEtZ3K @->QKv5koҡܧd>TdI$Aѣ$+%io2`V9*5J1дk&&F8꧋ۤ?-ʢb-4ࢬ[/);H{2iS[uͼI8\<;Ao?犆s      hl'S,i*T!~! -tyJjkdT^ 㛹F۷JW2|H_?=sPt:MT.ۭ&t#`; }8oKK)[=:Ay){%~{07TMii)Jf/P;K~٥=*ZqHq4(lZ=ϥqHHHH$@Op^%6Y_Q&'.M7A76`V&Ak.)W $PJRaq{h^>Ρ^}R|*XQ-$3[;w0YE.Vީ 6rVt(:ʌV0vvk}*rxbL;L_.a]+ѓ$MLݲBvTUxH._;H_^]>GVNzH6D)UUU- @#@)Dl_m2ց0n,rYn7E !m 6+o?協Ėg_^Do>qtu9X [ΙR׍ݶI L坺ړe}UkeӦ)xIi߻ϿkFZͣsKE= 4x4[=76<ڒ9JHr kV[3f W3eKJj*L!lז}n.+e5U: l+Kc۶\ՋPS?B$EY҂\Ue&P 뚐싮%>x*-] Ɲ'm+n۬ϖk}'>v6xn1Ad:_et['X'* Gі@&4g q 2L$@$X )DOvz6[#-V\˪,b 3+i*iutUpU9:9{\F}$kGڻT~Pǰf*w9:IZiahOyʒJ}vZ]?̩mhH^Ңj3QqIdqT'&k%`7x*+ [UHƝH\MArc ٵ:ٷ@ة[,^ /-yy[^cw<*K!T]r%GhBf|}znbXX&x?! ʇQbĻMEUc N"e|yH%Ös"U"cоΛdOe!rX]syn;ŒlѣRD֯_ET[2av[vla*GQ|m?gb9(?f9f]gwI {z\}uH//Fjkq{e/ud%}WYZ=j'*&N?cEyq2Q?#f?_\jK|Ò֩j^1ٿz;EEdٿ/Yiٮ-W"Ru"X՘1b?p)tɢrk  0$pH5!/˗GUJVy>[.R9i[u7+g"ʣפEDۣvT>ݮyW5Wuj++lJ{K:DzC 6&VlYY˛nޢ 8ſ-BJ*=i5X{w}s7_,=NJ:9Wһ WJ+$Qbem-߿;s__KƥЬ6EzH 1sH5U>e"=]_}xJ՟j{c!G UQ/kXnA$@$("c _s=c#ϑ7?D~'Wg{`roZ!䬬[/cDUQ$|jܶ~OeHުUITawٻWI#pI؛RZ#Q.Ka\ C=.ê8s@V._Z E"R]qI޸{,tZ-5Q3T1է}>abp>]oe4y]#"٪ddKؾAؼitPC~BWYV,xUڲ}Rcx!]gջko@dڣjsdٲ},)ڧD<a 'M%OHLU˕\^f[YӛkYY|*7p=F-0K~BdUWX h$@$@$ BAC]'P%$V={'U Iw_jEȢ].=aRVo$d #VydwOZ!F$@$@ R]2Qj/MĿ/ P}jWr[aOڿm\O^oHR (WH }۫QΔѺOү+%hr#:D޵SHQOH1+o}de'zTMhB)Ͼ1YU @t(:ptQ;ۻŷ0sDPF$@N 99EF'" ~Yr[x#UUU7/rbji{OTGXSYak3v[U-`zY2ܦk9p0vqq7Y4kC?m%oOR|PbK+%N U\X I !l@dkА$99\dJrSF_@k? @E$# ȕU0$LP`#i'3h#簽%d=N"#"%KӦm{*wի6oM7Hyg% DlJsiu1Yki5h 4qH/*ikt}N&h+ͺڋKi`=]/g2{ ߹Vһ./(YJKmKd[Vo\_Jԋg'1)I_VTkH18bj%mxEJL^Sd UPiJINvJ|V!6r !-< Ԋ-[fznU\( ы ޽{HHB "R]tY ^t>8Z$ջ 23[;4ԞD$T7R8YL)_.ȓ n[w?,?xLC}=k=#.{B;?NzOޡl?)[~>zG.ےeIlY{lt $BS $p !$cq"V{/9ծZ={̝yVs|`t/ROU¶8iɞmۉ%+\]\U77<! "Ϳ~J?2:yt caw`=D/-eoj6I=s eRdd}f§k k kmUei@iC\!KHrΒP$"a[ Ǩ8cTC@X+Bŝ\}CLf^܅ܽkr3&g7;LOw㨠 S(<A`83ʚ:΅&JJDq=o!pκm7h؂@twչ[ݺDו IL0ŹFBLp=H$gcU fXJrIr"$KM>E 6,`1FjkkSZd8Ԥ5<}&"In#mյ\!CA@@7Z8dm:|h4|OSzHK1=mձ# m0}*"zi1ݦ11\@@zK@),  Hd[kHN+dv-@w $B2!M4Ҽ\+vҶm@@@@@`(zߞ=$.6 >[MD _06?j+V^A'r9ojZm\g2m:؂p "/1e/* C@H2G-Kގ&'L[7 9X# >dK):vMINtpsg}*"X5!IKΣ+HBٺc'A@@@@`$:A9٧ G  sI!‘$UiAb˂hkk..  0${#u4^&g=jL]Jn|Cy=[2֚;8'Qkc=mWZu@@@@@@@l@H=e%۶~GB1I>XGήE޺ߦFΞJvbZzm<΀ YCFDHZ5I,AX%?f؅PC%dܯ#j-O\ۙO՝<_ǃA@@@@@@H"AiԛPd:{Sya% ɈEPGgBGUh*:e ^KlA@@@@@@`4ID#-tMD#G=k'N>;K7R]Iw ~|M֫7HUGs~R em:'O_I?$tms[egK7SMaQג{(wyDI?b[ԾrTh ?%}Zkᰳޡqns(|%쩩J8sH\a%x''I-s]rqoS2ښMQF͹\%ۿ\)trs5LxHprv%\HؠFA!tp*))AH^n[{T0vMD$ZjUN"ʒ$:52ɋ$. :ar[1Q_ov8~ގ#̙'j~ښ>輩~*/YeZ 0$475QnN)Ғb51c)~r"H%;FR-VۑEpyoh,p۱HoMBa0W rr!Q:u9ЪLZExI8!E" ^=&cN~X7ITO&UIkagV ԗ֎LKs:sl3d\Xm^\ '>D$WsPuRżjmS>u+i㭰Hjo(WU~6)1g6E)KTHJYM<,#]O.^Jۤu^Z%@q/rylki֋H2ϒo]Y_^@,~\kBֆϽ=27=HFVׇ;p1q@ZNӷE>~4yT%t~[  )9i?͘MȜefQ$sء+1}oo:%lwg2OoGG{5ڂUTT8`JKHR9a$ɹ6je$ZkcE:_rj79oT"KʺtZ[HB_ųHBaK(W\]FS=*QTSB_CWϝ}Io6LQKWs1;=昪'9r QBv}Y{jՕSYǀ)'"¦_Hy6"jmj'8G fγG8lOp9'O6g  ̠q)諏ɾkoN;Vwǥ0j&%{F egO>o@8P1֍@@{W@;{HΔmjy* ;`(mW̍>P/VRxc\HIG)b& ~yʙ&@?wOCE'wҁC|yV" @_G$5c?TYu^:2 m5p͏L۟Zj!R72AO_R5P~.4qH'>z^ a^QxzZxps} n /9_.svUmD{D% @$1[/ -ZN_M R%Pk&n /XFm, A.:NW]ܫau,QQ^FǏևw=-4HRyy)ܿHH4"eA|oM%$O<jN+Og/ 0$ PCCIVi0ax Z;{?g V)nBΪRثh*\}U`a]&{ a;$~9$%3 >դ36A@)'dm-@iy!$+`1INOV]ġ /F%^O^|*guwժFyi[uOLLϗA  0S F5!iμ$ {(a&$/'֨WPwfP9Swk[}@7r',&ϰh+Ρ,\1n|)GoT:s|*?yӜܶH <SuO 0T 9E69!dʑd>t83EqWF G .& =1~G?Ѕ4T2 r./4/1YxК*IqxIZWKI2r*8-L9_/"9 @$,$|--5Y&<̅$J۷g*S hbCm 967Nhwt";3^3;; skI4$H{[M*CKLۇFW6Fɩkwf wIbcI~PFC(!q:9Hskx 4aY|"ɇGNE:?Ume3QTa"*M$J*a">$TN93vɍ*AQT!I{IH$Ẳjr;]C-IrI~'YRBW=C"b$d8 'im>'(jնN,ד$A8 @'sswWF‚gy!$K(@5Q]Ń`rdoÇ̻wOW"1%°7e%1xܲ~jIMm={)[idqȁge4 H98#+ʘ5 )`Wc*3 ?)?I@)LFNfaM9׈S\Ր Al@w~Wɶ^[@`8N4&b,ME{(n"ye5&6Krs_O~"]qMY:/1fŝ>"6/ R_DBoEX*NڡVyɟܼ0Y)RjyTnwDmdpJif_J;`*OCy{)C劓k]g[sI?:H%  ԤLr^02ed$LMT"{Q4kfܜy>j}׶ô%<#ٵf16SO>:6WBu9Ķ-hYUYa֑5c?ש7} A@`4^ @vC&;w`9J$IWHcO^a$Iţ#yHk!1ɽfZW+2VJNTaeP(՟x;E^a1NIؕ?g+yETn}R^\8\Ժ,m[U(f #2ۯ>7;w.L̅Y,3S&++سUmwmt_֕zOS +z@J__J^V^c7|r!=o!M@@`4\_Y @` %v!z!sVyXdM5aT2@,`lZe lPU8ogxoxZ-s;+stqY'B F*4H<, ?演uӎETU3F^H׉kU[KOUSg+5?ƾ{Y)v?Km,ġy[h 0 ]nh>@/ -E89VY\FbItzfMdɝ(0 0< %H%1$0ddZ tU"5IwvEf^M4%U}9b9Mˣm؍~Z#7㼺jo$`  0 @Dί.  @i˓+U2mND g$y/Ufo$T/s{t%Vۅ1 qZ S}VBy%f$KNKuzJkdžulu_l.$C^IztʹW{36b mTtZͥb6?Eթ=bf'q*]jɎb7o?UU۟oW[_sM|JB`  0 @Dί.  H8u5qAj(%l76Fy"$ɾaؚWUo$m>ަӿMVFڪg.K~;7QEsn:@IDATO$O9VK/>WQoSү&u?}]*ގECJZ6 gv3 35[0/-y@&w^+0 `fN8H4qTGˡy5 q 5e_r>S`,4!3H*6S'Bo*tδgڅ鱃\"M` kR9 S-U[ НUDWc' "$If( Y qӼD`D%~pə{HrmCL@zŝA6^P}% I鼺^t8**=.2i>O z8x7V$K|q3<8 C@YY)S^ ]|r5^Kg;5;L=Y5%@% GY3Rqq5qX @l@w~W^Q@@@ %ʶ#ɰx nT3' l,@@`dlp>DƓW6S]q`E }1| ΅V_JSp8m7sņ8   +zoͣ W:?5_O}ʶ2j(/G!Q|&b$v>=Օ˱3r Ӽ)%u<@J"䶷t9렉I&Oo~Z Çx!I2mI-9vI>$y?mlнĎΥ&T[yr(! v@@@`@@D}s.Ls u7t*"IYHR9IDQ/#zX/9I΋',)^Wi$?ӸQ}iJ+>hȞhO_Q?4swJAcz3iίN;_^Kio^ n}-U%S8v'Ud9GU?FKSQ6"X#%H~_ K߽â-0~{4]ڋh&USnhi~>.}א>.$=3>ÓidH"nybi`ɉ2uˮ&__?%2IN'81z@@Va~3h|kh]4ia~_M@*%}4}?};_[>`!m%@~;(}ծ:/UKT6ǕpOQgh?SC".yWU}#Wљcԧ/)AU\E?w'֘ڢ,:/;?[HkiSbZs}5Qsm%mxx}%+܉XJ31xVxxV-PDDuZ7ґ4VW]q 0r z!I2m06WeZ_&4/${iA@@` i>V2s)T[AC&QEdh< $TE8,Lqd/xVWOKSB>*~PD~Ө$baHڲK7 OL(ToV0Gԓ<= ?WhllPI55:$ i;[c:((X>?tD.^&E<x4e%{+3|6ׇqYgzW&f$HwᣟVjRXŽMӖSUn2Uf'фu$fޣtK,Z4Im*"W&Օh⪵9瀺BԷ2|kNkU䋙X$ruspTIaG~OFI{."X_s6a #'QYY +1ˌi;82]CNTK cƊ3Tt{ NUŪnmA}q"~sH_C_>;Cǩ*306iA:o{'x7mdCYe$ a$ TP-iמ9M =Cv9Jr [e$aab˞߮UVGK} 3mA%t̻pK=27G9;?7-VN2qQ52G13FϽr]OOi"Q$~o9۱@,"9]s~ש\h?i 2%)\&9YbQD s\~AGu|Qrn:hXLxaqoL:j?Z_[Dmm[1H=F7 ŋ3,šIb HrFGU\I<Ez9ibɒGH${t"ɗ I+a3cj}ٚIEg2"-7:/du1U sDY*Kq'yi+9s.e0Tr#IrODI77¦]HM,䈸#ai"09y9 2F)UTSNA (W'\iU&ܧ}Ŭ§_DeRA'^U_fxQgTU73F֒ :/iM 7pt?'xkt(oGg{%zRs!1ߡkDz3WLG#V[NGמ$owRTO%7SWuN<=6jCoP)ēWd!;~r"p#*ϣ&b@DAC߳ GB=Z"Pi^; CLW'jn GGg&^OD0񳕧)2Ģ0idQ@0ʊ " )_e~Qn ֓h A"RP>E * /# ?^ eE8ҀD}]0Coӗ>=ngjSTmY-.xr4$_S0"pvM SN@Fq w,"-`~;ү [ruIl1 olΧqѱ=@@__S蚀x#I푔 k*5xU9$L[M)[ժhjnNm&-#TJ9jOP&9 z^'{Trb Y+0:YiM,~*wPM~*y au+)eoW݃FSScWMp@D@G⍴opѱ(U,bo 7uZIsWgټJĒh OB[ȴфNߟ0W=ښh+XK>QK(t֥ꜝ^DU>ۧP RwU@#НUD= 6-L+J#-\T{ܩTWaÖfPUAE"Rp2#`1R!OOE+\aviۯ Z8羇_OgA@@@@@@@@H1H$bXys>7=M" Ōe3Cc+>dift =R<7H[A$"Rgs 1A 5ZSc>߰      `\DꌘGH!8:'/? ~O--K kf`      È@Hnn CxޚaΤK?z:\|6CDؗW`G3|ա}9{GgΎHDLҤm쩽lfk@@@@@7VDrwWc!7Vgڧz c27D,k(Trj79ӨٗQRֽ8{߮e/Ƶ:oFn%7Eؕ˸} IXB^ItWTx")$4(L6L,@@`hSihN_p(6Ozj 3D,1E]p3~GȻQKCNwvoHT_V@^"FMQ)8{\uAO L[N''ґ Oa+U;{GtNOO$yhKZ8i^D~f$"RήO(wo4򏙡U<6?$t.?Ӓ'f Ã=;o@@_W~Vo(/T%y{ līPϦYGӲ Q]IY?+L%h%K)'I~jEvjĩD^ڗH;Ǒ9{Ә* Vɏdj֬ˇܠNxHU票1>;rO7W`.q   `z::> :;}5|x I\LvJ퉏7 YĞJvsש|'RQBRBO~Ө<5;9|#'ghyDҶ.%Ww5PְF'PWWKi)SEyZ˛FNpH]$9ӱ= ŃQe}KL CcA'du8#P_IھM'_ 3W>Lv%yTuyReV<Zƪ:N+jҿ_[*@puu&j;\_]re6y)8$AiU ӕGgxVC%#0{|/,fڷg'.hfgWTSQ@1\@@ycUbbzZK0ܷf̚CNF aDgXfكrݏbminڭ]g [`p293#Wfk(r`QW COEdZqt}$I-ktp^#!ItؠF'ÍZgTsJ.f5zJZ+bI15|joo'}L}tZM~hVUUAw,eȉŧ+(kmj Y  04 {H<Tfz*E)$=KWescIL<Ϟ@@` E|7h 9PO$HT n" 7$>}f@㔹=5Eu9kW_*>S%ͬ/0nɵ>MeTU4q+2-7BW3IO A; HR瑬PCm-غc (՟b۱J@x"eWњ(kXOQQɧ "itI2CB :A @w~W'Ro# I7~K=3#^A{^YM~&NOA Հ$uCy~pY[MWд_R5vwu-P{%&(8U*Smo-ϩUQuS!1Y 6]Q`V_[th{Qa7jdv Zmdٖʇʸ{;Co$x!uM̙%"0^W@ @Dꊐ 懲MQU~y{%ٓWX,Im 0=OD\ٲp%?s59{Wʽlԥ5m`d    0HX Ir,Fbee% {5++Sy#e aqi̘0i "W܊J.kZљu9G@ IF#UD0p71 Hi)fr+vc$ ]Mή[@_@D<2' i$$晤Ӷʵo$;k..R}}]וQ@@_[_G     `S43ɺPkhlЅMn|@@^%@@@F$&&icVHx Sm[6PsqM+A^ xh      =cKK ܾ˛  06q5NjwR F0ɉTTTHO   0 " ,o\ @@@@d6 gVgP   @" @%A@@@`aHL$Mjw%$u՟@@@ "ZpNtނ%tŗ/40}蜀λȜd( IbS] I19r>'}g8 XEKceoPT^^FyCih @I3"lA@@DA~py'Lcˋ$gTgSwv\RP=*^y dp!O݃ZX8ppp)Sg+әf$ѓ0ey/KATRrMVXdfStxHmSSNA9jdhF,id$u }D$ȄnH"\9QmS)|%{ w̍ j`/$ =*;IkjۇzWVVVXA@@s}kS=pXsx8̓;}я#,eպ|uMꬻiWZi.7Z1=j&U.uEz"4+t:[F"4t1TZH+ܠʥ漸 -t۲ڛRBi" P\?[W~X( HҧITtV9@ @Do6w$g7yC?cqݟQڧ -qpq#{Gg;&efhe U9e^1#?T%θӛWXZ9 $/ۿQK} Ma3VP0oXo$C'ιgfєADr=j\IN)6ݝRxrc;;{j)7,*]b\yy҉qfhU)*gNDJ~.9o1ljО-YFj_T^'({̞bhb۴v,5;Nޅ'%^N~6`|rpS}d Liø0W䬈TQLg~Y1g7q;ʭ$ow'ËMhiFūϺ7_v.=(n=z:}YZqt}ha1YI'>8B=]5.zox5Gw-m[ɅXk#$?EHϭ ŪJxX"Z_Du/rED2g^'X%"enٜH"eWP#XMOVMMMFLBִJH IV{42VDkm(ׅ:{j-V՝j5huߤ=˅W֞8)\س8ˢ=3$'7YU]3};|"M>YΉ,:BQs݆ki@%"mX2 o*wO6Yy H݀5j 0Q ER}Zb5E_xsx^.(KOEGx#Fji}hYγy#{pB yyh^>ެwZ~A54GAS'i2OkWuĻH"_$D˩$7oјE7tND*9yD"bTxkH=rOIrDMzgкbխ:V˕j%xh&^F_xQ/aE=9_57{ icmG<|; 0 " +0Įն96@s:jK5 s~eu&S0!_Z9$ V?by rHFqJI/uȑ4H^/Rw%bIlC'>z^=$ܹgLoj $RAQ=O/͞Ż 6`dn--Nc 4We  AW 3d8Oε97sI /͜[QĤ{'WTVq35@$bQU h{F% Q{.i Nhi{-eND>j'ɱKOКm6r%"rX/ԶO"$5W )=vwG%֖so?K9K9c?,z55sٛ{Uޣ?:=Y(Tw;CB;+:CҒYZ"ycXVJ\ cyb҇ȹ ЗG+.Fk#_G"3?⪜H;?x +v.$ ׮-' }:tnu?s@7 F𹈴|Ff,xkqpr3WUwJM1WI%Rz2׃/&#B;OJqb-2mGe[A-d!yR[w кQ_D@ njD$\P      CjK)8qڔԩ>ocZ)z͟uq Rv +@l#UA X*KD! Q|F ѡcXÈ@%9Y2jjS8I> W)lBWɉƷu,] C; "k^AzI/yp@@W q]e? !P'iB`>a)'0nmuVGE`u68t 09a*yxzu(G2fOjn~0ԕFYf+݇27ouTDVRHO.^Lu9Tv|CI#& !M^"mŅ;ݡG6Pu~Օ9"yU\'UŪ;5 !!΃KH^$R 4^! `q/rdYڕ3]VҮ7|V{ܙ33+=s X4pĢ%(kcvA!Lկעl+Ga1hɔoBEwc?~MYzقҥ+z/&ݹ_TƊh<6hǀ@;; Pb- Eq2_Z]l{^8:v!ʭT#nAqy+0(OǼu>3i1綟:kmks!>oҐ I xE$3 䮀d_PI2GWw٫}q|Hmx&[ܞNzZ*ڿq3]hs{J@Zf/",Y&YI$@$@AaHUHuRq{qOx|W8`, 0b7"<) ˕'Gɛ62-qX; so\_ʌzToƄ?wx=moaG q!@i\m8%.wrV߆#<7p.$nA#C66;ҙKp%8}fƒ}}u9?|A}ڳHH@PX${oJ׿+RD2!8"N>؝mab"R-Vl[Dĩ U'F]D<q9hhiWDJ&<0C+I|E$}Ûi(&_3!ss53`_wNi3R=Q˞xrlkI}Y斶PxD$@$LҠO;[a55cKݹ[UJtG@> V 8Tlsqnvk' lhӦ[; 9YfZhODr^XA$0aPD0H6w}ԜwQޏm6Fja;XXBVGˠb#SMwy|p$ Y=wkk3,mbN{V-@;k"&: mv6ٖ|d)"g":d=5w{'9uL1a9&&N}{zI/~=4n7;2F$@>I"O޶MS=!w["FCGR/H! mnA ,@~T; %'O%y:=GE ߊqڂyjZsuGv/ t.y$HH 8gDky:ϸ~VWI6\Kmjv>54=y5 V]pOCUԨEN%fY,==\T}fp?/q!_Ʒ'uH(ޣ/OH|@JJ*cbv.KԂrvcmYIL ,1K{!$h$Ct$kcC=v3 NYo[^ & mB$@B"iu6؃ }eNy~_|7:K${nAq "a#dYF5(\__و&?W*5 ih=?-Ւh*qte3=?EUe&p&@əףJ/@tFѐV]ioͿRל/p 3  z5 Tl8Teb! lD⌕tTT0uSZ]tPpw0_O!2+oMWZ(--9Gls f0&ʆ /]Thn5ײs("J=|ydz{1G;-" ~ںۈ[P%v$@$@$@$@cE $Ry]es>?%b"yjLῡ> }5=AHsbij!%EY꤬[nۃK[ B$01d/K*S4]*Z0yKWEb <\c <} `xH|Cڻw@iq&tYƛV{O6r3ʓі4h/%SQ=3BKsOgg; uŏ׿VfSmhUD;c6E WҴVƨtvX`Rut>$NHHHK '$'~/%EC# 1?[ʋiMM˲-@Mw*ΎD"ҐpMU{C^,$@$@$@$0TF5[Oi4(& x$=CcB GPDF!   T[Sښ (>b!X("MՐ W(>x_ɑ +*YG$@$@$@$@ANMb8v    #@i2 YaR6`@$,L> )^$  pqǦE    @]m *+O!9y:<9|lH$@$@$0rFΐH@LL, NOĭV+NԱ>Ï-t aضe#r cZZPVVa.$@$@$@%@i؏H y i? 6׿|HJp1Sۓ X49 "PP4M>@cC>>{Q1=B6;1a1;Cի}`A!贶k;tuuBpi׹$    ?#@n8K$9FQ]I,7(1ID !:yq\B"eY|cљSTҏ֚2CU9 m]yw *-?@G q9sJJ,ZOm8Rf ʷA阤5 Suc\ @|B"UVZV[XL>Tr.= E#}5I0ibl%L]+a+G3M%{Iag_t*VHF{4dO§GoK{~\'tM+!ڈ{/))2y J-Ah52m`2F%SU j7pbϠ6؀۰dٙHODZz^U [?\o(Fm=z=Z*j 0DƣV׉si>uӮ՗:.֖c_@沫= ޿ݎD۽N/Dh,ه ?E=3Xݺ HM >>5Ճ{! ږ H`("1;`VJ׿+nԡ϶ˮALe>S<q9hhiǟ;<ERLX#>5;n6HE ^`b^5ùl Lկעl+GIܟ$"##aT::zJEؽk? 5mdFB|zI9H(\IUOU V 8.utvUb'uJ}`@W؈L},?D O8 ֊pTk0!IXH`4XZَVXMX=Z;fBBթbriv*@T6>K[;F4:fb7_Gi|G/\}}oŗW Eᒛ2_J]l{^zQ6"'pR q +9Օ8Uv{:~I{cHj۷ w޲5NJ[kv؏H5P[D"i/^H'kOlcmmV1ld{ϪNnAώ$ПgIy} 1)&}EnU۵B{>@ۈ,0d=1~>;n ruGV+:]$ȈD}!OĸpaPvf &5{HE$g~t,H Hv,E}#U{,)}ufT ,M ^C2/UUZ@$ǎ[hrLΥ47!uyZ@j*;IOĊ_IW>ǎcd1H`ЂС @.w"ؽ^=d}32Wވ=W!щʛ '7o ivaMufGͯanu[v 됤GHR#?ɫoWOoŮ?W9}YyfiQ—̽lR;.\Rp!Ը1"0$E涾ę*f2"mSMXJ6pΚCGǏ^و&4kqCw]P{..1). 7FdmS0?Oܹq֎Nl=\߁U6(o}zqd#ln 秫yьcZ.,7?3)zއ˛p/>!dmمzεj[u?ܳ* '?'% bͶ-Y6^7 [sztBa&"{#I$0Dl4-l)q*hmʔt\v_(e$iƙ*v0ʋoeT P&NefZ+cUeLZ)j{I=rzbŒJ.o+G$*Bmis.1qj=scL6x2,_Ƈ[jQ8ޟkHH|ǮWKf6c{=%v`p(f~qTx >UnGkQߔT՞}+TPTl=ƶ_ݡۈy_Gz*~Q%_a SۧcsD WsgԊS_EhBbU9_| [y'O^so=EӠ̘5}Unz|$.~=4R[$xk[JqDG3&`E1 'PQ߆!Q~Ix8{esYZxiQH"Mo@ j[T WlknR Q1DZSU\g~o3 B LE1.P{}KJ-bO?7O>vYSNi9ҕZk__/>reHH`bҦWI`py݋i+#}^?.CXy8ݤ1җ^A\Oe4w?-$Zb9Ķ>RHR,-gGbD[ŃSmt Rgd&Fh!CS^D *Rv)O"^r[cuϝOT'>»?:$χޕC]~^l%HWaωzL͌X$RR&iQIe,"^IDC$~U+R~zx&-KyUVD_ܪ*0U̧h  "xH3狿V[ɂUjXT`S ņl}1,&*Fa >zG'ϝ_"ܮ03ܲ!|07?/2ӠI `*'/5J,R<-ie56G?Ge"UN?S"OǏG˖}|Q`vOyHHMD$ϤZ+O'Py쥽Zlz t~c%H_ 'E$ӛn;56Ol :.,]uNfEDD2eӅm=oQryۚq#p+`F!bS     8'gS†orޝڦx%s^5d}3vIe[]Tvש,Ç8ҦWH;>fY{4^է*yctJG+3*R3*}0=Qg^sn$:o}c$;?| Gy^SOkT 2JљS?z4Qi?@ͺ OznHH`,5C D 0\oBBG#ڒ2w\IU.7**MM==mFhrA*l͂C'dRY~$8fTԷxGN5>!Ćhٔd[l{`&`WPcQDTOo<ť(59s׿֞j}P;գ3qH_tٰln=9qX}{tR.3* .3* '|A?v> ' *|FyՄ9 G@ݶ:߸|U3(?* Gl|Pv6߹ɎR3dxӞdT3Ґ1 ࣟ^ⅠՂ}IHLv=41[2*ٷL0Cq֪0k|C`XHHHHH`4(C$0&cb$Ejk0=&A @{٬8   &y !22 VXO xE$9  `4B-bI{<>!ѯ(>S?~#J  _!E\w -G_ G?E]1iKaܣnNҖ\@%|jӫH]|h y :TVq'$@I"o7ΚH` TWU9fb0%.$&&aչdjO>BzB$@$@@ (,SνC)_Ǖ5)(, Wݦ.XK+5[wY ]!9Rp!Ԩd!#-fs2/XH|E$߿\ H,3ٽkh ^H7L$@>@`jo_ @g,g(Ӫ긽Z^gbP麿!,1 ).@l\L'8ޟ'R1'¥8g"X^U "%sǬ;~OhXM}4lL!Kp%d:h'M(Go'.~MS⃵#2iH PD^3y:Q USU j7pb;fm Xˇ?Q?As>L]@JJ*r Rp^TUV>k Apa 8hR$ APwG)r=*cC@ů]jk\%%c@m$ .2ϺG^ԭSOϩgՁ!aL-5 >qDD ?B`ŏSz>̲9v^n?mmA42wP"Q2B>J$#@n{JTH /"' A"Ss+kŵ( QttʼnxtwĠ.$$]I=/ I >>% uuؽk;ZTm/VK~ l<9['7X-(-9a?8[G/o=f, Ʌȴ|-$" υDƢdC@UJ)*k"QD@/WIǝ/xkԎp+[Tt4l^M~'rH٘|E$QX|_{sU"Dm J9Xph!9X_^'#P[[];dYmĤDDDl,HH4yqQdg~ x<WQEz|W^J3$y*D?)4.չǡé8M=S{m=RoC'7$-.YR}7Hǝ# x ϥMiE}#AElx=(+=A`ђ5{{vO$@o$Bט6hy'e 8n{s= = mJ 27TطE{#bGD2ycz=5 B$01laN˝շ;Ϲ.[V=mxf4+3. DػI&>+KzOd L^H*Pƙ7ֲ="AC&mm'fEK{S>ݺ"CAo VțҶ[揌,gdlŨ<5KY?g1D /#`IH{4&ӑ---c2 ڙ*pK@EO?AQMA'/TӣnDyw|Fv!.wnsͷXO@pz&/$_lkY =G&  5eϾ/;'nҬs %4D0u9CƑ;z⌳X}(]VV:"U=kMH(Zz괶kN"p1$&Ԡ琾֔BKdZ!*sAP;h Xш$?aPMNQ5 ݱ#C$@cH`(WQD3ҡ.K}Lq}ZUePKm.6C[+'7_?t>~/myN$0~w}1Gࠊw#3jznjnꑵM~ D"an_A{DȒ͕}eT]ʣg`L><mèW~yJteym#[h%"5zĞjEXXT&Šr"L.$>v\UàҌ̶狝x͍6L^y=B"\Bb y9z $0 a3`4EԴu=V*V<CZY>Gh'˝|[87    :?!4m93 Lե,lv$$}ԥ3 S4juHH}HE$ ֳTO )V޾m:::t`44i)((H%϶ Y'J$@$@FҠǶ6"0!V{Rf\#[RS>N4^H2`Aj+|e[C\d"͉slSm\HU6z}YW 3#b66^ lnCũrdMֳlmRsفuVm]2wΓHHƛ^OAĤ`[VWjnM5(qAڤX`em3!(4Ѩht6/*4kJ+` ޒLˌwvHl4b=/ ?H8 $թ+>ݾE""ΗĘr$@$@O#HIaǹֵ\O}c=$k*Ϣ0P\קTǐ;&tX22'cRj>jܭpN`#K{QRՂ?>o@ʄ!"m^mGҀxH`PD7H 47_ɞ-5-]/g'^Hy>3iNHH@sl~z%"@=X:5w{'9>Ik)q!l[mA"mɕh) /GXU9_QU'?~l/(A{OmWܮ:&tKDµ~Mjw @wZD%@iYH`ԗ)9jZ~̂QnllPNÇb 2 =#6$ĈmD;;HHH t1muuWP" m""*SλCKĭ:fInzDe_[֧>[nQ_ggkVdk%%& ""Reis$@$@$0! DLyAu{r]*$O-_s$@" _j=dm,ZB H]*D=;n`D$@$@$Ї޶]>$*5V -wbH[x1RfaT8JP['{iH&:;Wa,$@$@$@GNH*wDj m^ƸOJb;gdlŨU#6:sz;K'6gq8+j݋ZZZ|vEK0ǐ7DLA1\1(!B╨4K@X$ب>:|v\vgU X$$%#tQ[iX1=3'!Eei @QF v8xC: '"]EO!kwk*.CTz=s/̍na#     Ca0[yx ?œ3U"I<I1 EK̼!c mJ>'l++uu(cvgC򌕐љSa15{FEw|hr-^D$OSsFΈ$@$@$@$@Cܢ$~eI|J-6Ti EJI 1qJ= HI=+p6yQw?C[G>)gqs}K)a(^&A D6!OE@P0N{A N܃o^ JϺT'j /NŢAxr4 щ@> pD$ Կ͉4m9̾S L;HyxjGe7PU|U$*Zoxf- ehz;͸A_~ety#:Q     # XHƟEn ftڊT)"Ig^6TW1RoZ(Ϥx>Gg~3pfS#.]VtZl:-mrܣxVɶ9>5߭Eܹz H~#2ԫ`$OlvUA[OW6;w*w|uG"ՖCI -[褬}L'D>XA$@$@$@$@$@$@$0("9($ۙsoK$H Ho$$CD -6޿ڑmO9Oc#/kk3> U}XAL`Țvw(//g\; 4G2=k۽DH2$Mե6"V$ aٓNRDLJc!@Y s     $C hm[\!IGlasTwx鐎%+lBl= [ٖ&SPXԐI/B S˿2\Ygcmm2Pv8B,ZĠYsv{_yccv܎ǵȡ?  _'[@֊k]bwe tr^=g(u#kF_@ʨvnlEzy(L=u%.Ѕqcm1J47 ]Q13lFdL> ݢg@@N?ۧ+ Aۻ{4M @t{u_HNNAeeϭpu }eÚ9qX}{wHsSgԤ/I6k[3:llۻs T)/.tߴ t&Sˠm '7ӔمV90,{vz[h)!;7xeE3-C|B"ּrHH`l563($@$@$0rCF{L-ףh,?1[S Ry! ɹ3$6T mKIh'*%0ނ!He%x7_3ׇt4H$@$@$@$@$@.brQ?"""}0:;Pgb!,>խD@:3ne#=3bqU=v3JKOj">>A{`PöloJlk =ڌI)z݉G6IHp3O'VLXiH+a@Go*ĨاcH_YƦԑQo F{4LITCp<" +ǖXvD!0ﺹ~urV߆+GH뀿;yqq$0*諸3p֪aRPFMuՄ['D$@$@@ 6w{*AHt"|a9f'CQ=v7G厷G=>Ҩę5; ;RsfMFN$ 셁ضeJp'I D!`]DLO)va5X١ɨl0tMڎ=#$ң""ldz F&5M6ŽphnUM\0 T,d{\uLϡ!_i0]jBN"p$@>G 00P֦s& ,F*yG'R7#sQxNKNn|G#?&-߅(]'l);7J "|EL/nJ>ڂv; 8PEƕxG!QZڵ^܂P[v\Y $4=|55NW~(PsH6n$.(=c7&Ņᆕو );KF;"{R$J?oÆ=^^ўEGN2NǴ7lVucZۭg;/{ϕWp'hP wv92p k1 ;Qd?,*KCj<[SXI1Sf#eN~:'o ("U@Om'OA|"Qx<bg+;q?!RJ/B붓^܋QYP+cL3HEu6I*yOWTMigJP,XT% EbR Y׶᥍'po-z}L&9H ǹsR!uLSپڃqd#fxeS)K_x(5aIa`n%*hNϏ$0 *Js $@%)9flddfhF466¶,::)X'Oʺti;2Zp[=qFcI$@$@N@vh <^mk({!өͯ@^R +ϣ8j/ Xy,- 7E$b8]UtmsJubw<đןrKҒ]nbQlCBhSIFZUtGΝGxG WNI_w S]-K\; woM0*V~ivSwѩa$("IF@ T_xLLբE$sz˷[ӸgWk:a WH _j{7sTpdǕ\c!I"RO<# 475b/J=X8v6)---c54!  0N%.HbpVwX[Nʘo, WDRjX HjT4)iw/W9ޓ㰐 6G%H&@mTlH$@Kh dfMѓ(++^5G'  p@HT_el#;2-{h;1Uuu7[Z]ҧ+,g[i2C2V7U,9R%6e I`("  x8v0**Q_WGin%  Q"^ w8$$ᔖʣڣ)bR.ZNV=شXY*;[1%[NT(C~J]s hRln,$@o{يHH$SuU%$2)  ex3y&fgu\#B(t(ѨBν*YHw{^-p"#0uqsj|>|n"t}}:kҾ3q헗V j˴Gn;Tm PDr[ tXGb? jnEѵbƭ?w$^m몲ً&îz?Qudc",1MJI1mqZG?> gfÞQM%9bQf՘pۓAm]- tքN'Ͼ]{+m}fq^T+H\_9Fcxemrrr8uWN"?! S拏Lť<'  w>HFB <) M5k36It'**M*7`Gc:-N d }zL  ֒S%0("M_.|;_ЇoIH;Q4_΅HH"0m F$@$@$@$@$@$@$@$ PD/ (" HHHHHHHH("wHHHHHHHH`PE$@$@$@$@$@$@$@$@;@$@$@$@$@$@$@$@$0(H"b        H        EA @ (,ɰq>T*s$@$0bD1B     'JQ~A!1s';ΓH` _ hjjľ=pD9F#s }{?~+4AGkIqF$@$@$@$A[X,F$@$("y>m@     Caz1C ov6߾= 05B$@K"{HHHH@l G"{؊HHHHH` mR<@|BZB$@C"pU     /&7RDDBCCx xY !"l؊1C ';ΜHHHHH`u`m Hn$@~I"_v.HHHHbсWO$@C#@ihؚHHHHH`hkkիHH`l0&p(RK징V47}񇰪'e,$@$@$@$@#֦biaZ" L"޾4Fݎ7xe'}c:,KSHSHHH`0 Ť4De." q4.@:z>Ynj'22ֺs$@$@$@$0UV6yP' Vu4M iBVXlajz-قHHHH`0'Îmڷw: z"9Hg./jpNlA$@$@$@k)! ԗ kHHHHHHHHz 7ffMw} 8Ws"BClNj$@$@$@$@$@$@$@$0RFJp I}W=AZ.cƬ9>sϜ@dTԀxHHHHHHHFJ1FJpν9[҃#pep1cKi>t+ = n3gυbAWWXln!!!^_t;xIƸ͹7݆nyyh\_6/]ܔsp׿g,!      6z" }{v!yR*S&!88XJνY}Vw1#"ىSΗǟn̜5AAAho7dr\-v؊ظ8 005sOC=}!?aazn#A"꼵U+=%_W_>JNs}(\t9-YҒx;8){wgU Lf&;@"aV+*uz-Tb[Ѷ z]j{Zm=$!6?&;3;yg9|ϐyڳ/ HzpNve?s)5C:H @0I_y`ӑ}?y{ŷ:8J~ro*Ϝbs˳0O֮M?%ܟeHKY13e}O;kNӳ||^{ #]}*kz-/ٙYz׽)\yGrk{8ٌK>(g%c4q37~|{|;Y ҟ_O}o_Kub ϋ>O~.?[;z'ox~sq/Zp.rl @@/xq:y/L|h;3FL.L[nM|czp|F '-N?zk}mlD%}D3xӳ@*f)}Ŷw۰lqX{v8Nj_[ysKbc3-<1?|9=X@<ʳsČÏ82=c^٬M;>a~ *fj-y|{r"s^i) 5sfm3g=}ynl>i!_}Y41G̴Zm[vIQ<'b\#XK^sv:BX_O9zolmga"uL%(C="~=ws @ ֮y*Ѿ>s~Z.g7a5-%`&R3.c6mKςkפ9s!DQ{NÏceeRl_nm݆ Y9<en>*͙o;o~jCyYk_O?3O,g}wF[bV6>.OO.w׿)?W/_>H7x]piϪ\,}C >_4?St)ϙ|+",Ш(R$Y(qJE_So'@ymP=֨-`&Rl(qi|ʔٺ></֩Y~zѺ,jdֳmӅ#X9.a[~鑇WPҰX3[)$:#SBmyM>hdoLfEUK) N|s/~+J̰Oޟ @ĢlnZ @@9B_gfɧ~c3Ey\2%feY䁼%ߗݩ˖)f |sױvH>іk䋉nj%8qR~[Y\ww?s8.jKgfE=eE`6= fg~ͽmPIo>W4Tݶ @(3v6~hg>a7KZ1(Y:]̊ngĥYb5ޱX>s&~,☇\e6R,6ek1#'TʦbX:'}c](w~@ 7Ug/O|&~yW<=k˖BR\Jeƌ6'|<[_OEA/н?Iw时};̏ɷ"HcH#t  N&Oλ3(G@SO~~֫,k5{ϝÞR՗y矦nK8q7weJ?I1inY9y8,?qy.i1;ꋗ|*nrG}$$ŚPqeKhm{/0g:ݜaS{gz_(oQjVTa?B7q^u"{k.OےbU4P[ /ָ*影f9?|%yZd @@S T @`,dw.פ[Eމ'K_w[П<{LgnzޘObWط_eQ}xDz~Y? 㥍 @qÓr'e(Gx-1Ah͚5{pjl}mLq `Ce,]r75Xܻm4{.pħ_bW<؊ǵ39W:zDzK k[o;'yԱ{#00j]uk5ӱ%PF-e70q @lݛ- @):/X-F̚5;ZrkU]tɧLs[KbRH[6on;ɓ^`ݺk# @@*-VyWWWsiÆ-;!@@\%D*j p o @ ;jD'v(A`8\V @ P5!RFL{  @ @%J@W% @ @jB @ @J"J @ @@HU1%@ @ D*] @ @ tUK @@s ,<˖% @V+ 0iڴ=F j' @5\y/"@:3k8. Pk"Ua @ PP= @ @ B*6 @ @J"<'@ @TA@TQF @ @@B@ @ @*0JH @(Y@T @ P!RFI  @ @% J @ @ D(i# @ @dW= @-&paGi\ggLw B'@ PwY{d߷lR:^@4j$@ -?#@@ X]G^  @ @" ˮ @ @v"7 @ @`Ba`ٕ @ ЮBvy&@ @ C@4 , @ @U@Ԯ# @ @aeW @ @@ tk @gIOzuACv0i'o @h/{+]&M>#=cګzK5 j# @x{W*4y^ @H&t @!ёn NH @zy7n\_k1iLUB @L-oB)#@@yBL @izm2ii @@s k< @@)k<6oޜfΜtQǖ @@ wl @ L0!1[E P j4S+  @hvK{9=[6]ueiӗ5N&@j TZ @ ?Ja'%@@HC= @ 0juK 0i  @h3M6=8qR\w  @V!RR#@ vw @H,=R9 @hI&}M"] NE @'rZ mݺ5vMU6 @Y @Y~6lXi|^ @@ ~4 @ƍKz ѻ @B:: @N[lCm. Dj'@@δiӦv@  @RBJ  @Q` c7"@ "5)  @?ƏN;t¢SRggXThS6n fn^TM`͚5MO>O:ҔSS,u֦i @@k Zk<@[ Z 'Oidi۶m֯_4hM @@JH֭ X"A@  @F GpC @H7s @زeK"5`h@@ @)y|1^ @@ XXFD{ @ !0~4}4eʔu e3I@T'H!@ 0V9~QdqۭcUz @Hm @Rܙ'H==%) @B @@]| @@-֮E> @ @6" @ @j"բd @ @@ @  @ @jQ @ @Hm @ @ZH(ه @ B6} @ @@-BZC @hs!R> @ @!R-J!@ @t @ P% @ @\@?O@y'NV;SOٗx3?ҫW @M DjAKt%_Mt {?o_tnͽ.\N`:=ÏLqԠTyxӹtaGԍήtNo}NƁZ̝׈S;' @"5ݐhe q-#Ԕɓ}l ƚJkZ>pi)uսIG!սv @" _y7rN=/'mٲe\VfS{WQ0aBtϸqcW'L]lV-횇TlpRƹkpll'@ 0Ia,A@K <أGIgQ֛o'???>?/3`a l&ߦ7\ٯ䳎bv̔)Sӯ<}L'wݑ>O͞OsޝszT~tUJֿۋo{ϙ>q҄SOOOz3Xڰ~}Nץ.B8&.N7o=cK_۽Vz(+/N>XxeO˗-IͯO{N^gv=vt7/yowM#_hԩ{k׬I^WGYO+"|QG[ڶm[ZЃ_4 @@)'RR.Y^QoO"lشicot]"__lݒ>9"\ǞiҤiޝ K({?,X 7,p|KR}KM6ܘ. ;Bw}w_0aԝn,ڐ|hz;~Ϭq]wޖ4+[u] CM"³c[/.ܼySE_[[~[n{+M¶<Ʋ([RHO/YoH-<1?'O|=>3{bh _|Nm7]/AR9gAŒK_cӞ9,T8]S={A6e Mc1_Lv WMkfN׭K_񏾛3g4xc泎b?꽴/_K7]{͕+.˟7ga4sjC{hg= fWсKK"tq; i@DsWIZUg$&f-qSe֭y nr8 %FXz_{eM/q?ߓ~hͿzh'ku֮]CbVQ.EArE/Em1k+/KwY,f$M).kҿ)vHEH  ` @#"Αtz91 lDsF6#&J;,Dh3? ql]s!RMo_<49kV޻g!Yg9ysxgHӼUo~̢u{ߋ֢D/{GzpŲtߔfOymY8:HOevUjkK TnBݲ>fa:#5v]^؜3$X諃]@?O.n]|+.E'O.Gږ>yH[r..](W?]wAEQ.oWO&Nozv'Aɗ>NۋP쒭K⢯ yg(_t;߈EZ_ߩ֭)fk?C>i[3bVZzւC:4/юy;.u ~TxFtPV\f3F @F SN#Zhb[n!_c]eEs?pG<7@E/\?o?WRc@ekmQ$gnl&U|ugur) 3"lKwG?e^zUQϘ0S(WExP^vೞy?f=~Um)iKbmo~(qBE`d6!@0QK@KJw|~Z4ڸqco/.Jg[:RG־wanq -ΏUD |l}laqZd3/;G/̞ݩ.ʤ6uZtҩ~oȽô뙵j+? Ǟ{ޙQ5!nf%ek`) ,PHES @ X_g'@3~윢K1k?i5x,x c{\13ȣؾ/EHԷĥi1L37b^9J_t-7ewmnsɧ?6_ⲽF勇y{=?;^DۢDֿۢ/EV6'IPCFwDڼes篥MY(xۭ7>ҁBsK+2㨦X\%D PgZ>?<#@6wT:TL`8\V\ @ @@B2I @b @ @2He @ P1!RLs  @ @ePW' @ @bB  @ @"N @ @@H0%@ @! D*C] @ @ *6`K @(C@T:  @ @"Ul4 @ P uu @ @*& D؀i. @ @ !R$@ @TL@T\ @ @@B2I @b @ @2He @ P1!RLs  @ @ey4w_  @ @@uƢ'8 tE3A @ 0BHhq޴˖o|k͜H=IE( @ @ 4,D*PQB*:>w} @ @$HE(_يYK{M @ P@CyZ-] )B'AR-Z!@ @@C]49}p/ @ @@~9[1 i8.8f-ȣMo)K @ @H] ;m= S @ @ Wcm̚5;Zr^'@`&O2dM֭r; @z ;u:hp>W}MFuy  @ @l4a„qz--_d @ @@]CvǸ|qkWa8R,k$ wmCǞjCi'Gxf @ @H7&ZD@E3/pA;"=~|Z}M;l @U"UyR_  @AH0H @i'@ @FY  @ @" @ @v"(# @ @`BQ: @ Bve}$@ @R@4J@ @ @A@ @ @Q F p @ @@;a @ 0J!(N @h!R;> @ @F) D%  @ @ DjQG @ @(Ht8 @ @H0H @i'@ @FY  @ @;>wtIS<._$vگ}Ҳۏ뻏 @ @ u&RAzyo" r¢Rbo8=&q. @ @9"w{-xHDXSq.pcvu. @ @4F!R"*f ͍0iYEmisz$@ @[H# b %Υ @ @4@]֎YGQqZCqۄ v9(I(wM @ @蘿`aO=^@hkVZ|̙ozUiƍaTg"͚5;Zr6y&O2d#֭[;>v @wTt> (|LP@}KI1;){zh @ @HwG  @ @! Djs @ @ZL@b; @ @F @ @"؀ @ @,nl'8c &{J+/9sM>*mܸ1~.o<7g͚VZ938F r܊munh/Z~G @@󹪫EeK7N}{_oxK-]b(qxQ @ @kb&QYD1hWPl+bb6SDϤ @ @H%l}IE}y&  @ @c#P)ŸB/_  @ @+9mƜ UOPB)~Goi1k)c})۔)Sڵku!@<͛ @wG%0U ;[A۱>P5 Z)飻TqF; .cj @@;;ZC`8"UPTv/î]/P @1\U5g%@ @([@T @ P!RI  @ @e  @ @* D i" @ @l!R#~ @ @@H$M$@ @- D*{O @ @ @He  @ @"U`4 @ PP? @ @B & @ @"='@ @T@@TAD @ @@BG@ @ @ *0HH @([@T @ P!RI  @ @e  @ @* D i" @ @l!R#~ @ @@H$M$@ @- D*{O @ @ @He  @ @"U`4 @ PP? @ @B & @ @"='@ @T@@TAD @ @@BG@ @ @ *0HH @([@T @ P!RI  @ @e  @ @* D i" @ @l!R#~ @ @@H$M$@ @- D*{O @@COZ< `< @ @t5NXLtV7ضޝwm~B b@ < v.hm50bRh 4=2AsWz"I Q4ӷў \|,!E ~縜YFC; @|[O]C'BqT[?кWZO;2C P@-!R,ݿ @vz?-dpwwفF ~ט4A @@ y"NkCB߿M)6q; ZN mq!Jpggn69Dm6 @@ m-J@eE <:@ux\l Icg(M  @VsV>j ~*V\V+t_guܹo]z {œYfUVEU @]]i݉w۶k+4쏻:) @,<073*<*k$KشiSɂE68H:c@A1#ugaR%>l P1~ @@4):!REE`ҶUi؜]]cGuPM @hl <5Ob}ێźIE "څGشicΌi; @ZU f #@ HEh4BكmkՁ/#lIY @ P ѮԿ ]Xe4B`du"@ @@ ĝ.lqzDgCč7lt yM @@; g|mkR#I ٙ-ݝ?iTF @bQy-YHunT ,Ќ@i\|uKeoq_7 @ɚӓj(q'h,z%D eu 0}1\ @hAq-']"@ @ΠNG @hE!R+> @ @, D3 @ @ZQ@ԊO @ @:  t @ @V" @ @B:: @ @H8D @LIENDB`strawberry-graphql-django-0.62.0/docs/images/logo.png000066400000000000000000000161671502405145400225550ustar00rootroot00000000000000PNG  IHDR||Ծ pHYsy(qtEXtSoftwarewww.inkscape.org<IDATxyx]U?-mIR^T(" "^DN 8\@u\QE&Cڦm:7鐜y8iڔ4 9ϓ>߳w9qsg8"m1Tw 6,s֍ QXȦŊDX"x6%x-!K#n$&TCzI:ly8Pޯ@X &x T2 jJ`SJ.o%Iaؑ&LtZ֏RdxhށۣۚW-uuH@՜'@\uWA -GF%r L&<~T5w鍡J7ٓ1- 0,+n[D,hځnkEJCXSjH8Lli|`;Sދ 3m<n6F22E`ڶ~0?A.q58'd*d3,em%T{aTMr7Okd{ki#=ܐI^ ^I=蟀sO'_۽{w@}Gz j ]hBlK~*U(=Ling#CG[HwdЫ&f<{Fn2%ęZ¼nO*^t7vLИ1[[CsNJA ƃxl2X |tޕ#(P畲4ӘQ*]bto%XgE,P},sDaJ-eȻ}<ذР|a39u bg%AQް۸,,[:Y^[,NRV&9$<6ŗ~X2nH[xV*@)QަUYAD&q>| N ݊NWѼ$(Qa/Ig+̐(z@gËL C\Gf]X_[̮_,xeEP9T|7A>S1&F*E&l:JWXH.25leً#n<[8Gpv6YcEy2<#!Gd) 3фaY݊V).ڭhX!ee=Z+ơäy1l(P.*D4{p eWe\7;=6kSn~]]]B߷ #OP p6<#k2T`jlpeg̲E~Tg0Ǹ27*to2ji 5Q:oB޷M r+QD;$3D7њ]ݞ5cm*84M9,WSݜX+Eb<z) -xx֤s;Ə*O'c|"Đse{Qꀲ:UUӜ09OZ#vsX-,q~tKJf7Z[F =~1nXN; AAQFhVueA)TV4vऻXw.c}ai2)gȎnkŐg/g.K?xۙM=NM%h{n٥v8kaIs,?XdvGU-oEr .dz`݄UKKӦ>YsғW~2:[o#;f$zt/C q \s (ˀ3ܐ=xҹz&#Q+I'dO-:0f=dPqWyM3Bپkg^GUP?>qhDr .HQm>ƭ9r`ɱnN/2?LY ˅I*Z@ۙ5wH8T ]{zsm[\W=Ϧ慂!pͼ_ES7~"%(@ƏI30i܄[984Q}U'pń_)z9J럹FƫƫSwв蠈Y^AL;Cz~Yt,¶,,*ќaC|ozvn?ME5xn |OgYCu^O(=;\FѢZu+־Mw~c3$ȷ8X9"KeS}u|$SIoFV&#͝7TTwTӚ_|s9QU\~6oe4ߌWAWte|9zWbU/9'a`Qgo߁>Y3/ԤuT7]p>řW{~;2Ǝ |>}} ا5veWqWn+߻N=>Eo;Yߘq2lز5Xմ׮o3D핹-)ñכоz76j}O<}OR?q'ϙW}\G#y |)?/W ¡V.q)} WW/77ߨ7#B"ŊlޡȧCc 7}#~'*?縞+6-$z}d_!B5߾n)hC7֯eG"'VlӨZ. DY `_i D/~ ]o}tﶬءc_f7.aOk?u|σ:aF{ -e9I*5/omO"E>:Nd7,|_w-&Ŋ[t}Q\֜w7H❗@,b7i+G0eR=+nز7-a^;,c/FRqlƳU>#;EIgmȃxazÔFՖ?q[wibr"zE.y& <߾CU,Y|_!FW1:|?|;y#$;;-EV؄g՝-=J>z\2+ϼ/GeA-/'̩PчTk(W]nn {dxuc&bW 6N:Ѳd5Q^}w1h?-r* 1<Q_'ҡ? ;2 iS#y C w%7X1犚z%Ž&|<(FLga`<P\PN1b7_bT?9M.ON"Lޏ 21QJt;̆*7b^yʈɎe}-_.QA3\[wGrBCME7\ mc&Wl|!$A:|X@CfXJw=nl;j_^Df՞1Q;}(dTY'O~}a%xR}9C\SI>>QfD_5KuMG@g(2<?Eަ'yzSv+{ Z@7(wrQy-6yOo; {FJF;ضULTCĘ qqu;:Ouqs89q+% `IENDB`strawberry-graphql-django-0.62.0/docs/index.md000066400000000000000000000156701502405145400212710ustar00rootroot00000000000000--- title: Quick Start --- # Quick Start In this Quick-Start, we will: - Set up a basic pair of models with a relation between them. - Add them to a graphql schema and serve the graph API. - Query the graph API for model contents. For a more advanced example of a similar setup including a set of mutations and more queries, please check the [example app](https://github.com/strawberry-graphql/strawberry-django/tree/main/examples/django). ## Installation ```sh poetry add strawberry-graphql-django poetry add django-choices-field # Not required but recommended ``` (Not using poetry yet? `pip install strawberry-graphql-django` works fine too.) ## Define your application models We'll build an example database of fruit and their colours. > [!TIP] > You'll notice that for `Fruit.category`, we use `TextChoicesField` instead of `TextField(choices=...)`. > This allows strawberry-django to automatically use an enum in the graphQL schema, instead of > a string which would be the default behaviour for TextField. > > See the [choices-field integration](./integrations/choices-field.md) for more information. ```python title="models.py" from django.db import models from django_choices_field import TextChoicesField class FruitCategory(models.TextChoices): CITRUS = "citrus", "Citrus" BERRY = "berry", "Berry" class Fruit(models.Model): """A tasty treat""" name = models.CharField(max_length=20, help_text="The name of the fruit variety") category = TextChoicesField(choices_enum=FruitCategory, help_text="The category of the fruit") color = models.ForeignKey( "Color", on_delete=models.CASCADE, related_name="fruits", blank=True, null=True, help_text="The color of this kind of fruit", ) class Color(models.Model): """The hue of your tasty treat""" name = models.CharField( max_length=20, help_text="The color name", ) ``` You'll need to make migrations then migrate: ```sh python manage.py makemigrations python manage.py migrate ``` Now use the django shell, the admin, the loaddata command or whatever tool you like to load some fruits and colors. I've loaded a red strawberry (predictable, right?!) ready for later. ## Define types Before creating queries, you have to define a `type` for each model. A `type` is a fundamental unit of the [schema](https://strawberry.rocks/docs/types/schema) which describes the shape of the data that can be queried from the GraphQL server. Types can represent scalar values (like String, Int, Boolean, Float, and ID), enums, or complex objects that consist of many fields. > [!TIP] > A key feature of `strawberry-graphql-django` is that it provides helpers to create types from django models, > by automatically inferring types (and even documentation!!) from the model fields. > > See the [fields guide](./guide/fields.md) for more information. ```python title="types.py" import strawberry_django from strawberry import auto from . import models @strawberry_django.type(models.Fruit) class Fruit: id: auto name: auto category: auto color: "Color" # Strawberry will understand that this refers to the "Color" type that's defined below @strawberry_django.type(models.Color) class Color: id: auto name: auto fruits: list[Fruit] # This tells strawberry about the ForeignKey to the Fruit model and how to represent the Fruit instances on that relation ``` ## Build the queries and schema Next we want to assemble the [schema](https://strawberry.rocks/docs/types/schema) from its building block types. > [!WARNING] > You'll notice a familiar statement, `fruits: list[Fruit]`. We already used this statement in the previous step in `types.py`. > Seeing it twice can be a point of confusion when you're first getting to grips with graph and strawberry. > > The purpose here is similar but subtly different. Previously, the syntax defined that it was possible to make a query that **traverses** within the graph, from a Color to a list of Fruits. > Here, the usage defines a [**root** query](https://strawberry.rocks/docs/general/queries) (a bit like an entrypoint into the graph). > [!TIP] > We add the `DjangoOptimizerExtension` here. Don't worry about why for now, but you're almost certain to want it. > > See the [optimizer guide](./guide/optimizer.md) for more information. ```python title="schema.py" import strawberry from strawberry_django.optimizer import DjangoOptimizerExtension from .types import Fruit @strawberry.type class Query: fruits: list[Fruit] = strawberry_django.field() schema = strawberry.Schema( query=Query, extensions=[ DjangoOptimizerExtension, ], ) ``` ## Serving the API Now we're showing off. This isn't enabled by default, since existing django applications will likely have model docstrings and help text that aren't user-oriented. But if you're starting clean (or overhauling existing dosctrings and helptext), setting up the following is super useful for your API users. If you don't set these true, you can always provide user-oriented descriptions. See the ```python title="settings.py" STRAWBERRY_DJANGO = { "FIELD_DESCRIPTION_FROM_HELP_TEXT": True, "TYPE_DESCRIPTION_FROM_MODEL_DOCSTRING": True, } ``` ```python title="urls.py" from django.urls import include, path from strawberry.django.views import AsyncGraphQLView from .schema import schema urlpatterns = [ path('graphql', AsyncGraphQLView.as_view(schema=schema)), ] ``` This generates following schema: ```graphql title="schema.graphql" enum FruitCategory { CITRUS BERRY } """ A tasty treat """ type Fruit { id: ID! name: String! category: FruitCategory! color: Color } type Color { id: ID! """ field description """ name: String! fruits: [Fruit!] } type Query { fruits: [Fruit!]! } ``` ## Using the API Start your server with: ```sh python manage.py runserver ``` Then visit [localhost:8000/graphql](http://localhost:8000/graphql) in your browser. You should see the graphql explorer being served by django. Using the interactive query tool, you can query for the fruits you added earlier: ![GraphiQL with fruit](./images/graphiql-with-fruit.png) ## Next steps 1. [Defining more Django Types](./guide/types.md) 2. [Define Fields inside those Types](./guide/fields.md) 3. [Serve your API using ASGI or WSGI](./guide/views.md) 4. [Define filters for your fields](./guide/filters.md) 5. [Define orderings for your fields](./guide/ordering.md) 6. [Define pagination for your fields](./guide/pagination.md) 7. [Define queries for your schema](./guide/queries.md) 8. [Define mutations for your schema](./guide/mutations.md) 9. [Define subscriptions for your schema](./guide/subscriptions.md) 10. [Enable the Query Optimizer extension for performance improvement](./guide/optimizer.md) 11. [Use the relay integration for advanced pagination and model refetching](./guide/relay.md) 12. [Protect your fields using the Permission Extension](./guide/permissions.md) 13. [Write unit tests for your schema](./guide/unit-testing.md) strawberry-graphql-django-0.62.0/docs/integrations/000077500000000000000000000000001502405145400223355ustar00rootroot00000000000000strawberry-graphql-django-0.62.0/docs/integrations/channels.md000066400000000000000000000005551502405145400244570ustar00rootroot00000000000000--- title: Django Channels --- # django-channels Strawberry provides an integration for [django-channels](https://channels.readthedocs.io/en/stable/) to allow [subscriptions](../guide/subscriptions.md) to be used with django. Check the [official strawberry django docs](https://strawberry.rocks/docs/integrations/channels) for more information on how to use it. strawberry-graphql-django-0.62.0/docs/integrations/choices-field.md000066400000000000000000000020051502405145400253520ustar00rootroot00000000000000--- title: Django Choices Field --- # django-choices-field This lib provides integration for enum resolution for [Django's TextChoices/IntegerChoices](https://docs.djangoproject.com/en/4.2/ref/models/fields/#enumeration-types) when defining the fields using the [django-choices-field](https://github.com/bellini666/django-choices-field) lib: ```python title="models.py" from django.db import models from django_choices_field import TextChoicesField class Status(models.TextChoices): ACTIVE = "active", "Is Active" INACTIVE = "inactive", "Inactive" class Company(models.Model): status = TextChoicesField( choices_enum=Status, default=Status.ACTIVE, ) ``` ```python title="types.py" import strawberry import strawberry_django import .models @strawberry_django.type(models.Company) class Company: status: strawberry.auto ``` The code above would generate the following schema: ```graphql title="schema.graphql" enum Status { ACTIVE INACTIVE } type Company { status: Status } ``` strawberry-graphql-django-0.62.0/docs/integrations/debug-toolbar.md000066400000000000000000000016301502405145400254050ustar00rootroot00000000000000--- title: Django Debug Toolbar --- # django-debug-toolbar This integration provides integration between the [Django Debug Toolbar](https://github.com/jazzband/django-debug-toolbar) and `strawberry`, allowing it to display stats like `SQL Queries`, `CPU Time`, `Cache Hits`, etc for queries and mutations done inside the [graphiql page](https://github.com/graphql/graphiql). To use it, make sure you have the [Django Debug Toolbar](https://github.com/jazzband/django-debug-toolbar) installed and configured, then change its middleware settings from: ```python title="settings.py" MIDDLEWARE = [ ... "debug_toolbar.middleware.DebugToolbarMiddleware", ... ] ``` To: ```python title="settings.py" MIDDLEWARE = [ ... "strawberry_django.middlewares.debug_toolbar.DebugToolbarMiddleware", ... ] ``` Finally, ensure app `"strawberry_django"` is added to your `INSTALLED_APPS` in Django settings. strawberry-graphql-django-0.62.0/docs/integrations/guardian.md000066400000000000000000000004401502405145400244470ustar00rootroot00000000000000--- title: django-guardian --- # django-guardian This lib provides integration for per-object-permissions using [django-guardian](https://django-guardian.readthedocs.io/en/stable/). Check the [Permission Extension Guide](../guide/permissions.md) for more information on how to use it. strawberry-graphql-django-0.62.0/examples/000077500000000000000000000000001502405145400205155ustar00rootroot00000000000000strawberry-graphql-django-0.62.0/examples/django/000077500000000000000000000000001502405145400217575ustar00rootroot00000000000000strawberry-graphql-django-0.62.0/examples/django/.gitignore000066400000000000000000000000141502405145400237420ustar00rootroot00000000000000/db.sqlite3 strawberry-graphql-django-0.62.0/examples/django/README.md000066400000000000000000000006221502405145400232360ustar00rootroot00000000000000# Quick start Install poetry ```shell pip install poetry ``` Install project dependencies, run migrations, load test data and start development server. ```shell cd examples/django poetry install poetry run ./manage.py migrate poetry run ./manage.py loaddata berries poetry run ./manage.py runserver ``` After that you have web server and graphql endpoint running at http://127.0.0.1:8000/graphql. strawberry-graphql-django-0.62.0/examples/django/__init__.py000066400000000000000000000000001502405145400240560ustar00rootroot00000000000000strawberry-graphql-django-0.62.0/examples/django/app/000077500000000000000000000000001502405145400225375ustar00rootroot00000000000000strawberry-graphql-django-0.62.0/examples/django/app/__init__.py000066400000000000000000000000001502405145400246360ustar00rootroot00000000000000strawberry-graphql-django-0.62.0/examples/django/app/apps.py000066400000000000000000000001711502405145400240530ustar00rootroot00000000000000from django.apps import AppConfig class ExampleAppConfig(AppConfig): name = "app" verbose_name = "Example App" strawberry-graphql-django-0.62.0/examples/django/app/fixtures/000077500000000000000000000000001502405145400244105ustar00rootroot00000000000000strawberry-graphql-django-0.62.0/examples/django/app/fixtures/berries.json000066400000000000000000000007031502405145400267360ustar00rootroot00000000000000[ { "model": "app.fruit", "pk": 1, "fields": { "name": "strawberry", "color": 1 } }, { "model": "app.fruit", "pk": 2, "fields": { "name": "raspberry", "color": 1 } }, { "model": "app.fruit", "pk": 3, "fields": { "name": "blueberry", "color": 2 } }, { "model": "app.color", "pk": 1, "fields": { "name": "red" } }, { "model": "app.color", "pk": 2, "fields": { "name": "blue" } } ] strawberry-graphql-django-0.62.0/examples/django/app/migrations/000077500000000000000000000000001502405145400247135ustar00rootroot00000000000000strawberry-graphql-django-0.62.0/examples/django/app/migrations/0001_initial.py000066400000000000000000000026241502405145400273620ustar00rootroot00000000000000# Generated by Django 3.2 on 2021-04-13 12:06 import django.db.models.deletion from django.db import migrations, models class Migration(migrations.Migration): initial = True dependencies = [] operations = [ migrations.CreateModel( name="Color", fields=[ ( "id", models.AutoField( auto_created=True, primary_key=True, serialize=False, verbose_name="ID", ), ), ("name", models.CharField(max_length=20)), ], ), migrations.CreateModel( name="Fruit", fields=[ ( "id", models.AutoField( auto_created=True, primary_key=True, serialize=False, verbose_name="ID", ), ), ("name", models.CharField(max_length=20)), ( "color", models.ForeignKey( on_delete=django.db.models.deletion.CASCADE, related_name="fruits", to="app.color", ), ), ], ), ] strawberry-graphql-django-0.62.0/examples/django/app/migrations/0002_alter_fruit_color.py000066400000000000000000000011131502405145400314400ustar00rootroot00000000000000# Generated by Django 3.2 on 2021-04-21 14:52 import django.db.models.deletion from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ("app", "0001_initial"), ] operations = [ migrations.AlterField( model_name="fruit", name="color", field=models.ForeignKey( blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name="fruits", to="app.color", ), ), ] strawberry-graphql-django-0.62.0/examples/django/app/migrations/__init__.py000066400000000000000000000000001502405145400270120ustar00rootroot00000000000000strawberry-graphql-django-0.62.0/examples/django/app/models.py000066400000000000000000000005131502405145400243730ustar00rootroot00000000000000from django.db import models class Fruit(models.Model): name = models.CharField(max_length=20) color = models.ForeignKey( "Color", blank=True, null=True, related_name="fruits", on_delete=models.CASCADE, ) class Color(models.Model): name = models.CharField(max_length=20) strawberry-graphql-django-0.62.0/examples/django/app/schema.py000066400000000000000000000020761502405145400243560ustar00rootroot00000000000000import strawberry import strawberry_django from strawberry_django import auth, mutations from .types import ( Color, ColorInput, ColorPartialInput, Fruit, FruitInput, FruitPartialInput, User, UserInput, ) @strawberry.type class Query: fruit: Fruit = strawberry_django.field() fruits: list[Fruit] = strawberry_django.field() color: Color = strawberry_django.field() colors: list[Color] = strawberry_django.field() @strawberry.type class Mutation: create_fruit: Fruit = mutations.create(FruitInput) create_fruits: list[Fruit] = mutations.create(FruitInput) update_fruits: list[Fruit] = mutations.update(FruitPartialInput) delete_fruits: list[Fruit] = mutations.delete() create_color: Color = mutations.create(ColorInput) create_colors: list[Color] = mutations.create(ColorInput) update_colors: list[Color] = mutations.update(ColorPartialInput) delete_colors: list[Color] = mutations.delete() register: User = auth.register(UserInput) schema = strawberry.Schema(query=Query, mutation=Mutation) strawberry-graphql-django-0.62.0/examples/django/app/settings.py000066400000000000000000000037271502405145400247620ustar00rootroot00000000000000from pathlib import Path BASE_DIR = Path(__file__).resolve().parent.parent SECRET_KEY = "very-secret-key" DEBUG = True INTERNAL_IPS = [ "127.0.0.1", ] ALLOWED_HOSTS = [] INSTALLED_APPS = [ "django.contrib.auth", "django.contrib.contenttypes", "django.contrib.sessions", "django.contrib.staticfiles", "debug_toolbar", "strawberry_django", "app", ] MIDDLEWARE = [ "django.middleware.security.SecurityMiddleware", "django.contrib.sessions.middleware.SessionMiddleware", "django.middleware.common.CommonMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware", "strawberry_django.middlewares.debug_toolbar.DebugToolbarMiddleware", ] AUTH_PASSWORD_VALIDATORS = [ { "NAME": ( "django.contrib.auth.password_validation.UserAttributeSimilarityValidator" ), }, { "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", "OPTIONS": { "min_length": 9, }, }, { "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", }, { "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", }, ] ROOT_URLCONF = "app.urls" TEMPLATES = [ { "BACKEND": "django.template.backends.django.DjangoTemplates", "DIRS": [], "APP_DIRS": True, "OPTIONS": { "context_processors": [ "django.template.context_processors.debug", "django.template.context_processors.request", "django.contrib.auth.context_processors.auth", ], }, }, ] DATABASES = { "default": { "ENGINE": "django.db.backends.sqlite3", "NAME": BASE_DIR / "db.sqlite3", }, } STATIC_URL = "/static/" STATICFILES_FINDERS = [ "django.contrib.staticfiles.finders.FileSystemFinder", "django.contrib.staticfiles.finders.AppDirectoriesFinder", ] DEFAULT_AUTO_FIELD = "django.db.models.AutoField" strawberry-graphql-django-0.62.0/examples/django/app/types.py000066400000000000000000000042051502405145400242560ustar00rootroot00000000000000from typing import Optional from strawberry import auto import strawberry_django from django.contrib.auth import get_user_model from django.db.models import Q from . import models # filters @strawberry_django.filters.filter_type(models.Fruit, lookups=True) class FruitFilter: id: auto name: auto color: Optional["ColorFilter"] @strawberry_django.filter_field def special_filter(self, prefix: str, value: str): return Q(**{f"{prefix}name": value}) @strawberry_django.filters.filter_type(models.Color, lookups=True) class ColorFilter: id: auto name: auto fruits: Optional[FruitFilter] @strawberry_django.filter_field def filter(self, prefix, queryset): return queryset, Q() # order @strawberry_django.ordering.order(models.Fruit) class FruitOrder: name: auto color: Optional["ColorOrder"] @strawberry_django.ordering.order(models.Color) class ColorOrder: name: auto fruits: FruitOrder @strawberry_django.order_field def special_order(self, prefix: str, value: auto): return [value.resolve(f"{prefix}fruits__name")] # types @strawberry_django.type( models.Fruit, filters=FruitFilter, order=FruitOrder, pagination=True, ) class Fruit: id: auto name: auto color: Optional["Color"] @strawberry_django.type( models.Color, filters=ColorFilter, order=ColorOrder, pagination=True, ) class Color: id: auto name: auto fruits: list[Fruit] @strawberry_django.type(get_user_model()) class User: id: auto username: auto password: auto email: auto # input types @strawberry_django.input(models.Fruit) class FruitInput: id: auto name: auto color: auto @strawberry_django.input(models.Color) class ColorInput: id: auto name: auto fruits: auto @strawberry_django.input(get_user_model()) class UserInput: username: auto password: auto email: auto # partial input types @strawberry_django.input(models.Fruit, partial=True) class FruitPartialInput(FruitInput): pass @strawberry_django.input(models.Color, partial=True) class ColorPartialInput(ColorInput): pass strawberry-graphql-django-0.62.0/examples/django/app/urls.py000066400000000000000000000010171502405145400240750ustar00rootroot00000000000000from strawberry.django.views import AsyncGraphQLView, GraphQLView from django.conf.urls.static import static from django.urls import path from django.urls.conf import include from django.views.generic.base import RedirectView from .schema import schema urlpatterns = [ path("", RedirectView.as_view(url="graphql")), path("graphql/sync", GraphQLView.as_view(schema=schema)), path("graphql", AsyncGraphQLView.as_view(schema=schema)), path("__debug__/", include("debug_toolbar.urls")), *static("/media"), ] strawberry-graphql-django-0.62.0/examples/django/manage.py000077500000000000000000000012251502405145400235640ustar00rootroot00000000000000#!/usr/bin/env python """Django's command-line utility for administrative tasks.""" import os import sys def main(): """Run administrative tasks.""" os.environ.setdefault("DJANGO_SETTINGS_MODULE", "app.settings") try: from django.core.management import execute_from_command_line except ImportError as exc: raise ImportError( "Couldn't import Django. Are you sure it's installed and " "available on your PYTHONPATH environment variable? Did you " "forget to activate a virtual environment?", ) from exc execute_from_command_line(sys.argv) if __name__ == "__main__": main() strawberry-graphql-django-0.62.0/examples/django/poetry.lock000066400000000000000000000220151502405145400241530ustar00rootroot00000000000000# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. [[package]] name = "asgiref" version = "3.7.2" description = "ASGI specs, helper code, and adapters" optional = false python-versions = ">=3.7" files = [ {file = "asgiref-3.7.2-py3-none-any.whl", hash = "sha256:89b2ef2247e3b562a16eef663bc0e2e703ec6468e2fa8a5cd61cd449786d4f6e"}, {file = "asgiref-3.7.2.tar.gz", hash = "sha256:9e0ce3aa93a819ba5b45120216b23878cf6e8525eb3848653452b4192b92afed"}, ] [package.dependencies] typing-extensions = {version = ">=4", markers = "python_version < \"3.11\""} [package.extras] tests = ["mypy (>=0.800)", "pytest", "pytest-asyncio"] [[package]] name = "backports-zoneinfo" version = "0.2.1" description = "Backport of the standard library zoneinfo module" optional = false python-versions = ">=3.6" files = [ {file = "backports.zoneinfo-0.2.1-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:da6013fd84a690242c310d77ddb8441a559e9cb3d3d59ebac9aca1a57b2e18bc"}, {file = "backports.zoneinfo-0.2.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:89a48c0d158a3cc3f654da4c2de1ceba85263fafb861b98b59040a5086259722"}, {file = "backports.zoneinfo-0.2.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:1c5742112073a563c81f786e77514969acb58649bcdf6cdf0b4ed31a348d4546"}, {file = "backports.zoneinfo-0.2.1-cp36-cp36m-win32.whl", hash = "sha256:e8236383a20872c0cdf5a62b554b27538db7fa1bbec52429d8d106effbaeca08"}, {file = "backports.zoneinfo-0.2.1-cp36-cp36m-win_amd64.whl", hash = "sha256:8439c030a11780786a2002261569bdf362264f605dfa4d65090b64b05c9f79a7"}, {file = "backports.zoneinfo-0.2.1-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:f04e857b59d9d1ccc39ce2da1021d196e47234873820cbeaad210724b1ee28ac"}, {file = "backports.zoneinfo-0.2.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:17746bd546106fa389c51dbea67c8b7c8f0d14b5526a579ca6ccf5ed72c526cf"}, {file = "backports.zoneinfo-0.2.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:5c144945a7752ca544b4b78c8c41544cdfaf9786f25fe5ffb10e838e19a27570"}, {file = "backports.zoneinfo-0.2.1-cp37-cp37m-win32.whl", hash = "sha256:e55b384612d93be96506932a786bbcde5a2db7a9e6a4bb4bffe8b733f5b9036b"}, {file = "backports.zoneinfo-0.2.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a76b38c52400b762e48131494ba26be363491ac4f9a04c1b7e92483d169f6582"}, {file = "backports.zoneinfo-0.2.1-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:8961c0f32cd0336fb8e8ead11a1f8cd99ec07145ec2931122faaac1c8f7fd987"}, {file = "backports.zoneinfo-0.2.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e81b76cace8eda1fca50e345242ba977f9be6ae3945af8d46326d776b4cf78d1"}, {file = "backports.zoneinfo-0.2.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7b0a64cda4145548fed9efc10322770f929b944ce5cee6c0dfe0c87bf4c0c8c9"}, {file = "backports.zoneinfo-0.2.1-cp38-cp38-win32.whl", hash = "sha256:1b13e654a55cd45672cb54ed12148cd33628f672548f373963b0bff67b217328"}, {file = "backports.zoneinfo-0.2.1-cp38-cp38-win_amd64.whl", hash = "sha256:4a0f800587060bf8880f954dbef70de6c11bbe59c673c3d818921f042f9954a6"}, {file = "backports.zoneinfo-0.2.1.tar.gz", hash = "sha256:fadbfe37f74051d024037f223b8e001611eac868b5c5b06144ef4d8b799862f2"}, ] [package.extras] tzdata = ["tzdata"] [[package]] name = "django" version = "4.2.10" description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design." optional = false python-versions = ">=3.8" files = [ {file = "Django-4.2.10-py3-none-any.whl", hash = "sha256:a2d4c4d4ea0b6f0895acde632071aff6400bfc331228fc978b05452a0ff3e9f1"}, {file = "Django-4.2.10.tar.gz", hash = "sha256:b1260ed381b10a11753c73444408e19869f3241fc45c985cd55a30177c789d13"}, ] [package.dependencies] asgiref = ">=3.6.0,<4" "backports.zoneinfo" = {version = "*", markers = "python_version < \"3.9\""} sqlparse = ">=0.3.1" tzdata = {version = "*", markers = "sys_platform == \"win32\""} [package.extras] argon2 = ["argon2-cffi (>=19.1.0)"] bcrypt = ["bcrypt"] [[package]] name = "django-debug-toolbar" version = "3.8.1" description = "A configurable set of panels that display various debug information about the current request/response." optional = false python-versions = ">=3.7" files = [ {file = "django_debug_toolbar-3.8.1-py3-none-any.whl", hash = "sha256:879f8a4672d41621c06a4d322dcffa630fc4df056cada6e417ed01db0e5e0478"}, {file = "django_debug_toolbar-3.8.1.tar.gz", hash = "sha256:24ef1a7d44d25e60d7951e378454c6509bf536dce7e7d9d36e7c387db499bc27"}, ] [package.dependencies] django = ">=3.2.4" sqlparse = ">=0.2" [[package]] name = "graphql-core" version = "3.2.3" description = "GraphQL implementation for Python, a port of GraphQL.js, the JavaScript reference implementation for GraphQL." optional = false python-versions = ">=3.6,<4" files = [ {file = "graphql-core-3.2.3.tar.gz", hash = "sha256:06d2aad0ac723e35b1cb47885d3e5c45e956a53bc1b209a9fc5369007fe46676"}, {file = "graphql_core-3.2.3-py3-none-any.whl", hash = "sha256:5766780452bd5ec8ba133f8bf287dc92713e3868ddd83aee4faab9fc3e303dc3"}, ] [[package]] name = "python-dateutil" version = "2.8.2" description = "Extensions to the standard Python datetime module" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" files = [ {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, ] [package.dependencies] six = ">=1.5" [[package]] name = "six" version = "1.16.0" description = "Python 2 and 3 compatibility utilities" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" files = [ {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, ] [[package]] name = "sqlparse" version = "0.4.3" description = "A non-validating SQL parser." optional = false python-versions = ">=3.5" files = [ {file = "sqlparse-0.4.3-py3-none-any.whl", hash = "sha256:0323c0ec29cd52bceabc1b4d9d579e311f3e4961b98d174201d5622a23b85e34"}, {file = "sqlparse-0.4.3.tar.gz", hash = "sha256:69ca804846bb114d2ec380e4360a8a340db83f0ccf3afceeb1404df028f57268"}, ] [[package]] name = "strawberry-graphql" version = "0.159.0" description = "A library for creating GraphQL APIs" optional = false python-versions = ">=3.7,<4.0" files = [ {file = "strawberry_graphql-0.159.0-py3-none-any.whl", hash = "sha256:459890ef2f4afb89fc6b13fc24e2453db1904ac37bc85a08990814c19ef06ee2"}, {file = "strawberry_graphql-0.159.0.tar.gz", hash = "sha256:0650b2a398457981f4a3ae139336f26c1c1bdda3a18118bc84ecb68edc45b663"}, ] [package.dependencies] graphql-core = ">=3.2.0,<3.3.0" python-dateutil = ">=2.7.0,<3.0.0" typing_extensions = ">=3.7.4,<5.0.0" [package.extras] aiohttp = ["aiohttp (>=3.7.4.post0,<4.0.0)"] asgi = ["python-multipart (>=0.0.5,<0.0.6)", "starlette (>=0.13.6)"] chalice = ["chalice (>=1.22,<2.0)"] channels = ["asgiref (>=3.2,<4.0)", "channels (>=3.0.5)"] cli = ["click (>=7.0,<9.0)", "libcst (>=0.4.7)", "pygments (>=2.3,<3.0)", "rich (>=12.0.0)"] debug = ["libcst (>=0.4.7)", "rich (>=12.0.0)"] debug-server = ["click (>=7.0,<9.0)", "libcst (>=0.4.7)", "pygments (>=2.3,<3.0)", "python-multipart (>=0.0.5,<0.0.6)", "rich (>=12.0.0)", "starlette (>=0.13.6)", "uvicorn (>=0.11.6,<0.21.0)"] django = ["Django (>=3.2)", "asgiref (>=3.2,<4.0)"] fastapi = ["fastapi (>=0.65.2)", "python-multipart (>=0.0.5,<0.0.6)"] flask = ["flask (>=1.1)"] opentelemetry = ["opentelemetry-api (<2)", "opentelemetry-sdk (<2)"] pydantic = ["pydantic (<2)"] sanic = ["sanic (>=20.12.2)"] [[package]] name = "strawberry-graphql-django" version = "0.9.2" description = "Strawberry GraphQL Django extension" optional = false python-versions = ">=3.7,<4.0" files = [] develop = false [package.dependencies] Django = ">=3.2" strawberry-graphql = ">=0.154.0" [package.extras] debug-toolbar = ["django-debug-toolbar (>=3.4)"] [package.source] type = "directory" url = "../.." [[package]] name = "typing-extensions" version = "4.4.0" description = "Backported and Experimental Type Hints for Python 3.7+" optional = false python-versions = ">=3.7" files = [ {file = "typing_extensions-4.4.0-py3-none-any.whl", hash = "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e"}, {file = "typing_extensions-4.4.0.tar.gz", hash = "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa"}, ] [[package]] name = "tzdata" version = "2023.4" description = "Provider of IANA time zone data" optional = false python-versions = ">=2" files = [ {file = "tzdata-2023.4-py2.py3-none-any.whl", hash = "sha256:aa3ace4329eeacda5b7beb7ea08ece826c28d761cda36e747cfbf97996d39bf3"}, {file = "tzdata-2023.4.tar.gz", hash = "sha256:dd54c94f294765522c77399649b4fefd95522479a664a0cec87f41bebc6148c9"}, ] [metadata] lock-version = "2.0" python-versions = "^3.8" content-hash = "a6f08fff8b54f70dd16666b7f6d37308716493945af4b89c59c03e2fff49dc47" strawberry-graphql-django-0.62.0/examples/django/pyproject.toml000066400000000000000000000005621502405145400246760ustar00rootroot00000000000000[tool.poetry] name = "testapp" version = "0.1.0" description = "" authors = ["Lauri Hintsala "] [tool.poetry.dependencies] python = "^3.7" strawberry-graphql-django = {path = "../.."} [tool.poetry.dev-dependencies] django-debug-toolbar = ">=3.4" [build-system] requires = ["poetry-core>=1.0.0"] build-backend = "poetry.core.masonry.api" strawberry-graphql-django-0.62.0/poetry.lock000066400000000000000000002126731502405145400211060ustar00rootroot00000000000000# This file is automatically @generated by Poetry 2.1.3 and should not be changed by hand. [[package]] name = "asgiref" version = "3.8.1" description = "ASGI specs, helper code, and adapters" optional = false python-versions = ">=3.8" groups = ["main", "dev"] files = [ {file = "asgiref-3.8.1-py3-none-any.whl", hash = "sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47"}, {file = "asgiref-3.8.1.tar.gz", hash = "sha256:c343bd80a0bec947a9860adb4c432ffa7db769836c64238fc34bdc3fec84d590"}, ] [package.dependencies] typing-extensions = {version = ">=4", markers = "python_version < \"3.11\""} [package.extras] tests = ["mypy (>=0.800)", "pytest", "pytest-asyncio"] [[package]] name = "channels" version = "4.2.2" description = "Brings async, event-driven capabilities to Django." optional = false python-versions = ">=3.8" groups = ["dev"] files = [ {file = "channels-4.2.2-py3-none-any.whl", hash = "sha256:ff36a6e1576cacf40bcdc615fa7aece7a709fc4fdd2dc87f2971f4061ffdaa81"}, {file = "channels-4.2.2.tar.gz", hash = "sha256:8d7208e48ab8fdb972aaeae8311ce920637d97656ffc7ae5eca4f93f84bcd9a0"}, ] [package.dependencies] asgiref = ">=3.6.0,<4" Django = ">=4.2" [package.extras] daphne = ["daphne (>=4.0.0)"] tests = ["async-timeout", "coverage (>=4.5,<5.0)", "pytest", "pytest-asyncio", "pytest-django"] [[package]] name = "colorama" version = "0.4.6" description = "Cross-platform colored terminal text." optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" groups = ["dev"] files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] [[package]] name = "coverage" version = "7.8.2" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.9" groups = ["dev"] files = [ {file = "coverage-7.8.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bd8ec21e1443fd7a447881332f7ce9d35b8fbd2849e761bb290b584535636b0a"}, {file = "coverage-7.8.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4c26c2396674816deaeae7ded0e2b42c26537280f8fe313335858ffff35019be"}, {file = "coverage-7.8.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1aec326ed237e5880bfe69ad41616d333712c7937bcefc1343145e972938f9b3"}, {file = "coverage-7.8.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5e818796f71702d7a13e50c70de2a1924f729228580bcba1607cccf32eea46e6"}, {file = "coverage-7.8.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:546e537d9e24efc765c9c891328f30f826e3e4808e31f5d0f87c4ba12bbd1622"}, {file = "coverage-7.8.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ab9b09a2349f58e73f8ebc06fac546dd623e23b063e5398343c5270072e3201c"}, {file = "coverage-7.8.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:fd51355ab8a372d89fb0e6a31719e825cf8df8b6724bee942fb5b92c3f016ba3"}, {file = "coverage-7.8.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:0774df1e093acb6c9e4d58bce7f86656aeed6c132a16e2337692c12786b32404"}, {file = "coverage-7.8.2-cp310-cp310-win32.whl", hash = "sha256:00f2e2f2e37f47e5f54423aeefd6c32a7dbcedc033fcd3928a4f4948e8b96af7"}, {file = "coverage-7.8.2-cp310-cp310-win_amd64.whl", hash = "sha256:145b07bea229821d51811bf15eeab346c236d523838eda395ea969d120d13347"}, {file = "coverage-7.8.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b99058eef42e6a8dcd135afb068b3d53aff3921ce699e127602efff9956457a9"}, {file = "coverage-7.8.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5feb7f2c3e6ea94d3b877def0270dff0947b8d8c04cfa34a17be0a4dc1836879"}, {file = "coverage-7.8.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:670a13249b957bb9050fab12d86acef7bf8f6a879b9d1a883799276e0d4c674a"}, {file = "coverage-7.8.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0bdc8bf760459a4a4187b452213e04d039990211f98644c7292adf1e471162b5"}, {file = "coverage-7.8.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:07a989c867986c2a75f158f03fdb413128aad29aca9d4dbce5fc755672d96f11"}, {file = "coverage-7.8.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2db10dedeb619a771ef0e2949ccba7b75e33905de959c2643a4607bef2f3fb3a"}, {file = "coverage-7.8.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e6ea7dba4e92926b7b5f0990634b78ea02f208d04af520c73a7c876d5a8d36cb"}, {file = "coverage-7.8.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ef2f22795a7aca99fc3c84393a55a53dd18ab8c93fb431004e4d8f0774150f54"}, {file = "coverage-7.8.2-cp311-cp311-win32.whl", hash = "sha256:641988828bc18a6368fe72355df5f1703e44411adbe49bba5644b941ce6f2e3a"}, {file = "coverage-7.8.2-cp311-cp311-win_amd64.whl", hash = "sha256:8ab4a51cb39dc1933ba627e0875046d150e88478dbe22ce145a68393e9652975"}, {file = "coverage-7.8.2-cp311-cp311-win_arm64.whl", hash = "sha256:8966a821e2083c74d88cca5b7dcccc0a3a888a596a04c0b9668a891de3a0cc53"}, {file = "coverage-7.8.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e2f6fe3654468d061942591aef56686131335b7a8325684eda85dacdf311356c"}, {file = "coverage-7.8.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:76090fab50610798cc05241bf83b603477c40ee87acd358b66196ab0ca44ffa1"}, {file = "coverage-7.8.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2bd0a0a5054be160777a7920b731a0570284db5142abaaf81bcbb282b8d99279"}, {file = "coverage-7.8.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:da23ce9a3d356d0affe9c7036030b5c8f14556bd970c9b224f9c8205505e3b99"}, {file = "coverage-7.8.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9392773cffeb8d7e042a7b15b82a414011e9d2b5fdbbd3f7e6a6b17d5e21b20"}, {file = "coverage-7.8.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:876cbfd0b09ce09d81585d266c07a32657beb3eaec896f39484b631555be0fe2"}, {file = "coverage-7.8.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3da9b771c98977a13fbc3830f6caa85cae6c9c83911d24cb2d218e9394259c57"}, {file = "coverage-7.8.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9a990f6510b3292686713bfef26d0049cd63b9c7bb17e0864f133cbfd2e6167f"}, {file = "coverage-7.8.2-cp312-cp312-win32.whl", hash = "sha256:bf8111cddd0f2b54d34e96613e7fbdd59a673f0cf5574b61134ae75b6f5a33b8"}, {file = "coverage-7.8.2-cp312-cp312-win_amd64.whl", hash = "sha256:86a323a275e9e44cdf228af9b71c5030861d4d2610886ab920d9945672a81223"}, {file = "coverage-7.8.2-cp312-cp312-win_arm64.whl", hash = "sha256:820157de3a589e992689ffcda8639fbabb313b323d26388d02e154164c57b07f"}, {file = "coverage-7.8.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ea561010914ec1c26ab4188aef8b1567272ef6de096312716f90e5baa79ef8ca"}, {file = "coverage-7.8.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cb86337a4fcdd0e598ff2caeb513ac604d2f3da6d53df2c8e368e07ee38e277d"}, {file = "coverage-7.8.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26a4636ddb666971345541b59899e969f3b301143dd86b0ddbb570bd591f1e85"}, {file = "coverage-7.8.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5040536cf9b13fb033f76bcb5e1e5cb3b57c4807fef37db9e0ed129c6a094257"}, {file = "coverage-7.8.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc67994df9bcd7e0150a47ef41278b9e0a0ea187caba72414b71dc590b99a108"}, {file = "coverage-7.8.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6e6c86888fd076d9e0fe848af0a2142bf606044dc5ceee0aa9eddb56e26895a0"}, {file = "coverage-7.8.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:684ca9f58119b8e26bef860db33524ae0365601492e86ba0b71d513f525e7050"}, {file = "coverage-7.8.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8165584ddedb49204c4e18da083913bdf6a982bfb558632a79bdaadcdafd0d48"}, {file = "coverage-7.8.2-cp313-cp313-win32.whl", hash = "sha256:34759ee2c65362163699cc917bdb2a54114dd06d19bab860725f94ef45a3d9b7"}, {file = "coverage-7.8.2-cp313-cp313-win_amd64.whl", hash = "sha256:2f9bc608fbafaee40eb60a9a53dbfb90f53cc66d3d32c2849dc27cf5638a21e3"}, {file = "coverage-7.8.2-cp313-cp313-win_arm64.whl", hash = "sha256:9fe449ee461a3b0c7105690419d0b0aba1232f4ff6d120a9e241e58a556733f7"}, {file = "coverage-7.8.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:8369a7c8ef66bded2b6484053749ff220dbf83cba84f3398c84c51a6f748a008"}, {file = "coverage-7.8.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:159b81df53a5fcbc7d45dae3adad554fdbde9829a994e15227b3f9d816d00b36"}, {file = "coverage-7.8.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e6fcbbd35a96192d042c691c9e0c49ef54bd7ed865846a3c9d624c30bb67ce46"}, {file = "coverage-7.8.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:05364b9cc82f138cc86128dc4e2e1251c2981a2218bfcd556fe6b0fbaa3501be"}, {file = "coverage-7.8.2-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46d532db4e5ff3979ce47d18e2fe8ecad283eeb7367726da0e5ef88e4fe64740"}, {file = "coverage-7.8.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4000a31c34932e7e4fa0381a3d6deb43dc0c8f458e3e7ea6502e6238e10be625"}, {file = "coverage-7.8.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:43ff5033d657cd51f83015c3b7a443287250dc14e69910577c3e03bd2e06f27b"}, {file = "coverage-7.8.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:94316e13f0981cbbba132c1f9f365cac1d26716aaac130866ca812006f662199"}, {file = "coverage-7.8.2-cp313-cp313t-win32.whl", hash = "sha256:3f5673888d3676d0a745c3d0e16da338c5eea300cb1f4ada9c872981265e76d8"}, {file = "coverage-7.8.2-cp313-cp313t-win_amd64.whl", hash = "sha256:2c08b05ee8d7861e45dc5a2cc4195c8c66dca5ac613144eb6ebeaff2d502e73d"}, {file = "coverage-7.8.2-cp313-cp313t-win_arm64.whl", hash = "sha256:1e1448bb72b387755e1ff3ef1268a06617afd94188164960dba8d0245a46004b"}, {file = "coverage-7.8.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:496948261eaac5ac9cf43f5d0a9f6eb7a6d4cb3bedb2c5d294138142f5c18f2a"}, {file = "coverage-7.8.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:eacd2de0d30871eff893bab0b67840a96445edcb3c8fd915e6b11ac4b2f3fa6d"}, {file = "coverage-7.8.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b039ffddc99ad65d5078ef300e0c7eed08c270dc26570440e3ef18beb816c1ca"}, {file = "coverage-7.8.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0e49824808d4375ede9dd84e9961a59c47f9113039f1a525e6be170aa4f5c34d"}, {file = "coverage-7.8.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b069938961dfad881dc2f8d02b47645cd2f455d3809ba92a8a687bf513839787"}, {file = "coverage-7.8.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:de77c3ba8bb686d1c411e78ee1b97e6e0b963fb98b1637658dd9ad2c875cf9d7"}, {file = "coverage-7.8.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1676628065a498943bd3f64f099bb573e08cf1bc6088bbe33cf4424e0876f4b3"}, {file = "coverage-7.8.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:8e1a26e7e50076e35f7afafde570ca2b4d7900a491174ca357d29dece5aacee7"}, {file = "coverage-7.8.2-cp39-cp39-win32.whl", hash = "sha256:6782a12bf76fa61ad9350d5a6ef5f3f020b57f5e6305cbc663803f2ebd0f270a"}, {file = "coverage-7.8.2-cp39-cp39-win_amd64.whl", hash = "sha256:1efa4166ba75ccefd647f2d78b64f53f14fb82622bc94c5a5cb0a622f50f1c9e"}, {file = "coverage-7.8.2-pp39.pp310.pp311-none-any.whl", hash = "sha256:ec455eedf3ba0bbdf8f5a570012617eb305c63cb9f03428d39bf544cb2b94837"}, {file = "coverage-7.8.2-py3-none-any.whl", hash = "sha256:726f32ee3713f7359696331a18daf0c3b3a70bb0ae71141b9d3c52be7c595e32"}, {file = "coverage-7.8.2.tar.gz", hash = "sha256:a886d531373a1f6ff9fad2a2ba4a045b68467b779ae729ee0b3b10ac20033b27"}, ] [package.dependencies] tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} [package.extras] toml = ["tomli ; python_full_version <= \"3.11.0a6\""] [[package]] name = "django" version = "4.2.22" description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design." optional = false python-versions = ">=3.8" groups = ["main", "dev"] files = [ {file = "django-4.2.22-py3-none-any.whl", hash = "sha256:0a32773b5b7f4e774a155ee253ab24a841fed7e9e9061db08bf2ce9711da404d"}, {file = "django-4.2.22.tar.gz", hash = "sha256:e726764b094407c313adba5e2e866ab88f00436cad85c540a5bf76dc0a912c9e"}, ] [package.dependencies] asgiref = ">=3.6.0,<4" sqlparse = ">=0.3.1" tzdata = {version = "*", markers = "sys_platform == \"win32\""} [package.extras] argon2 = ["argon2-cffi (>=19.1.0)"] bcrypt = ["bcrypt"] [[package]] name = "django-choices-field" version = "2.3.0" description = "Django field that set/get django's new TextChoices/IntegerChoices enum." optional = false python-versions = ">=3.8,<4.0" groups = ["main", "dev"] files = [ {file = "django_choices_field-2.3.0-py3-none-any.whl", hash = "sha256:4bdcccf802bff9065af19798810de494dd16337fa46dae01680ede12a10280d6"}, {file = "django_choices_field-2.3.0.tar.gz", hash = "sha256:bb0c85c79737ab98bfb9c0d9ddf98010d612c0585be767890e25fd192c3d1694"}, ] [package.dependencies] django = ">=3.2" typing_extensions = ">=4.0.0" [[package]] name = "django-debug-toolbar" version = "4.4.6" description = "A configurable set of panels that display various debug information about the current request/response." optional = false python-versions = ">=3.8" groups = ["main", "dev"] files = [ {file = "django_debug_toolbar-4.4.6-py3-none-any.whl", hash = "sha256:3beb671c9ec44ffb817fad2780667f172bd1c067dbcabad6268ce39a81335f45"}, {file = "django_debug_toolbar-4.4.6.tar.gz", hash = "sha256:36e421cb908c2f0675e07f9f41e3d1d8618dc386392ec82d23bcfcd5d29c7044"}, ] [package.dependencies] django = ">=4.2.9" sqlparse = ">=0.2" [[package]] name = "django-guardian" version = "2.4.0" description = "Implementation of per object permissions for Django." optional = false python-versions = ">=3.5" groups = ["dev"] files = [ {file = "django-guardian-2.4.0.tar.gz", hash = "sha256:c58a68ae76922d33e6bdc0e69af1892097838de56e93e78a8361090bcd9f89a0"}, {file = "django_guardian-2.4.0-py3-none-any.whl", hash = "sha256:440ca61358427e575323648b25f8384739e54c38b3d655c81d75e0cd0d61b697"}, ] [package.dependencies] Django = ">=2.2" [[package]] name = "django-model-utils" version = "5.0.0" description = "Django model mixins and utilities" optional = false python-versions = ">=3.8" groups = ["dev"] files = [ {file = "django_model_utils-5.0.0-py3-none-any.whl", hash = "sha256:fec78e6c323d565a221f7c4edc703f4567d7bb1caeafe1acd16a80c5ff82056b"}, {file = "django_model_utils-5.0.0.tar.gz", hash = "sha256:041cdd6230d2fbf6cd943e1969318bce762272077f4ecd333ab2263924b4e5eb"}, ] [package.dependencies] Django = ">=3.2" [[package]] name = "django-polymorphic" version = "3.1.0" description = "Seamless polymorphic inheritance for Django models" optional = false python-versions = "*" groups = ["dev"] files = [ {file = "django-polymorphic-3.1.0.tar.gz", hash = "sha256:d6955b5308bf6e41dcb22ba7c96f00b51dfa497a8a5ab1e9c06c7951bf417bf8"}, {file = "django_polymorphic-3.1.0-py3-none-any.whl", hash = "sha256:08bc4f4f4a773a19b2deced5a56deddd1ef56ebd15207bf4052e2901c25ef57e"}, ] [package.dependencies] Django = ">=2.1" [[package]] name = "django-tree-queries" version = "0.19.0" description = "Tree queries with explicit opt-in, without configurability" optional = false python-versions = ">=3.8" groups = ["dev"] files = [ {file = "django_tree_queries-0.19.0-py3-none-any.whl", hash = "sha256:05b9e3158e31612528f136b4704a8d807e14edc0b4a607a45377e6132517ba2c"}, {file = "django_tree_queries-0.19.0.tar.gz", hash = "sha256:d1325e75f96e90b86c4316a3d63498101ec05703f4e629786b561e8aaab0e4a7"}, ] [package.extras] tests = ["coverage"] [[package]] name = "django-types" version = "0.20.0" description = "Type stubs for Django" optional = false python-versions = "<4.0,>=3.8" groups = ["dev"] files = [ {file = "django_types-0.20.0-py3-none-any.whl", hash = "sha256:a0b5c2c9a1e591684bb21a93b64e50ca6cb2d3eab48f49faff1eac706bd3a9c7"}, {file = "django_types-0.20.0.tar.gz", hash = "sha256:4e55d2c56155e3d69d75def9eb1d95a891303f2ac19fccf6fe8056da4293fae7"}, ] [package.dependencies] types-psycopg2 = ">=2.9.21.13" [[package]] name = "docopt" version = "0.6.2" description = "Pythonic argument parser, that will make you smile" optional = false python-versions = "*" groups = ["dev"] files = [ {file = "docopt-0.6.2.tar.gz", hash = "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491"}, ] [[package]] name = "exceptiongroup" version = "1.3.0" description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" groups = ["dev"] markers = "python_version < \"3.11\"" files = [ {file = "exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10"}, {file = "exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88"}, ] [package.dependencies] typing-extensions = {version = ">=4.6.0", markers = "python_version < \"3.13\""} [package.extras] test = ["pytest (>=6)"] [[package]] name = "factory-boy" version = "3.3.3" description = "A versatile test fixtures replacement based on thoughtbot's factory_bot for Ruby." optional = false python-versions = ">=3.8" groups = ["dev"] files = [ {file = "factory_boy-3.3.3-py2.py3-none-any.whl", hash = "sha256:1c39e3289f7e667c4285433f305f8d506efc2fe9c73aaea4151ebd5cdea394fc"}, {file = "factory_boy-3.3.3.tar.gz", hash = "sha256:866862d226128dfac7f2b4160287e899daf54f2612778327dd03d0e2cb1e3d03"}, ] [package.dependencies] Faker = ">=0.7.0" [package.extras] dev = ["Django", "Pillow", "SQLAlchemy", "coverage", "flake8", "isort", "mongoengine", "mongomock", "mypy", "tox", "wheel (>=0.32.0)", "zest.releaser[recommended]"] doc = ["Sphinx", "sphinx-rtd-theme", "sphinxcontrib-spelling"] [[package]] name = "faker" version = "37.3.0" description = "Faker is a Python package that generates fake data for you." optional = false python-versions = ">=3.9" groups = ["dev"] files = [ {file = "faker-37.3.0-py3-none-any.whl", hash = "sha256:48c94daa16a432f2d2bc803c7ff602509699fca228d13e97e379cd860a7e216e"}, {file = "faker-37.3.0.tar.gz", hash = "sha256:77b79e7a2228d57175133af0bbcdd26dc623df81db390ee52f5104d46c010f2f"}, ] [package.dependencies] tzdata = "*" [[package]] name = "graphql-core" version = "3.2.6" description = "GraphQL implementation for Python, a port of GraphQL.js, the JavaScript reference implementation for GraphQL." optional = false python-versions = "<4,>=3.6" groups = ["main"] files = [ {file = "graphql_core-3.2.6-py3-none-any.whl", hash = "sha256:78b016718c161a6fb20a7d97bbf107f331cd1afe53e45566c59f776ed7f0b45f"}, {file = "graphql_core-3.2.6.tar.gz", hash = "sha256:c08eec22f9e40f0bd61d805907e3b3b1b9a320bc606e23dc145eebca07c8fbab"}, ] [package.dependencies] typing-extensions = {version = ">=4,<5", markers = "python_version < \"3.10\""} [[package]] name = "iniconfig" version = "2.1.0" description = "brain-dead simple config-ini parsing" optional = false python-versions = ">=3.8" groups = ["dev"] files = [ {file = "iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760"}, {file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"}, ] [[package]] name = "packaging" version = "25.0" description = "Core utilities for Python packages" optional = false python-versions = ">=3.8" groups = ["main", "dev"] files = [ {file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"}, {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"}, ] [[package]] name = "pillow" version = "11.2.1" description = "Python Imaging Library (Fork)" optional = false python-versions = ">=3.9" groups = ["dev"] files = [ {file = "pillow-11.2.1-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:d57a75d53922fc20c165016a20d9c44f73305e67c351bbc60d1adaf662e74047"}, {file = "pillow-11.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:127bf6ac4a5b58b3d32fc8289656f77f80567d65660bc46f72c0d77e6600cc95"}, {file = "pillow-11.2.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b4ba4be812c7a40280629e55ae0b14a0aafa150dd6451297562e1764808bbe61"}, {file = "pillow-11.2.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8bd62331e5032bc396a93609982a9ab6b411c05078a52f5fe3cc59234a3abd1"}, {file = "pillow-11.2.1-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:562d11134c97a62fe3af29581f083033179f7ff435f78392565a1ad2d1c2c45c"}, {file = "pillow-11.2.1-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:c97209e85b5be259994eb5b69ff50c5d20cca0f458ef9abd835e262d9d88b39d"}, {file = "pillow-11.2.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0c3e6d0f59171dfa2e25d7116217543310908dfa2770aa64b8f87605f8cacc97"}, {file = "pillow-11.2.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc1c3bc53befb6096b84165956e886b1729634a799e9d6329a0c512ab651e579"}, {file = "pillow-11.2.1-cp310-cp310-win32.whl", hash = "sha256:312c77b7f07ab2139924d2639860e084ec2a13e72af54d4f08ac843a5fc9c79d"}, {file = "pillow-11.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:9bc7ae48b8057a611e5fe9f853baa88093b9a76303937449397899385da06fad"}, {file = "pillow-11.2.1-cp310-cp310-win_arm64.whl", hash = "sha256:2728567e249cdd939f6cc3d1f049595c66e4187f3c34078cbc0a7d21c47482d2"}, {file = "pillow-11.2.1-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:35ca289f712ccfc699508c4658a1d14652e8033e9b69839edf83cbdd0ba39e70"}, {file = "pillow-11.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0409af9f829f87a2dfb7e259f78f317a5351f2045158be321fd135973fff7bf"}, {file = "pillow-11.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4e5c5edee874dce4f653dbe59db7c73a600119fbea8d31f53423586ee2aafd7"}, {file = "pillow-11.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b93a07e76d13bff9444f1a029e0af2964e654bfc2e2c2d46bfd080df5ad5f3d8"}, {file = "pillow-11.2.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:e6def7eed9e7fa90fde255afaf08060dc4b343bbe524a8f69bdd2a2f0018f600"}, {file = "pillow-11.2.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:8f4f3724c068be008c08257207210c138d5f3731af6c155a81c2b09a9eb3a788"}, {file = "pillow-11.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a0a6709b47019dff32e678bc12c63008311b82b9327613f534e496dacaefb71e"}, {file = "pillow-11.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f6b0c664ccb879109ee3ca702a9272d877f4fcd21e5eb63c26422fd6e415365e"}, {file = "pillow-11.2.1-cp311-cp311-win32.whl", hash = "sha256:cc5d875d56e49f112b6def6813c4e3d3036d269c008bf8aef72cd08d20ca6df6"}, {file = "pillow-11.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:0f5c7eda47bf8e3c8a283762cab94e496ba977a420868cb819159980b6709193"}, {file = "pillow-11.2.1-cp311-cp311-win_arm64.whl", hash = "sha256:4d375eb838755f2528ac8cbc926c3e31cc49ca4ad0cf79cff48b20e30634a4a7"}, {file = "pillow-11.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:78afba22027b4accef10dbd5eed84425930ba41b3ea0a86fa8d20baaf19d807f"}, {file = "pillow-11.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:78092232a4ab376a35d68c4e6d5e00dfd73454bd12b230420025fbe178ee3b0b"}, {file = "pillow-11.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25a5f306095c6780c52e6bbb6109624b95c5b18e40aab1c3041da3e9e0cd3e2d"}, {file = "pillow-11.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c7b29dbd4281923a2bfe562acb734cee96bbb129e96e6972d315ed9f232bef4"}, {file = "pillow-11.2.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:3e645b020f3209a0181a418bffe7b4a93171eef6c4ef6cc20980b30bebf17b7d"}, {file = "pillow-11.2.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:b2dbea1012ccb784a65349f57bbc93730b96e85b42e9bf7b01ef40443db720b4"}, {file = "pillow-11.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:da3104c57bbd72948d75f6a9389e6727d2ab6333c3617f0a89d72d4940aa0443"}, {file = "pillow-11.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:598174aef4589af795f66f9caab87ba4ff860ce08cd5bb447c6fc553ffee603c"}, {file = "pillow-11.2.1-cp312-cp312-win32.whl", hash = "sha256:1d535df14716e7f8776b9e7fee118576d65572b4aad3ed639be9e4fa88a1cad3"}, {file = "pillow-11.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:14e33b28bf17c7a38eede290f77db7c664e4eb01f7869e37fa98a5aa95978941"}, {file = "pillow-11.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:21e1470ac9e5739ff880c211fc3af01e3ae505859392bf65458c224d0bf283eb"}, {file = "pillow-11.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:fdec757fea0b793056419bca3e9932eb2b0ceec90ef4813ea4c1e072c389eb28"}, {file = "pillow-11.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b0e130705d568e2f43a17bcbe74d90958e8a16263868a12c3e0d9c8162690830"}, {file = "pillow-11.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bdb5e09068332578214cadd9c05e3d64d99e0e87591be22a324bdbc18925be0"}, {file = "pillow-11.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d189ba1bebfbc0c0e529159631ec72bb9e9bc041f01ec6d3233d6d82eb823bc1"}, {file = "pillow-11.2.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:191955c55d8a712fab8934a42bfefbf99dd0b5875078240943f913bb66d46d9f"}, {file = "pillow-11.2.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:ad275964d52e2243430472fc5d2c2334b4fc3ff9c16cb0a19254e25efa03a155"}, {file = "pillow-11.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:750f96efe0597382660d8b53e90dd1dd44568a8edb51cb7f9d5d918b80d4de14"}, {file = "pillow-11.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fe15238d3798788d00716637b3d4e7bb6bde18b26e5d08335a96e88564a36b6b"}, {file = "pillow-11.2.1-cp313-cp313-win32.whl", hash = "sha256:3fe735ced9a607fee4f481423a9c36701a39719252a9bb251679635f99d0f7d2"}, {file = "pillow-11.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:74ee3d7ecb3f3c05459ba95eed5efa28d6092d751ce9bf20e3e253a4e497e691"}, {file = "pillow-11.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:5119225c622403afb4b44bad4c1ca6c1f98eed79db8d3bc6e4e160fc6339d66c"}, {file = "pillow-11.2.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:8ce2e8411c7aaef53e6bb29fe98f28cd4fbd9a1d9be2eeea434331aac0536b22"}, {file = "pillow-11.2.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:9ee66787e095127116d91dea2143db65c7bb1e232f617aa5957c0d9d2a3f23a7"}, {file = "pillow-11.2.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9622e3b6c1d8b551b6e6f21873bdcc55762b4b2126633014cea1803368a9aa16"}, {file = "pillow-11.2.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63b5dff3a68f371ea06025a1a6966c9a1e1ee452fc8020c2cd0ea41b83e9037b"}, {file = "pillow-11.2.1-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:31df6e2d3d8fc99f993fd253e97fae451a8db2e7207acf97859732273e108406"}, {file = "pillow-11.2.1-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:062b7a42d672c45a70fa1f8b43d1d38ff76b63421cbbe7f88146b39e8a558d91"}, {file = "pillow-11.2.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4eb92eca2711ef8be42fd3f67533765d9fd043b8c80db204f16c8ea62ee1a751"}, {file = "pillow-11.2.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f91ebf30830a48c825590aede79376cb40f110b387c17ee9bd59932c961044f9"}, {file = "pillow-11.2.1-cp313-cp313t-win32.whl", hash = "sha256:e0b55f27f584ed623221cfe995c912c61606be8513bfa0e07d2c674b4516d9dd"}, {file = "pillow-11.2.1-cp313-cp313t-win_amd64.whl", hash = "sha256:36d6b82164c39ce5482f649b437382c0fb2395eabc1e2b1702a6deb8ad647d6e"}, {file = "pillow-11.2.1-cp313-cp313t-win_arm64.whl", hash = "sha256:225c832a13326e34f212d2072982bb1adb210e0cc0b153e688743018c94a2681"}, {file = "pillow-11.2.1-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:7491cf8a79b8eb867d419648fff2f83cb0b3891c8b36da92cc7f1931d46108c8"}, {file = "pillow-11.2.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8b02d8f9cb83c52578a0b4beadba92e37d83a4ef11570a8688bbf43f4ca50909"}, {file = "pillow-11.2.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:014ca0050c85003620526b0ac1ac53f56fc93af128f7546623cc8e31875ab928"}, {file = "pillow-11.2.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3692b68c87096ac6308296d96354eddd25f98740c9d2ab54e1549d6c8aea9d79"}, {file = "pillow-11.2.1-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:f781dcb0bc9929adc77bad571b8621ecb1e4cdef86e940fe2e5b5ee24fd33b35"}, {file = "pillow-11.2.1-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:2b490402c96f907a166615e9a5afacf2519e28295f157ec3a2bb9bd57de638cb"}, {file = "pillow-11.2.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:dd6b20b93b3ccc9c1b597999209e4bc5cf2853f9ee66e3fc9a400a78733ffc9a"}, {file = "pillow-11.2.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:4b835d89c08a6c2ee7781b8dd0a30209a8012b5f09c0a665b65b0eb3560b6f36"}, {file = "pillow-11.2.1-cp39-cp39-win32.whl", hash = "sha256:b10428b3416d4f9c61f94b494681280be7686bda15898a3a9e08eb66a6d92d67"}, {file = "pillow-11.2.1-cp39-cp39-win_amd64.whl", hash = "sha256:6ebce70c3f486acf7591a3d73431fa504a4e18a9b97ff27f5f47b7368e4b9dd1"}, {file = "pillow-11.2.1-cp39-cp39-win_arm64.whl", hash = "sha256:c27476257b2fdcd7872d54cfd119b3a9ce4610fb85c8e32b70b42e3680a29a1e"}, {file = "pillow-11.2.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:9b7b0d4fd2635f54ad82785d56bc0d94f147096493a79985d0ab57aedd563156"}, {file = "pillow-11.2.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:aa442755e31c64037aa7c1cb186e0b369f8416c567381852c63444dd666fb772"}, {file = "pillow-11.2.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f0d3348c95b766f54b76116d53d4cb171b52992a1027e7ca50c81b43b9d9e363"}, {file = "pillow-11.2.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85d27ea4c889342f7e35f6d56e7e1cb345632ad592e8c51b693d7b7556043ce0"}, {file = "pillow-11.2.1-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:bf2c33d6791c598142f00c9c4c7d47f6476731c31081331664eb26d6ab583e01"}, {file = "pillow-11.2.1-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e616e7154c37669fc1dfc14584f11e284e05d1c650e1c0f972f281c4ccc53193"}, {file = "pillow-11.2.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:39ad2e0f424394e3aebc40168845fee52df1394a4673a6ee512d840d14ab3013"}, {file = "pillow-11.2.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:80f1df8dbe9572b4b7abdfa17eb5d78dd620b1d55d9e25f834efdbee872d3aed"}, {file = "pillow-11.2.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ea926cfbc3957090becbcbbb65ad177161a2ff2ad578b5a6ec9bb1e1cd78753c"}, {file = "pillow-11.2.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:738db0e0941ca0376804d4de6a782c005245264edaa253ffce24e5a15cbdc7bd"}, {file = "pillow-11.2.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9db98ab6565c69082ec9b0d4e40dd9f6181dab0dd236d26f7a50b8b9bfbd5076"}, {file = "pillow-11.2.1-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:036e53f4170e270ddb8797d4c590e6dd14d28e15c7da375c18978045f7e6c37b"}, {file = "pillow-11.2.1-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:14f73f7c291279bd65fda51ee87affd7c1e097709f7fdd0188957a16c264601f"}, {file = "pillow-11.2.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:208653868d5c9ecc2b327f9b9ef34e0e42a4cdd172c2988fd81d62d2bc9bc044"}, {file = "pillow-11.2.1.tar.gz", hash = "sha256:a64dd61998416367b7ef979b73d3a85853ba9bec4c2925f74e588879a58716b6"}, ] [package.extras] docs = ["furo", "olefile", "sphinx (>=8.2)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinxext-opengraph"] fpx = ["olefile"] mic = ["olefile"] test-arrow = ["pyarrow"] tests = ["check-manifest", "coverage (>=7.4.2)", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout", "trove-classifiers (>=2024.10.12)"] typing = ["typing-extensions ; python_version < \"3.10\""] xmp = ["defusedxml"] [[package]] name = "pluggy" version = "1.6.0" description = "plugin and hook calling mechanisms for python" optional = false python-versions = ">=3.9" groups = ["dev"] files = [ {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"}, {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"}, ] [package.extras] dev = ["pre-commit", "tox"] testing = ["coverage", "pytest", "pytest-benchmark"] [[package]] name = "psycopg2" version = "2.9.10" description = "psycopg2 - Python-PostgreSQL Database Adapter" optional = false python-versions = ">=3.8" groups = ["dev"] files = [ {file = "psycopg2-2.9.10-cp310-cp310-win32.whl", hash = "sha256:5df2b672140f95adb453af93a7d669d7a7bf0a56bcd26f1502329166f4a61716"}, {file = "psycopg2-2.9.10-cp310-cp310-win_amd64.whl", hash = "sha256:c6f7b8561225f9e711a9c47087388a97fdc948211c10a4bccbf0ba68ab7b3b5a"}, {file = "psycopg2-2.9.10-cp311-cp311-win32.whl", hash = "sha256:47c4f9875125344f4c2b870e41b6aad585901318068acd01de93f3677a6522c2"}, {file = "psycopg2-2.9.10-cp311-cp311-win_amd64.whl", hash = "sha256:0435034157049f6846e95103bd8f5a668788dd913a7c30162ca9503fdf542cb4"}, {file = "psycopg2-2.9.10-cp312-cp312-win32.whl", hash = "sha256:65a63d7ab0e067e2cdb3cf266de39663203d38d6a8ed97f5ca0cb315c73fe067"}, {file = "psycopg2-2.9.10-cp312-cp312-win_amd64.whl", hash = "sha256:4a579d6243da40a7b3182e0430493dbd55950c493d8c68f4eec0b302f6bbf20e"}, {file = "psycopg2-2.9.10-cp313-cp313-win_amd64.whl", hash = "sha256:91fd603a2155da8d0cfcdbf8ab24a2d54bca72795b90d2a3ed2b6da8d979dee2"}, {file = "psycopg2-2.9.10-cp39-cp39-win32.whl", hash = "sha256:9d5b3b94b79a844a986d029eee38998232451119ad653aea42bb9220a8c5066b"}, {file = "psycopg2-2.9.10-cp39-cp39-win_amd64.whl", hash = "sha256:88138c8dedcbfa96408023ea2b0c369eda40fe5d75002c0964c78f46f11fa442"}, {file = "psycopg2-2.9.10.tar.gz", hash = "sha256:12ec0b40b0273f95296233e8750441339298e6a572f7039da5b260e3c8b60e11"}, ] [[package]] name = "psycopg2-binary" version = "2.9.10" description = "psycopg2 - Python-PostgreSQL Database Adapter" optional = false python-versions = ">=3.8" groups = ["dev"] files = [ {file = "psycopg2-binary-2.9.10.tar.gz", hash = "sha256:4b3df0e6990aa98acda57d983942eff13d824135fe2250e6522edaa782a06de2"}, {file = "psycopg2_binary-2.9.10-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:0ea8e3d0ae83564f2fc554955d327fa081d065c8ca5cc6d2abb643e2c9c1200f"}, {file = "psycopg2_binary-2.9.10-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:3e9c76f0ac6f92ecfc79516a8034a544926430f7b080ec5a0537bca389ee0906"}, {file = "psycopg2_binary-2.9.10-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ad26b467a405c798aaa1458ba09d7e2b6e5f96b1ce0ac15d82fd9f95dc38a92"}, {file = "psycopg2_binary-2.9.10-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:270934a475a0e4b6925b5f804e3809dd5f90f8613621d062848dd82f9cd62007"}, {file = "psycopg2_binary-2.9.10-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:48b338f08d93e7be4ab2b5f1dbe69dc5e9ef07170fe1f86514422076d9c010d0"}, {file = "psycopg2_binary-2.9.10-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f4152f8f76d2023aac16285576a9ecd2b11a9895373a1f10fd9db54b3ff06b4"}, {file = "psycopg2_binary-2.9.10-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:32581b3020c72d7a421009ee1c6bf4a131ef5f0a968fab2e2de0c9d2bb4577f1"}, {file = "psycopg2_binary-2.9.10-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:2ce3e21dc3437b1d960521eca599d57408a695a0d3c26797ea0f72e834c7ffe5"}, {file = "psycopg2_binary-2.9.10-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:e984839e75e0b60cfe75e351db53d6db750b00de45644c5d1f7ee5d1f34a1ce5"}, {file = "psycopg2_binary-2.9.10-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3c4745a90b78e51d9ba06e2088a2fe0c693ae19cc8cb051ccda44e8df8a6eb53"}, {file = "psycopg2_binary-2.9.10-cp310-cp310-win32.whl", hash = "sha256:e5720a5d25e3b99cd0dc5c8a440570469ff82659bb09431c1439b92caf184d3b"}, {file = "psycopg2_binary-2.9.10-cp310-cp310-win_amd64.whl", hash = "sha256:3c18f74eb4386bf35e92ab2354a12c17e5eb4d9798e4c0ad3a00783eae7cd9f1"}, {file = "psycopg2_binary-2.9.10-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:04392983d0bb89a8717772a193cfaac58871321e3ec69514e1c4e0d4957b5aff"}, {file = "psycopg2_binary-2.9.10-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:1a6784f0ce3fec4edc64e985865c17778514325074adf5ad8f80636cd029ef7c"}, {file = "psycopg2_binary-2.9.10-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5f86c56eeb91dc3135b3fd8a95dc7ae14c538a2f3ad77a19645cf55bab1799c"}, {file = "psycopg2_binary-2.9.10-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2b3d2491d4d78b6b14f76881905c7a8a8abcf974aad4a8a0b065273a0ed7a2cb"}, {file = "psycopg2_binary-2.9.10-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2286791ececda3a723d1910441c793be44625d86d1a4e79942751197f4d30341"}, {file = "psycopg2_binary-2.9.10-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:512d29bb12608891e349af6a0cccedce51677725a921c07dba6342beaf576f9a"}, {file = "psycopg2_binary-2.9.10-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5a507320c58903967ef7384355a4da7ff3f28132d679aeb23572753cbf2ec10b"}, {file = "psycopg2_binary-2.9.10-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6d4fa1079cab9018f4d0bd2db307beaa612b0d13ba73b5c6304b9fe2fb441ff7"}, {file = "psycopg2_binary-2.9.10-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:851485a42dbb0bdc1edcdabdb8557c09c9655dfa2ca0460ff210522e073e319e"}, {file = "psycopg2_binary-2.9.10-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:35958ec9e46432d9076286dda67942ed6d968b9c3a6a2fd62b48939d1d78bf68"}, {file = "psycopg2_binary-2.9.10-cp311-cp311-win32.whl", hash = "sha256:ecced182e935529727401b24d76634a357c71c9275b356efafd8a2a91ec07392"}, {file = "psycopg2_binary-2.9.10-cp311-cp311-win_amd64.whl", hash = "sha256:ee0e8c683a7ff25d23b55b11161c2663d4b099770f6085ff0a20d4505778d6b4"}, {file = "psycopg2_binary-2.9.10-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:880845dfe1f85d9d5f7c412efea7a08946a46894537e4e5d091732eb1d34d9a0"}, {file = "psycopg2_binary-2.9.10-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:9440fa522a79356aaa482aa4ba500b65f28e5d0e63b801abf6aa152a29bd842a"}, {file = "psycopg2_binary-2.9.10-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e3923c1d9870c49a2d44f795df0c889a22380d36ef92440ff618ec315757e539"}, {file = "psycopg2_binary-2.9.10-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b2c956c028ea5de47ff3a8d6b3cc3330ab45cf0b7c3da35a2d6ff8420896526"}, {file = "psycopg2_binary-2.9.10-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f758ed67cab30b9a8d2833609513ce4d3bd027641673d4ebc9c067e4d208eec1"}, {file = "psycopg2_binary-2.9.10-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cd9b4f2cfab88ed4a9106192de509464b75a906462fb846b936eabe45c2063e"}, {file = "psycopg2_binary-2.9.10-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6dc08420625b5a20b53551c50deae6e231e6371194fa0651dbe0fb206452ae1f"}, {file = "psycopg2_binary-2.9.10-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:d7cd730dfa7c36dbe8724426bf5612798734bff2d3c3857f36f2733f5bfc7c00"}, {file = "psycopg2_binary-2.9.10-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:155e69561d54d02b3c3209545fb08938e27889ff5a10c19de8d23eb5a41be8a5"}, {file = "psycopg2_binary-2.9.10-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c3cc28a6fd5a4a26224007712e79b81dbaee2ffb90ff406256158ec4d7b52b47"}, {file = "psycopg2_binary-2.9.10-cp312-cp312-win32.whl", hash = "sha256:ec8a77f521a17506a24a5f626cb2aee7850f9b69a0afe704586f63a464f3cd64"}, {file = "psycopg2_binary-2.9.10-cp312-cp312-win_amd64.whl", hash = "sha256:18c5ee682b9c6dd3696dad6e54cc7ff3a1a9020df6a5c0f861ef8bfd338c3ca0"}, {file = "psycopg2_binary-2.9.10-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:26540d4a9a4e2b096f1ff9cce51253d0504dca5a85872c7f7be23be5a53eb18d"}, {file = "psycopg2_binary-2.9.10-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:e217ce4d37667df0bc1c397fdcd8de5e81018ef305aed9415c3b093faaeb10fb"}, {file = "psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:245159e7ab20a71d989da00f280ca57da7641fa2cdcf71749c193cea540a74f7"}, {file = "psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c4ded1a24b20021ebe677b7b08ad10bf09aac197d6943bfe6fec70ac4e4690d"}, {file = "psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3abb691ff9e57d4a93355f60d4f4c1dd2d68326c968e7db17ea96df3c023ef73"}, {file = "psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8608c078134f0b3cbd9f89b34bd60a943b23fd33cc5f065e8d5f840061bd0673"}, {file = "psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:230eeae2d71594103cd5b93fd29d1ace6420d0b86f4778739cb1a5a32f607d1f"}, {file = "psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:bb89f0a835bcfc1d42ccd5f41f04870c1b936d8507c6df12b7737febc40f0909"}, {file = "psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f0c2d907a1e102526dd2986df638343388b94c33860ff3bbe1384130828714b1"}, {file = "psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f8157bed2f51db683f31306aa497311b560f2265998122abe1dce6428bd86567"}, {file = "psycopg2_binary-2.9.10-cp313-cp313-win_amd64.whl", hash = "sha256:27422aa5f11fbcd9b18da48373eb67081243662f9b46e6fd07c3eb46e4535142"}, {file = "psycopg2_binary-2.9.10-cp38-cp38-macosx_12_0_x86_64.whl", hash = "sha256:eb09aa7f9cecb45027683bb55aebaaf45a0df8bf6de68801a6afdc7947bb09d4"}, {file = "psycopg2_binary-2.9.10-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b73d6d7f0ccdad7bc43e6d34273f70d587ef62f824d7261c4ae9b8b1b6af90e8"}, {file = "psycopg2_binary-2.9.10-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ce5ab4bf46a211a8e924d307c1b1fcda82368586a19d0a24f8ae166f5c784864"}, {file = "psycopg2_binary-2.9.10-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:056470c3dc57904bbf63d6f534988bafc4e970ffd50f6271fc4ee7daad9498a5"}, {file = "psycopg2_binary-2.9.10-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:73aa0e31fa4bb82578f3a6c74a73c273367727de397a7a0f07bd83cbea696baa"}, {file = "psycopg2_binary-2.9.10-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:8de718c0e1c4b982a54b41779667242bc630b2197948405b7bd8ce16bcecac92"}, {file = "psycopg2_binary-2.9.10-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:5c370b1e4975df846b0277b4deba86419ca77dbc25047f535b0bb03d1a544d44"}, {file = "psycopg2_binary-2.9.10-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:ffe8ed017e4ed70f68b7b371d84b7d4a790368db9203dfc2d222febd3a9c8863"}, {file = "psycopg2_binary-2.9.10-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:8aecc5e80c63f7459a1a2ab2c64df952051df196294d9f739933a9f6687e86b3"}, {file = "psycopg2_binary-2.9.10-cp39-cp39-macosx_12_0_x86_64.whl", hash = "sha256:7a813c8bdbaaaab1f078014b9b0b13f5de757e2b5d9be6403639b298a04d218b"}, {file = "psycopg2_binary-2.9.10-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d00924255d7fc916ef66e4bf22f354a940c67179ad3fd7067d7a0a9c84d2fbfc"}, {file = "psycopg2_binary-2.9.10-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7559bce4b505762d737172556a4e6ea8a9998ecac1e39b5233465093e8cee697"}, {file = "psycopg2_binary-2.9.10-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e8b58f0a96e7a1e341fc894f62c1177a7c83febebb5ff9123b579418fdc8a481"}, {file = "psycopg2_binary-2.9.10-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b269105e59ac96aba877c1707c600ae55711d9dcd3fc4b5012e4af68e30c648"}, {file = "psycopg2_binary-2.9.10-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:79625966e176dc97ddabc142351e0409e28acf4660b88d1cf6adb876d20c490d"}, {file = "psycopg2_binary-2.9.10-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:8aabf1c1a04584c168984ac678a668094d831f152859d06e055288fa515e4d30"}, {file = "psycopg2_binary-2.9.10-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:19721ac03892001ee8fdd11507e6a2e01f4e37014def96379411ca99d78aeb2c"}, {file = "psycopg2_binary-2.9.10-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7f5d859928e635fa3ce3477704acee0f667b3a3d3e4bb109f2b18d4005f38287"}, {file = "psycopg2_binary-2.9.10-cp39-cp39-win32.whl", hash = "sha256:3216ccf953b3f267691c90c6fe742e45d890d8272326b4a8b20850a03d05b7b8"}, {file = "psycopg2_binary-2.9.10-cp39-cp39-win_amd64.whl", hash = "sha256:30e34c4e97964805f715206c7b789d54a78b70f3ff19fbe590104b71c45600e5"}, ] [[package]] name = "pygments" version = "2.19.1" description = "Pygments is a syntax highlighting package written in Python." optional = false python-versions = ">=3.8" groups = ["dev"] files = [ {file = "pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c"}, {file = "pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f"}, ] [package.extras] windows-terminal = ["colorama (>=0.4.6)"] [[package]] name = "pytest" version = "8.4.0" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.9" groups = ["dev"] files = [ {file = "pytest-8.4.0-py3-none-any.whl", hash = "sha256:f40f825768ad76c0977cbacdf1fd37c6f7a468e460ea6a0636078f8972d4517e"}, {file = "pytest-8.4.0.tar.gz", hash = "sha256:14d920b48472ea0dbf68e45b96cd1ffda4705f33307dcc86c676c1b5104838a6"}, ] [package.dependencies] colorama = {version = ">=0.4", markers = "sys_platform == \"win32\""} exceptiongroup = {version = ">=1", markers = "python_version < \"3.11\""} iniconfig = ">=1" packaging = ">=20" pluggy = ">=1.5,<2" pygments = ">=2.7.2" tomli = {version = ">=1", markers = "python_version < \"3.11\""} [package.extras] dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"] [[package]] name = "pytest-asyncio" version = "1.0.0" description = "Pytest support for asyncio" optional = false python-versions = ">=3.9" groups = ["dev"] files = [ {file = "pytest_asyncio-1.0.0-py3-none-any.whl", hash = "sha256:4f024da9f1ef945e680dc68610b52550e36590a67fd31bb3b4943979a1f90ef3"}, {file = "pytest_asyncio-1.0.0.tar.gz", hash = "sha256:d15463d13f4456e1ead2594520216b225a16f781e144f8fdf6c5bb4667c48b3f"}, ] [package.dependencies] pytest = ">=8.2,<9" typing-extensions = {version = ">=4.12", markers = "python_version < \"3.10\""} [package.extras] docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1)"] testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"] [[package]] name = "pytest-cov" version = "6.1.1" description = "Pytest plugin for measuring coverage." optional = false python-versions = ">=3.9" groups = ["dev"] files = [ {file = "pytest_cov-6.1.1-py3-none-any.whl", hash = "sha256:bddf29ed2d0ab6f4df17b4c55b0a657287db8684af9c42ea546b21b1041b3dde"}, {file = "pytest_cov-6.1.1.tar.gz", hash = "sha256:46935f7aaefba760e716c2ebfbe1c216240b9592966e7da99ea8292d4d3e2a0a"}, ] [package.dependencies] coverage = {version = ">=7.5", extras = ["toml"]} pytest = ">=4.6" [package.extras] testing = ["fields", "hunter", "process-tests", "pytest-xdist", "virtualenv"] [[package]] name = "pytest-django" version = "4.11.1" description = "A Django plugin for pytest." optional = false python-versions = ">=3.8" groups = ["dev"] files = [ {file = "pytest_django-4.11.1-py3-none-any.whl", hash = "sha256:1b63773f648aa3d8541000c26929c1ea63934be1cfa674c76436966d73fe6a10"}, {file = "pytest_django-4.11.1.tar.gz", hash = "sha256:a949141a1ee103cb0e7a20f1451d355f83f5e4a5d07bdd4dcfdd1fd0ff227991"}, ] [package.dependencies] pytest = ">=7.0.0" [package.extras] docs = ["sphinx", "sphinx_rtd_theme"] testing = ["Django", "django-configurations (>=2.0)"] [[package]] name = "pytest-mock" version = "3.14.1" description = "Thin-wrapper around the mock package for easier use with pytest" optional = false python-versions = ">=3.8" groups = ["dev"] files = [ {file = "pytest_mock-3.14.1-py3-none-any.whl", hash = "sha256:178aefcd11307d874b4cd3100344e7e2d888d9791a6a1d9bfe90fbc1b74fd1d0"}, {file = "pytest_mock-3.14.1.tar.gz", hash = "sha256:159e9edac4c451ce77a5cdb9fc5d1100708d2dd4ba3c3df572f14097351af80e"}, ] [package.dependencies] pytest = ">=6.2.5" [package.extras] dev = ["pre-commit", "pytest-asyncio", "tox"] [[package]] name = "pytest-snapshot" version = "0.9.0" description = "A plugin for snapshot testing with pytest." optional = false python-versions = ">=3.5" groups = ["dev"] files = [ {file = "pytest-snapshot-0.9.0.tar.gz", hash = "sha256:c7013c3abc3e860f9feff899f8b4debe3708650d8d8242a61bf2625ff64db7f3"}, {file = "pytest_snapshot-0.9.0-py3-none-any.whl", hash = "sha256:4b9fe1c21c868fe53a545e4e3184d36bc1c88946e3f5c1d9dd676962a9b3d4ab"}, ] [package.dependencies] pytest = ">=3.0.0" [[package]] name = "pytest-watch" version = "4.2.0" description = "Local continuous test runner with pytest and watchdog." optional = false python-versions = "*" groups = ["dev"] files = [ {file = "pytest-watch-4.2.0.tar.gz", hash = "sha256:06136f03d5b361718b8d0d234042f7b2f203910d8568f63df2f866b547b3d4b9"}, ] [package.dependencies] colorama = ">=0.3.3" docopt = ">=0.4.0" pytest = ">=2.6.4" watchdog = ">=0.6.0" [[package]] name = "python-dateutil" version = "2.9.0.post0" description = "Extensions to the standard Python datetime module" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" groups = ["main"] files = [ {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, ] [package.dependencies] six = ">=1.5" [[package]] name = "ruff" version = "0.11.13" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" groups = ["dev"] files = [ {file = "ruff-0.11.13-py3-none-linux_armv6l.whl", hash = "sha256:4bdfbf1240533f40042ec00c9e09a3aade6f8c10b6414cf11b519488d2635d46"}, {file = "ruff-0.11.13-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:aef9c9ed1b5ca28bb15c7eac83b8670cf3b20b478195bd49c8d756ba0a36cf48"}, {file = "ruff-0.11.13-py3-none-macosx_11_0_arm64.whl", hash = "sha256:53b15a9dfdce029c842e9a5aebc3855e9ab7771395979ff85b7c1dedb53ddc2b"}, {file = "ruff-0.11.13-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab153241400789138d13f362c43f7edecc0edfffce2afa6a68434000ecd8f69a"}, {file = "ruff-0.11.13-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6c51f93029d54a910d3d24f7dd0bb909e31b6cd989a5e4ac513f4eb41629f0dc"}, {file = "ruff-0.11.13-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1808b3ed53e1a777c2ef733aca9051dc9bf7c99b26ece15cb59a0320fbdbd629"}, {file = "ruff-0.11.13-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:d28ce58b5ecf0f43c1b71edffabe6ed7f245d5336b17805803312ec9bc665933"}, {file = "ruff-0.11.13-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:55e4bc3a77842da33c16d55b32c6cac1ec5fb0fbec9c8c513bdce76c4f922165"}, {file = "ruff-0.11.13-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:633bf2c6f35678c56ec73189ba6fa19ff1c5e4807a78bf60ef487b9dd272cc71"}, {file = "ruff-0.11.13-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ffbc82d70424b275b089166310448051afdc6e914fdab90e08df66c43bb5ca9"}, {file = "ruff-0.11.13-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4a9ddd3ec62a9a89578c85842b836e4ac832d4a2e0bfaad3b02243f930ceafcc"}, {file = "ruff-0.11.13-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d237a496e0778d719efb05058c64d28b757c77824e04ffe8796c7436e26712b7"}, {file = "ruff-0.11.13-py3-none-musllinux_1_2_i686.whl", hash = "sha256:26816a218ca6ef02142343fd24c70f7cd8c5aa6c203bca284407adf675984432"}, {file = "ruff-0.11.13-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:51c3f95abd9331dc5b87c47ac7f376db5616041173826dfd556cfe3d4977f492"}, {file = "ruff-0.11.13-py3-none-win32.whl", hash = "sha256:96c27935418e4e8e77a26bb05962817f28b8ef3843a6c6cc49d8783b5507f250"}, {file = "ruff-0.11.13-py3-none-win_amd64.whl", hash = "sha256:29c3189895a8a6a657b7af4e97d330c8a3afd2c9c8f46c81e2fc5a31866517e3"}, {file = "ruff-0.11.13-py3-none-win_arm64.whl", hash = "sha256:b4385285e9179d608ff1d2fb9922062663c658605819a6876d8beef0c30b7f3b"}, {file = "ruff-0.11.13.tar.gz", hash = "sha256:26fa247dc68d1d4e72c179e08889a25ac0c7ba4d78aecfc835d49cbfd60bf514"}, ] [[package]] name = "setuptools" version = "80.9.0" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false python-versions = ">=3.9" groups = ["dev"] files = [ {file = "setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922"}, {file = "setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c"}, ] [package.extras] check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\"", "ruff (>=0.8.0) ; sys_platform != \"cygwin\""] core = ["importlib_metadata (>=6) ; python_version < \"3.10\"", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging (>=24.2)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1) ; python_version < \"3.11\"", "wheel (>=0.43.0)"] cover = ["pytest-cov"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"] enabler = ["pytest-enabler (>=2.2)"] test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21) ; python_version >= \"3.9\" and sys_platform != \"cygwin\"", "jaraco.envs (>=2.2)", "jaraco.path (>=3.7.2)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf ; sys_platform != \"cygwin\"", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] type = ["importlib_metadata (>=7.0.2) ; python_version < \"3.10\"", "jaraco.develop (>=7.21) ; sys_platform != \"cygwin\"", "mypy (==1.14.*)", "pytest-mypy"] [[package]] name = "six" version = "1.17.0" description = "Python 2 and 3 compatibility utilities" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" groups = ["main"] files = [ {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, ] [[package]] name = "sqlparse" version = "0.5.3" description = "A non-validating SQL parser." optional = false python-versions = ">=3.8" groups = ["main", "dev"] files = [ {file = "sqlparse-0.5.3-py3-none-any.whl", hash = "sha256:cf2196ed3418f3ba5de6af7e82c694a9fbdbfecccdfc72e281548517081f16ca"}, {file = "sqlparse-0.5.3.tar.gz", hash = "sha256:09f67787f56a0b16ecdbde1bfc7f5d9c3371ca683cfeaa8e6ff60b4807ec9272"}, ] [package.extras] dev = ["build", "hatch"] doc = ["sphinx"] [[package]] name = "strawberry-graphql" version = "0.271.1" description = "A library for creating GraphQL APIs" optional = false python-versions = ">=3.9" groups = ["main"] files = [ {file = "strawberry_graphql-0.271.1-py3-none-any.whl", hash = "sha256:21107ca984e5340dce106b0723fd70341e18b3591634a378a9f3576a1407767f"}, {file = "strawberry_graphql-0.271.1.tar.gz", hash = "sha256:72f019229198bb70bc804c8c5856e100a00dc618db14b58b6e9d9459bc599cb2"}, ] [package.dependencies] graphql-core = {version = ">=3.2.0,<3.4.0", markers = "python_version >= \"3.9\" and python_version < \"4.0\""} packaging = ">=23" python-dateutil = ">=2.7.0,<3.0.0" typing-extensions = ">=4.5.0" [package.extras] aiohttp = ["aiohttp (>=3.7.4.post0,<4.0.0)"] asgi = ["python-multipart (>=0.0.7)", "starlette (>=0.18.0)"] chalice = ["chalice (>=1.22,<2.0)"] channels = ["asgiref (>=3.2,<4.0)", "channels (>=3.0.5)"] cli = ["libcst (>=0.4.7,<1.8.0)", "pygments (>=2.3,<3.0)", "rich (>=12.0.0)", "typer (>=0.7.0)"] debug = ["libcst (>=0.4.7,<1.8.0)", "rich (>=12.0.0)"] debug-server = ["libcst (>=0.4.7,<1.8.0)", "pygments (>=2.3,<3.0)", "python-multipart (>=0.0.7)", "rich (>=12.0.0)", "starlette (>=0.18.0)", "typer (>=0.7.0)", "uvicorn (>=0.11.6)", "websockets (>=15.0.1,<16.0.0)"] django = ["Django (>=3.2)", "asgiref (>=3.2,<4.0)"] fastapi = ["fastapi (>=0.65.2)", "python-multipart (>=0.0.7)"] flask = ["flask (>=1.1)"] litestar = ["litestar (>=2) ; python_version >= \"3.10\" and python_version < \"4.0\""] opentelemetry = ["opentelemetry-api (<2)", "opentelemetry-sdk (<2)"] pydantic = ["pydantic (>1.6.1)"] pyinstrument = ["pyinstrument (>=4.0.0)"] quart = ["quart (>=0.19.3)"] sanic = ["sanic (>=20.12.2)"] [[package]] name = "tomli" version = "2.2.1" description = "A lil' TOML parser" optional = false python-versions = ">=3.8" groups = ["dev"] markers = "python_full_version <= \"3.11.0a6\"" files = [ {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"}, {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"}, {file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"}, {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"}, {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"}, {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"}, {file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"}, {file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"}, {file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"}, {file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"}, {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"}, {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"}, {file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"}, {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"}, {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"}, {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"}, {file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"}, {file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"}, {file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"}, {file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"}, {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"}, {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"}, {file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"}, {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"}, {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"}, {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"}, {file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"}, {file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"}, {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"}, {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}, ] [[package]] name = "types-psycopg2" version = "2.9.21.20250516" description = "Typing stubs for psycopg2" optional = false python-versions = ">=3.9" groups = ["dev"] files = [ {file = "types_psycopg2-2.9.21.20250516-py3-none-any.whl", hash = "sha256:2a9212d1e5e507017b31486ce8147634d06b85d652769d7a2d91d53cb4edbd41"}, {file = "types_psycopg2-2.9.21.20250516.tar.gz", hash = "sha256:6721018279175cce10b9582202e2a2b4a0da667857ccf82a97691bdb5ecd610f"}, ] [[package]] name = "typing-extensions" version = "4.14.0" description = "Backported and Experimental Type Hints for Python 3.9+" optional = false python-versions = ">=3.9" groups = ["main", "dev"] files = [ {file = "typing_extensions-4.14.0-py3-none-any.whl", hash = "sha256:a1514509136dd0b477638fc68d6a91497af5076466ad0fa6c338e44e359944af"}, {file = "typing_extensions-4.14.0.tar.gz", hash = "sha256:8676b788e32f02ab42d9e7c61324048ae4c6d844a399eebace3d4979d75ceef4"}, ] [[package]] name = "tzdata" version = "2025.2" description = "Provider of IANA time zone data" optional = false python-versions = ">=2" groups = ["main", "dev"] files = [ {file = "tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8"}, {file = "tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9"}, ] markers = {main = "sys_platform == \"win32\""} [[package]] name = "watchdog" version = "6.0.0" description = "Filesystem events monitoring" optional = false python-versions = ">=3.9" groups = ["dev"] files = [ {file = "watchdog-6.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1cdb490583ebd691c012b3d6dae011000fe42edb7a82ece80965b42abd61f26"}, {file = "watchdog-6.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bc64ab3bdb6a04d69d4023b29422170b74681784ffb9463ed4870cf2f3e66112"}, {file = "watchdog-6.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c897ac1b55c5a1461e16dae288d22bb2e412ba9807df8397a635d88f671d36c3"}, {file = "watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c"}, {file = "watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2"}, {file = "watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c"}, {file = "watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948"}, {file = "watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860"}, {file = "watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0"}, {file = "watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c"}, {file = "watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134"}, {file = "watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b"}, {file = "watchdog-6.0.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e6f0e77c9417e7cd62af82529b10563db3423625c5fce018430b249bf977f9e8"}, {file = "watchdog-6.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:90c8e78f3b94014f7aaae121e6b909674df5b46ec24d6bebc45c44c56729af2a"}, {file = "watchdog-6.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e7631a77ffb1f7d2eefa4445ebbee491c720a5661ddf6df3498ebecae5ed375c"}, {file = "watchdog-6.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c7ac31a19f4545dd92fc25d200694098f42c9a8e391bc00bdd362c5736dbf881"}, {file = "watchdog-6.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9513f27a1a582d9808cf21a07dae516f0fab1cf2d7683a742c498b93eedabb11"}, {file = "watchdog-6.0.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7a0e56874cfbc4b9b05c60c8a1926fedf56324bb08cfbc188969777940aef3aa"}, {file = "watchdog-6.0.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:e6439e374fc012255b4ec786ae3c4bc838cd7309a540e5fe0952d03687d8804e"}, {file = "watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13"}, {file = "watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379"}, {file = "watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e"}, {file = "watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f"}, {file = "watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26"}, {file = "watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c"}, {file = "watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2"}, {file = "watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a"}, {file = "watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680"}, {file = "watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f"}, {file = "watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282"}, ] [package.extras] watchmedo = ["PyYAML (>=3.10)"] [extras] debug-toolbar = ["django-debug-toolbar"] enum = ["django-choices-field"] [metadata] lock-version = "2.1" python-versions = ">=3.9,<4.0" content-hash = "9581f3088b90897fa487251e2021f0c47028ec905c16c66f20e91138c825d83f" strawberry-graphql-django-0.62.0/pyproject.toml000066400000000000000000000106061502405145400216160ustar00rootroot00000000000000[tool.poetry] name = "strawberry-graphql-django" packages = [{ include = "strawberry_django" }] version = "0.62.0" description = "Strawberry GraphQL Django extension" authors = [ "Lauri Hintsala ", "Thiago Bellini Ribeiro ", ] maintainers = ["Thiago Bellini Ribeiro "] repository = "https://github.com/strawberry-graphql/strawberry-django" documentation = "https://strawberry.rocks/docs/django" license = "MIT" readme = "README.md" keywords = ["graphql", "api", "django", "strawberry-graphql"] classifiers = [ "Environment :: Web Environment", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Framework :: Django", "Framework :: Django :: 4.2", "Framework :: Django :: 5.0", "Framework :: Django :: 5.1", "Framework :: Django :: 5.2", ] [tool.poetry.dependencies] python = ">=3.9,<4.0" django = ">=4.2" asgiref = ">=3.8" django-choices-field = { version = ">=2.2.2", optional = true } django-debug-toolbar = { version = ">=3.4", optional = true } strawberry-graphql = ">=0.264.0" [tool.poetry.group.dev.dependencies] channels = { version = ">=3.0.5" } django-choices-field = "^2.2.2" django-debug-toolbar = "^4.4.6" django-guardian = "^2.4.0" django-types = "^0.20.0" factory-boy = "^3.2.1" pillow = "^11.0.0" pytest = "^8.0.2" pytest-asyncio = "^1.0.0" pytest-cov = "^6.0.0" pytest-django = "^4.1.0" pytest-mock = "^3.5.1" pytest-snapshot = "^0.9.0" pytest-watch = "^4.2.0" ruff = "^0.11.2" django-polymorphic = "^3.1.0" setuptools = "^80.1.0" psycopg2 = "^2.9.9" psycopg2-binary = "^2.9.9" django-tree-queries = "^0.19.0" django-model-utils = "^5.0.0" [tool.poetry.extras] debug-toolbar = ["django-debug-toolbar"] enum = ["django-choices-field"] [build-system] requires = ["poetry-core>=1.0.0", "setuptools"] build-backend = "poetry.core.masonry.api" [tool.ruff] target-version = "py39" preview = true [tool.ruff.lint] extend-select = [ "A", "ASYNC", "B", "BLE", "C4", "COM", "D", "D2", "D3", "D4", "DTZ", "E", "ERA", "EXE", "F", "FURB", "G", "I", "ICN001", "INP", "ISC", "N", "PERF", "PGH", "PIE", "PL", "PT", "PTH", "PYI", "Q", "RET", "RSE", "RUF", "SIM", "SLF", "SLOT", "T10", "T20", "TCH", "TID", "TRY", "UP", "W", "YTT", ] extend-ignore = [ "A005", "D1", "D203", "D213", "D417", "E203", "PGH003", "PLR09", "SLF001", "TRY003", "PLR6301", "PLC0415", "TC002", # ruff formatter recommends to disable those "COM812", "COM819", "D206", "E111", "E114", "E117", "E501", "ISC001", "Q000", "Q001", "Q002", "Q003", "W191", ] exclude = [ ".eggs", ".git", ".hg", ".mypy_cache", ".tox", ".venv", "__pycached__", "_build", "buck-out", "build", "dist", ] [tool.ruff.lint.per-file-ignores] "tests/*" = ["A003", "PLW0603", "PLR2004"] "examples/*" = ["A003"] "**/migrations/*" = ["RUF012"] [tool.ruff.lint.pylint] max-nested-blocks = 7 [tool.ruff.lint.isort] [tool.ruff.format] [tool.pyright] pythonVersion = "3.9" useLibraryCodeForTypes = true exclude = [".venv", "**/migrations", "dist", "docs"] reportCallInDefaultInitializer = "warning" reportMatchNotExhaustive = "warning" reportMissingSuperCall = "warning" reportOverlappingOverload = "warning" reportUninitializedInstanceVariable = "none" reportUnnecessaryCast = "warning" reportUnnecessaryTypeIgnoreComment = "warning" reportUntypedNamedTuple = "error" reportUnusedExpression = "warning" reportUnnecessaryComparison = "warning" reportUnnecessaryContains = "warning" strictDictionaryInference = true strictListInference = true strictSetInference = true [tool.pytest.ini_options] DJANGO_SETTINGS_MODULE = "tests.django_settings" testpaths = ["tests"] filterwarnings = "ignore:.*is deprecated.*:DeprecationWarning" addopts = "--nomigrations --cov=./ --cov-report term-missing:skip-covered" asyncio_mode = "auto" strawberry-graphql-django-0.62.0/strawberry_django/000077500000000000000000000000001502405145400224255ustar00rootroot00000000000000strawberry-graphql-django-0.62.0/strawberry_django/__init__.py000066400000000000000000000041721502405145400245420ustar00rootroot00000000000000import warnings from typing import TYPE_CHECKING, Any from . import auth, filters, mutations, ordering, pagination, relay from .fields.field import connection, field, node, offset_paginated from .fields.filter_order import filter_field, order_field from .fields.filter_types import ( BaseFilterLookup, ComparisonFilterLookup, DateFilterLookup, DatetimeFilterLookup, FilterLookup, RangeLookup, TimeFilterLookup, ) from .fields.types import ( DjangoFileType, DjangoImageType, DjangoModelType, ListInput, ManyToManyInput, ManyToOneInput, NodeInput, NodeInputPartial, OneToManyInput, OneToOneInput, ) from .filters import filter_type, process_filters from .mutations.mutations import input_mutation, mutation from .ordering import Ordering, order, order_type, process_order from .resolvers import django_resolver from .type import input, interface, partial, type # noqa: A004 if TYPE_CHECKING: from strawberry_django.filters import filter # noqa: A004, F401 __all__ = [ "BaseFilterLookup", "ComparisonFilterLookup", "DateFilterLookup", "DatetimeFilterLookup", "DjangoFileType", "DjangoImageType", "DjangoModelType", "FilterLookup", "ListInput", "ManyToManyInput", "ManyToOneInput", "NodeInput", "NodeInputPartial", "OneToManyInput", "OneToOneInput", "Ordering", "RangeLookup", "TimeFilterLookup", "auth", "connection", "django_resolver", "field", "filter_field", "filter_type", "filters", "input", "input_mutation", "interface", "mutation", "mutations", "node", "offset_paginated", "order", "order_field", "order_type", "ordering", "pagination", "partial", "process_filters", "process_order", "relay", "type", ] def __getattr__(name: str) -> Any: if name == "filter": warnings.warn( "`filter` is deprecated, use `filter_type` instead.", DeprecationWarning, stacklevel=2, ) return filter_type raise AttributeError(f"module {__name__} has no attribute {name}") strawberry-graphql-django-0.62.0/strawberry_django/apps.py000066400000000000000000000002231502405145400237370ustar00rootroot00000000000000from django.apps import AppConfig class StrawberryDjangoConfig(AppConfig): name = "strawberry_django" verbose_name = "Strawberry django" strawberry-graphql-django-0.62.0/strawberry_django/arguments.py000066400000000000000000000011371502405145400250060ustar00rootroot00000000000000from typing import Optional from strawberry import UNSET from strawberry.annotation import StrawberryAnnotation from strawberry.types.arguments import StrawberryArgument def argument( name: str, type_: type, *, is_list: bool = False, is_optional: bool = False, default: object = UNSET, ): if is_list: type_ = list[type_] if is_optional: type_ = Optional[type_] return StrawberryArgument( default=default, description=None, graphql_name=None, python_name=name, type_annotation=StrawberryAnnotation(type_), ) strawberry-graphql-django-0.62.0/strawberry_django/auth/000077500000000000000000000000001502405145400233665ustar00rootroot00000000000000strawberry-graphql-django-0.62.0/strawberry_django/auth/__init__.py000066400000000000000000000002141502405145400254740ustar00rootroot00000000000000from .mutations import login, logout, register from .queries import current_user __all__ = ["current_user", "login", "logout", "register"] strawberry-graphql-django-0.62.0/strawberry_django/auth/mutations.py000066400000000000000000000065611502405145400257730ustar00rootroot00000000000000from __future__ import annotations import functools from typing import TYPE_CHECKING, Any, cast import strawberry from asgiref.sync import async_to_sync from django.contrib import auth from django.contrib.auth.password_validation import validate_password from django.core.exceptions import ValidationError from strawberry_django.auth.utils import get_current_user from strawberry_django.mutations import mutations, resolvers from strawberry_django.mutations.fields import DjangoCreateMutation from strawberry_django.optimizer import DjangoOptimizerExtension from strawberry_django.resolvers import django_resolver from strawberry_django.utils.requests import get_request try: # Django-channels is not always used/intalled, # therefore it shouldn't be it a hard requirement. from channels import auth as channels_auth except ModuleNotFoundError: channels_auth = None if TYPE_CHECKING: from django.contrib.auth.base_user import AbstractBaseUser from strawberry.types import Info @django_resolver def resolve_login(info: Info, username: str, password: str) -> AbstractBaseUser: request = get_request(info) user = auth.authenticate(request, username=username, password=password) if user is None: raise ValidationError("Incorrect username/password") try: auth.login(request, user) except AttributeError: # ASGI in combo with websockets needs the channels login functionality. # to ensure we're talking about channels, let's veriy that our # request is actually channelsrequest try: scope = request.consumer.scope # type: ignore async_to_sync(channels_auth.login)(scope, user) # type: ignore # According to channels docs you must save the session scope["session"].save() except (AttributeError, NameError): # When Django-channels is not installed, # this code will be non-existing pass return user @django_resolver def resolve_logout(info: Info) -> bool: user = get_current_user(info) ret = user.is_authenticated try: request = get_request(info) auth.logout(request) except AttributeError: try: scope = request.consumer.scope # type: ignore async_to_sync(channels_auth.logout)(scope) # type: ignore except (AttributeError, NameError): # When Django-channels is not installed, # this code will be non-existing pass return ret class DjangoRegisterMutation(DjangoCreateMutation): def create(self, data: dict[str, Any], *, info: Info): model = cast("type[AbstractBaseUser]", self.django_model) assert model is not None password = data.pop("password") validate_password(password) # Do not optimize anything while retrieving the object to update with DjangoOptimizerExtension.disabled(): return resolvers.create( info, model, data, key_attr=self.key_attr, full_clean=self.full_clean, pre_save_hook=lambda obj: obj.set_password(password), ) login = functools.partial(strawberry.mutation, resolver=resolve_login) logout = functools.partial(strawberry.mutation, resolver=resolve_logout) register = mutations.create if TYPE_CHECKING else DjangoRegisterMutation strawberry-graphql-django-0.62.0/strawberry_django/auth/queries.py000066400000000000000000000011271502405145400254160ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING from django.core.exceptions import ValidationError import strawberry_django from .utils import get_current_user if TYPE_CHECKING: from strawberry.types import Info from strawberry_django.utils.typing import UserType def resolve_current_user(info: Info) -> UserType: user = get_current_user(info) if not getattr(user, "is_authenticated", False): raise ValidationError("User is not logged in.") return user def current_user(): return strawberry_django.field(resolver=resolve_current_user) strawberry-graphql-django-0.62.0/strawberry_django/auth/utils.py000066400000000000000000000030131502405145400250750ustar00rootroot00000000000000from typing import Literal, overload from asgiref.sync import sync_to_async from strawberry.types import Info from strawberry_django.utils.requests import get_request from strawberry_django.utils.typing import UserType @overload def get_current_user(info: Info, *, strict: Literal[True]) -> UserType: ... @overload def get_current_user(info: Info, *, strict: bool = False) -> UserType: ... def get_current_user(info: Info, *, strict: bool = False) -> UserType: """Get and return the current user based on various scenarios.""" request = get_request(info) try: user = request.user except AttributeError: try: # queries/mutations in ASGI move the user into consumer scope user = request.consumer.scope["user"] # type: ignore except AttributeError: # websockets / subscriptions move scope inside of the request user = request.scope.get("user") # type: ignore if user is None: raise ValueError("No user found in the current request") # Access an attribute inside the user object to force loading it in async contexts. _ = user.is_authenticated return user @overload async def aget_current_user( info: Info, *, strict: Literal[True], ) -> UserType: ... @overload async def aget_current_user( info: Info, *, strict: bool = False, ) -> UserType: ... async def aget_current_user(info: Info, *, strict: bool = False) -> UserType: return await sync_to_async(get_current_user)(info, strict=strict) strawberry-graphql-django-0.62.0/strawberry_django/descriptors.py000066400000000000000000000137511502405145400253470ustar00rootroot00000000000000import inspect from collections.abc import Callable from typing import ( TYPE_CHECKING, Any, Generic, Optional, TypeVar, Union, overload, ) from django.db.models.base import Model from strawberry.exceptions import MissingFieldAnnotationError from typing_extensions import Self if TYPE_CHECKING: from strawberry_django.optimizer import OptimizerStore from .utils.typing import AnnotateType, PrefetchType, TypeOrMapping, TypeOrSequence __all__ = [ "ModelProperty", "model_cached_property", "model_property", ] _M = TypeVar("_M", bound=Model) _R = TypeVar("_R") class ModelProperty(Generic[_M, _R]): """Model property with optimization hinting functionality.""" name: str store: "OptimizerStore" def __init__( self, func: Callable[[_M], _R], *, cached: bool = False, meta: Optional[dict[Any, Any]] = None, only: Optional["TypeOrSequence[str]"] = None, select_related: Optional["TypeOrSequence[str]"] = None, prefetch_related: Optional["TypeOrSequence[PrefetchType]"] = None, annotate: Optional["TypeOrMapping[AnnotateType]"] = None, ): from .optimizer import OptimizerStore super().__init__() self.func = func self.cached = cached self.meta = meta self.store = OptimizerStore.with_hints( only=only, select_related=select_related, prefetch_related=prefetch_related, annotate=annotate, ) def __set_name__(self, owner: type[_M], name: str): self.origin = owner self.name = name @overload def __get__(self, obj: _M, cls: type[_M]) -> _R: ... @overload def __get__(self, obj: None, cls: type[_M]) -> Self: ... def __get__(self, obj, cls=None): if obj is None: return self if not self.cached: return self.func(obj) try: ret = obj.__dict__[self.name] except KeyError: ret = self.func(obj) obj.__dict__[self.name] = ret return ret @property def description(self) -> Optional[str]: if not self.func.__doc__: return None return inspect.cleandoc(self.func.__doc__) @property def type_annotation(self) -> Union[object, str]: ret = self.func.__annotations__.get("return") if ret is None: raise MissingFieldAnnotationError(self.name, self.origin) return ret @overload def model_property( func: Callable[[_M], _R], *, cached: bool = False, meta: Optional[dict[Any, Any]] = None, only: Optional["TypeOrSequence[str]"] = None, select_related: Optional["TypeOrSequence[str]"] = None, prefetch_related: Optional["TypeOrSequence[PrefetchType]"] = None, annotate: Optional["TypeOrMapping[AnnotateType]"] = None, ) -> ModelProperty[_M, _R]: ... @overload def model_property( func: None = ..., *, cached: bool = False, meta: Optional[dict[Any, Any]] = None, only: Optional["TypeOrSequence[str]"] = None, select_related: Optional["TypeOrSequence[str]"] = None, prefetch_related: Optional["TypeOrSequence[PrefetchType]"] = None, annotate: Optional["TypeOrMapping[AnnotateType]"] = None, ) -> Callable[[Callable[[_M], _R]], ModelProperty[_M, _R]]: ... def model_property( func=None, *, cached: bool = False, meta: Optional[dict[Any, Any]] = None, only: Optional["TypeOrSequence[str]"] = None, select_related: Optional["TypeOrSequence[str]"] = None, prefetch_related: Optional["TypeOrSequence[PrefetchType]"] = None, annotate: Optional["TypeOrMapping[AnnotateType]"] = None, ) -> Any: def wrapper(f): return ModelProperty( f, cached=cached, meta=meta, only=only, select_related=select_related, prefetch_related=prefetch_related, annotate=annotate, ) if func is not None: return wrapper(func) return wrapper def model_cached_property( func=None, *, meta: Optional[dict[Any, Any]] = None, only: Optional["TypeOrSequence[str]"] = None, select_related: Optional["TypeOrSequence[str]"] = None, prefetch_related: Optional["TypeOrSequence[PrefetchType]"] = None, annotate: Optional["TypeOrMapping[AnnotateType]"] = None, ): """Property with gql optimization hinting. Decorate a method, just like you would do with a `@property`, and when accessing it through a graphql resolver, if `DjangoOptimizerExtension` is enabled, it will automatically optimize the hintings on this field. Args: ---- func: The method to decorate. meta: Some extra metadata to be attached to the field. only: Optional sequence of values to optimize using `QuerySet.only` select_related: Optional sequence of values to optimize using `QuerySet.select_related` prefetch_related: Optional sequence of values to optimize using `QuerySet.prefetch_related` annotate: Optional mapping of values to use in `QuerySet.annotate` Returns: ------- The decorated method. Examples: -------- In a model, define it like this to have the hintings defined in `col_b_formatted` automatically optimized. >>> class SomeModel(models.Model): ... col_a = models.CharField() ... col_b = models.CharField() ... ... @model_cached_property(only=["col_b"]) ... def col_b_formatted(self): ... return f"Formatted: {self.col_b}" ... >>> @gql.django.type(SomeModel) ... class SomeModelType ... col_a: gql.auto ... col_b_formatted: gql.auto """ return model_property( func, cached=True, meta=meta, only=only, select_related=select_related, prefetch_related=prefetch_related, annotate=annotate, ) strawberry-graphql-django-0.62.0/strawberry_django/exceptions.py000066400000000000000000000046201502405145400251620ustar00rootroot00000000000000from __future__ import annotations from functools import cached_property from typing import TYPE_CHECKING from strawberry.exceptions.exception import StrawberryException from strawberry.exceptions.utils.source_finder import SourceFinder if TYPE_CHECKING: from strawberry.exceptions.exception_source import ExceptionSource from strawberry_django.fields.filter_order import FilterOrderFieldResolver class MissingFieldArgumentError(StrawberryException): def __init__(self, field_name: str, resolver: FilterOrderFieldResolver): self.function = resolver.wrapped_func self.message = f'Missing required argument "{field_name}" in "{resolver.name}"' self.rich_message = ( f'[bold red]Missing argument [underline]"{field_name}" for field ' f"`[underline]{resolver.name}[/]`" ) self.annotation_message = "field missing argument" super().__init__(self.message) @cached_property def exception_source(self) -> ExceptionSource | None: # pragma: no cover source_finder = SourceFinder() return source_finder.find_function_from_object(self.function) # type: ignore class ForbiddenFieldArgumentError(StrawberryException): def __init__(self, resolver: FilterOrderFieldResolver, arguments: list[str]): self.extra_arguments = arguments self.function = resolver.wrapped_func self.argument_name = arguments[0] self.message = ( f'Found disallowed {self.extra_arguments_str} in field "{resolver.name}"' ) self.rich_message = ( f"Found disallowed {self.extra_arguments_str} in " f"`[underline]{resolver.name}[/]`" ) self.suggestion = "To fix this error, remove offending argument(s)" self.annotation_message = "forbidden field argument" super().__init__(self.message) @property def extra_arguments_str(self) -> str: arguments = self.extra_arguments if len(arguments) == 1: return f'argument "{arguments[0]}"' head = ", ".join(arguments[:-1]) return f'arguments "{head}" and "{arguments[-1]}"' @cached_property def exception_source(self) -> ExceptionSource | None: # pragma: no cover source_finder = SourceFinder() return source_finder.find_argument_from_object( self.function, # type: ignore self.argument_name, ) strawberry-graphql-django-0.62.0/strawberry_django/extensions/000077500000000000000000000000001502405145400246245ustar00rootroot00000000000000strawberry-graphql-django-0.62.0/strawberry_django/extensions/__init__.py000066400000000000000000000000001502405145400267230ustar00rootroot00000000000000strawberry-graphql-django-0.62.0/strawberry_django/extensions/django_cache_base.py000066400000000000000000000037161502405145400305640ustar00rootroot00000000000000from collections.abc import Callable, Hashable from functools import _make_key # noqa: PLC2701 from typing import Optional, cast from django.core.cache import caches from django.core.cache.backends.base import DEFAULT_TIMEOUT from strawberry.extensions import SchemaExtension from strawberry.types import ExecutionContext class DjangoCacheBase(SchemaExtension): """Base for a Cache that uses Django built in cache instead of an in memory cache. Arguments: --------- `cache_name: str` Name of the Django Cache to use, defaults to 'default' `timeout: Optional[int]` How long to hold items in the cache. See the Django Cache docs for details https://docs.djangoproject.com/en/4.0/topics/cache/ `hash_fn: Optional[Callable[[Tuple, Dict], str]]` A function to use to generate the cache keys Defaults to the same key generator as functools.lru_cache WARNING! The default function does NOT work with memcached and will generate warnings """ def __init__( self, cache_name: str = "default", timeout: Optional[int] = None, hash_fn: Optional[Callable[[tuple, dict], Hashable]] = None, *, execution_context: Optional[ExecutionContext] = None, ): super().__init__(execution_context=cast("ExecutionContext", execution_context)) self.cache = caches[cache_name] self.timeout = timeout or DEFAULT_TIMEOUT # Use same key generating function as functools.lru_cache as default self.hash_fn = hash_fn or (lambda args, kwargs: _make_key(args, kwargs, False)) def execute_cached(self, func, *args, **kwargs): hash_key = cast("str", self.hash_fn(args, kwargs)) cache_result = self.cache.get(hash_key) if cache_result is not None: return cache_result func_result = func(*args, **kwargs) self.cache.set(hash_key, func_result, timeout=self.timeout) return func_result strawberry-graphql-django-0.62.0/strawberry_django/extensions/django_validation_cache.py000066400000000000000000000012731502405145400320000ustar00rootroot00000000000000from collections.abc import Iterator from graphql.validation import validate from strawberry.schema.validation_rules.one_of import OneOfInputValidationRule from .django_cache_base import DjangoCacheBase class DjangoValidationCache(DjangoCacheBase): def on_validate(self) -> Iterator[None]: execution_context = self.execution_context errors = self.execute_cached( validate, execution_context.schema._schema, execution_context.graphql_document, ( *execution_context.validation_rules, OneOfInputValidationRule, ), ) execution_context.errors = errors yield None strawberry-graphql-django-0.62.0/strawberry_django/fields/000077500000000000000000000000001502405145400236735ustar00rootroot00000000000000strawberry-graphql-django-0.62.0/strawberry_django/fields/__init__.py000066400000000000000000000000001502405145400257720ustar00rootroot00000000000000strawberry-graphql-django-0.62.0/strawberry_django/fields/base.py000066400000000000000000000212511502405145400251600ustar00rootroot00000000000000from __future__ import annotations import functools from typing import TYPE_CHECKING, Any, Optional, TypeVar, cast import django from django.db.models import ForeignKey from strawberry import relay from strawberry.annotation import StrawberryAnnotation from strawberry.types import get_object_definition from strawberry.types.auto import StrawberryAuto from strawberry.types.base import ( StrawberryContainer, StrawberryList, StrawberryOptional, StrawberryType, WithStrawberryObjectDefinition, ) from strawberry.types.field import UNRESOLVED, StrawberryField from strawberry.types.union import StrawberryUnion from strawberry.utils.inspect import get_specialized_type_var_map from strawberry_django.descriptors import ModelProperty from strawberry_django.resolvers import django_resolver from strawberry_django.utils.typing import ( WithStrawberryDjangoObjectDefinition, get_django_definition, has_django_definition, unwrap_type, ) if TYPE_CHECKING: from django.db import models from strawberry.types import Info from strawberry.types.object_type import StrawberryObjectDefinition from typing_extensions import Literal, Self from strawberry_django.type import StrawberryDjangoDefinition _QS = TypeVar("_QS", bound="models.QuerySet") if django.VERSION >= (5, 0): from django.db.models import GeneratedField # type: ignore else: GeneratedField = None class StrawberryDjangoFieldBase(StrawberryField): def __init__( self, django_name: str | None = None, graphql_name: str | None = None, python_name: str | None = None, **kwargs, ): self.is_relation = False self.django_name = django_name self.origin_django_type: StrawberryDjangoDefinition[Any, Any] | None = None super().__init__(graphql_name=graphql_name, python_name=python_name, **kwargs) def __copy__(self) -> Self: new_field = super().__copy__() new_field.django_name = self.django_name new_field.is_relation = self.is_relation new_field.origin_django_type = self.origin_django_type return new_field @property def is_basic_field(self) -> bool: """Mark this field as not basic. All StrawberryDjango fields define a custom resolver that needs to be run, so always return False here. """ return False @functools.cached_property def is_async(self) -> bool: # Our default resolver is sync by default but will return a coroutine # when running ASGI. If we happen to have an extension that only supports # async, make sure we mark the field as async as well to support resolving # it properly. return super().is_async or any( e.supports_async and not e.supports_sync for e in self.extensions ) @functools.cached_property def django_type(self) -> type[WithStrawberryDjangoObjectDefinition] | None: from strawberry_django.pagination import OffsetPaginated origin = unwrap_type(self.type) object_definition = get_object_definition(origin) if object_definition and issubclass( object_definition.origin, (relay.Connection, OffsetPaginated) ): origin_specialized_type_var_map = ( get_specialized_type_var_map(cast("type", origin)) or {} ) origin = origin_specialized_type_var_map.get("NodeType") if origin is None: origin = object_definition.type_var_map.get("NodeType") if origin is None: specialized_type_var_map = ( object_definition.specialized_type_var_map or {} ) origin = specialized_type_var_map["NodeType"] origin = unwrap_type(origin) if isinstance(origin, StrawberryUnion): origin_list: list[type[WithStrawberryDjangoObjectDefinition]] = [] for t in origin.types: while isinstance(t, StrawberryContainer): t = t.of_type # noqa: PLW2901 if has_django_definition(t): origin_list.append(t) origin = origin_list[0] if len(origin_list) == 1 else None return origin if has_django_definition(origin) else None @functools.cached_property def django_model(self) -> type[models.Model] | None: django_type = self.django_type return ( django_type.__strawberry_django_definition__.model if django_type is not None else None ) @functools.cached_property def is_model_property(self) -> bool: django_definition = self.origin_django_type return django_definition is not None and isinstance( getattr(django_definition.model, self.python_name, None), ModelProperty ) @functools.cached_property def is_optional(self) -> bool: return isinstance(self.type, StrawberryOptional) @functools.cached_property def is_list(self) -> bool: type_ = self.type if isinstance(type_, StrawberryOptional): type_ = type_.of_type return isinstance(type_, StrawberryList) @functools.cached_property def is_paginated(self) -> bool: from strawberry_django.pagination import OffsetPaginated type_ = self.type if isinstance(type_, StrawberryOptional): type_ = type_.of_type return isinstance(type_, type) and issubclass(type_, OffsetPaginated) @functools.cached_property def is_connection(self) -> bool: type_ = self.type if isinstance(type_, StrawberryOptional): type_ = type_.of_type return isinstance(type_, type) and issubclass(type_, relay.Connection) @functools.cached_property def safe_resolver(self): resolver = self.base_resolver assert resolver if not resolver.is_async: resolver = django_resolver(resolver, qs_hook=None) return resolver def resolve_type( self, *, type_definition: StrawberryObjectDefinition | None = None, ) -> ( StrawberryType | type[WithStrawberryObjectDefinition] | Literal[UNRESOLVED] # type: ignore ): resolved = super().resolve_type(type_definition=type_definition) if resolved is UNRESOLVED: return resolved try: resolved_django_type = get_django_definition(unwrap_type(resolved)) except KeyError: return UNRESOLVED if self.origin_django_type and ( # FIXME: Why does this come as Any sometimes when using future annotations? resolved is Any or isinstance(resolved, StrawberryAuto) # If the resolved type is an input but the origin is not, or vice versa, # resolve this again or ( resolved_django_type and resolved_django_type.is_input != self.origin_django_type.is_input ) ): from .types import get_model_field, is_optional, resolve_model_field_type model_field = get_model_field( self.origin_django_type.model, self.django_name or self.python_name or self.name, ) resolved_type = resolve_model_field_type( ( model_field.target_field if ( self.python_name.endswith("_id") and isinstance(model_field, ForeignKey) ) else model_field ), self.origin_django_type, ) is_generated_field = GeneratedField is not None and isinstance( model_field, GeneratedField ) field_to_check = ( model_field.output_field if is_generated_field else model_field # type: ignore ) if is_optional( field_to_check, self.origin_django_type.is_input, self.origin_django_type.is_partial, ): resolved_type = Optional[resolved_type] self.type_annotation = StrawberryAnnotation(resolved_type) resolved = super().type if isinstance(resolved, StrawberryAuto): resolved = UNRESOLVED return resolved def resolver( self, source: Any, info: Info | None, args: list[Any], kwargs: dict[str, Any], ) -> Any: return self.safe_resolver(*args, **kwargs) def get_result(self, source, info, args, kwargs): return self.resolver(info, source, args, kwargs) def get_queryset(self, queryset: _QS, info: Info, **kwargs) -> _QS: return queryset strawberry-graphql-django-0.62.0/strawberry_django/fields/field.py000066400000000000000000001205051502405145400253330ustar00rootroot00000000000000from __future__ import annotations import dataclasses import inspect import warnings from collections.abc import ( AsyncIterable, AsyncIterator, Callable, Iterable, Iterator, Mapping, Sequence, ) from functools import cached_property from typing import ( TYPE_CHECKING, Any, TypeVar, Union, cast, overload, ) from asgiref.sync import sync_to_async from django.core.exceptions import ObjectDoesNotExist from django.db import models from django.db.models.fields.files import FileDescriptor from django.db.models.fields.related import ( ForwardManyToOneDescriptor, ReverseManyToOneDescriptor, ReverseOneToOneDescriptor, ) from django.db.models.manager import BaseManager from django.db.models.query import MAX_GET_RESULTS # type: ignore from django.db.models.query_utils import DeferredAttribute from strawberry import UNSET, relay from strawberry.annotation import StrawberryAnnotation from strawberry.extensions.field_extension import FieldExtension from strawberry.types.field import _RESOLVER_TYPE # noqa: PLC2701 from strawberry.types.fields.resolver import StrawberryResolver from strawberry.types.info import Info from strawberry.utils.await_maybe import await_maybe from typing_extensions import TypeAlias from strawberry_django import optimizer from strawberry_django.arguments import argument from strawberry_django.descriptors import ModelProperty from strawberry_django.fields.base import StrawberryDjangoFieldBase from strawberry_django.filters import FILTERS_ARG, StrawberryDjangoFieldFilters from strawberry_django.optimizer import OptimizerStore, is_optimized_by_prefetching from strawberry_django.ordering import ( ORDER_ARG, ORDERING_ARG, StrawberryDjangoFieldOrdering, ) from strawberry_django.pagination import ( PAGINATION_ARG, OffsetPaginated, OffsetPaginationInput, StrawberryDjangoPagination, ) from strawberry_django.permissions import filter_with_perms from strawberry_django.queryset import run_type_get_queryset from strawberry_django.relay import resolve_model_nodes from strawberry_django.resolvers import ( default_qs_hook, django_getattr, django_resolver, resolve_base_manager, ) if TYPE_CHECKING: from graphql.pyutils import AwaitableOrValue from strawberry import BasePermission from strawberry.extensions.field_extension import SyncExtensionResolver from strawberry.relay.types import NodeIterableType from strawberry.types.arguments import StrawberryArgument from strawberry.types.base import WithStrawberryObjectDefinition from strawberry.types.field import StrawberryField from strawberry.types.unset import UnsetType from typing_extensions import Literal, Self from strawberry_django.utils.typing import ( AnnotateType, PrefetchType, TypeOrMapping, TypeOrSequence, ) _T = TypeVar("_T") class StrawberryDjangoField( StrawberryDjangoPagination, StrawberryDjangoFieldOrdering, StrawberryDjangoFieldFilters, StrawberryDjangoFieldBase, ): """Basic django field. StrawberryDjangoField inherits all features from StrawberryField and implements Django specific functionalities like ordering, filtering and pagination. This field takes care of that Django ORM is always accessed from sync context. Resolver function is wrapped in sync_to_async decorator in async context. See more information about that from Django documentation. https://docs.djangoproject.com/en/3.2/topics/async/ StrawberryDjangoField has following properties * django_name - django name which is used to access the field of model instance * is_relation - True if field is resolving django model relationship * origin_django_type - pointer to the origin of this field kwargs argument is passed to ordering, filtering, pagination and StrawberryField super classes. """ def __init__( self, *args, only: TypeOrSequence[str] | None = None, select_related: TypeOrSequence[str] | None = None, prefetch_related: TypeOrSequence[PrefetchType] | None = None, annotate: TypeOrMapping[AnnotateType] | None = None, disable_optimization: bool = False, **kwargs, ): self.disable_optimization = disable_optimization self.store = OptimizerStore.with_hints( only=only, select_related=select_related, prefetch_related=prefetch_related, annotate=annotate, ) # FIXME: Probably remove this when depending on graphql-core 3.3.0+ self.disable_fetch_list_results: bool = False super().__init__(*args, **kwargs) def __copy__(self) -> Self: new_field = super().__copy__() new_field.disable_optimization = self.disable_optimization new_field.store = self.store.copy() return new_field @cached_property def _need_remove_filters_argument(self): if not self.base_resolver or not self.is_connection: return False return not any( p.name == FILTERS_ARG or p.kind == p.VAR_KEYWORD for p in self.base_resolver.signature.parameters.values() ) @cached_property def _need_remove_order_argument(self): if not self.base_resolver or not self.is_connection: return False return not any( p.name == ORDER_ARG or p.kind == p.VAR_KEYWORD for p in self.base_resolver.signature.parameters.values() ) @cached_property def _need_remove_ordering_argument(self): if not self.base_resolver or not self.is_connection: return False return not any( p.name == ORDERING_ARG or p.kind == p.VAR_KEYWORD for p in self.base_resolver.signature.parameters.values() ) def get_result( self, source: models.Model | None, info: Info | None, args: list[Any], kwargs: dict[str, Any], ) -> AwaitableOrValue[Any]: is_awaitable = False if self.base_resolver is not None: resolver_kwargs = kwargs.copy() if self._need_remove_order_argument: resolver_kwargs.pop(ORDER_ARG, None) if self._need_remove_ordering_argument: resolver_kwargs.pop(ORDERING_ARG, None) if self._need_remove_filters_argument: resolver_kwargs.pop(FILTERS_ARG, None) assert info result = self.resolver(source, info, args, resolver_kwargs) is_awaitable = inspect.isawaitable(result) elif source is None: model = self.django_model assert model is not None result = model._default_manager.all() else: # Small optimization to async resolvers avoid having to call it in an # sync_to_async context if the value is already cached, since it will not # hit the db anymore attname = self.django_name or self.python_name attr = getattr(source.__class__, attname, None) try: if isinstance(attr, ModelProperty): result = source.__dict__[attr.name] elif isinstance(attr, DeferredAttribute): # If the value is cached, retrieve it with getattr because # some fields wrap values at that time (e.g. FileField). # If this next like fails, it will raise KeyError and get # us out of the loop before we can do getattr source.__dict__[attr.field.attname] result = getattr(source, attr.field.attname) elif isinstance(attr, ForwardManyToOneDescriptor): # This will raise KeyError if it is not cached result = attr.field.get_cached_value(source) # type: ignore elif isinstance(attr, ReverseOneToOneDescriptor): # This will raise KeyError if it is not cached result = attr.related.get_cached_value(source) elif isinstance(attr, ReverseManyToOneDescriptor): # This returns a queryset, it is async safe result = getattr(source, attname) else: raise KeyError # noqa: TRY301 except KeyError: if "info" not in kwargs: kwargs["info"] = info return django_getattr( source, attname, qs_hook=self.get_queryset_hook(**kwargs), # Reversed OneToOne will raise ObjectDoesNotExist when # trying to access it if the relation doesn't exist. except_as_none=(ObjectDoesNotExist,) if self.is_optional else None, empty_file_descriptor_as_null=True, ) else: # FileField/ImageField will always return a FileDescriptor, even when the # field is "null". If it is falsy (i.e. doesn't have a file) we should # return `None` instead. if isinstance(attr, FileDescriptor) and not result: result = None if is_awaitable or self.is_async: async def async_resolver(): resolved = await await_maybe(result) if isinstance(resolved, BaseManager): resolved = resolve_base_manager(resolved) if isinstance(resolved, models.QuerySet): if "info" not in kwargs: kwargs["info"] = info resolved = await sync_to_async(self.get_queryset_hook(**kwargs))( resolved ) return resolved return async_resolver() if isinstance(result, BaseManager): result = resolve_base_manager(result) if isinstance(result, models.QuerySet): if "info" not in kwargs: kwargs["info"] = info result = django_resolver( self.get_queryset_hook(**kwargs), qs_hook=lambda qs: qs, )(result) return result def get_queryset_hook(self, info: Info, **kwargs): if self.is_connection or self.is_paginated: # We don't want to fetch results yet, those will be done by the connection/pagination def qs_hook(qs: models.QuerySet): # type: ignore return self.get_queryset(qs, info, **kwargs) elif self.is_list: def qs_hook(qs: models.QuerySet): # type: ignore qs = self.get_queryset(qs, info, **kwargs) if not self.disable_fetch_list_results: qs = default_qs_hook(qs) return qs elif self.is_optional: def qs_hook(qs: models.QuerySet): # type: ignore qs = self.get_queryset(qs, info, **kwargs) return qs.first() else: def qs_hook(qs: models.QuerySet): qs = self.get_queryset(qs, info, **kwargs) # Don't use qs.get() if the queryset is optimized by prefetching. # Calling get in that case would disregard the prefetched results, because get implicitly # adds a limit to the query if (result_cache := qs._result_cache) is not None: # type: ignore # mimic behavior of get() # the queryset is already prefetched, no issue with just using len() qs_len = len(result_cache) if qs_len == 0: raise qs.model.DoesNotExist( f"{qs.model._meta.object_name} matching query does not exist." ) if qs_len != 1: raise qs.model.MultipleObjectsReturned( f"get() returned more than one {qs.model._meta.object_name} -- it returned " f"{qs_len if qs_len < MAX_GET_RESULTS else f'more than {qs_len - 1}'}!" ) return result_cache[0] return qs.get() return qs_hook def get_queryset(self, queryset, info, **kwargs): # If the queryset been optimized at prefetch phase, this function has already been # called by the optimizer extension, meaning we don't want to call it again if is_optimized_by_prefetching(queryset): return queryset queryset = run_type_get_queryset(queryset, self.django_type, info) queryset = super().get_queryset( filter_with_perms(queryset, info), info, **kwargs ) # If optimizer extension is enabled, optimize this queryset if ( not self.disable_optimization and (ext := optimizer.optimizer.get()) is not None ): queryset = ext.optimize(queryset, info=info) return queryset def _get_field_arguments_for_extensions( field: StrawberryDjangoField, *, add_filters: bool = True, add_order: bool = True, add_pagination: bool = True, ) -> list[StrawberryArgument]: """Get a list of arguments to be set to fields using extensions. Because we have a base_resolver defined in those, our parents will not add order/filters/pagination resolvers in here, so we need to add them by hand (unless they are somewhat in there). We are not adding pagination because it doesn't make sense together with a Connection """ args: dict[str, StrawberryArgument] = {a.python_name: a for a in field.arguments} if add_filters and FILTERS_ARG not in args: filters = field.get_filters() if filters not in (None, UNSET): # noqa: PLR6201 args[FILTERS_ARG] = argument(FILTERS_ARG, filters, is_optional=True) if add_order and ORDER_ARG not in args: order = field.get_order() if order not in (None, UNSET): # noqa: PLR6201 args[ORDER_ARG] = argument(ORDER_ARG, order, is_optional=True) if add_order and ORDERING_ARG not in args: ordering = field.get_ordering() if ordering not in (None, UNSET): # noqa: PLR6201 args[ORDERING_ARG] = argument( ORDERING_ARG, ordering, is_list=True, default=[] ) if add_pagination and PAGINATION_ARG not in args: pagination = field.get_pagination() if pagination not in (None, UNSET): # noqa: PLR6201 args[PAGINATION_ARG] = argument( PAGINATION_ARG, pagination, is_optional=True, ) return list(args.values()) class StrawberryDjangoConnectionExtension(relay.ConnectionExtension): def apply(self, field: StrawberryField) -> None: if not isinstance(field, StrawberryDjangoField): raise TypeError( "The extension can only be applied to StrawberryDjangoField" ) field.arguments = _get_field_arguments_for_extensions( field, add_pagination=False, ) if field.base_resolver is None: def default_resolver( root: models.Model | None, info: Info, **kwargs: Any, ) -> Iterable[Any]: assert isinstance(field, StrawberryDjangoField) django_type = field.django_type if root is not None: # If this is a nested field, call get_result instead because we want # to retrieve the queryset from its RelatedManager retval = cast( "models.QuerySet", getattr(root, field.django_name or field.python_name).all(), ) else: if django_type is None: raise TypeError( "Django connection without a resolver needs to define a" " connection for one and only one django type. To use" " it in a union, define your own resolver that handles" " each of those", ) retval = resolve_model_nodes( django_type, info=info, required=True, ) return cast("Iterable[Any]", retval) default_resolver.can_optimize = True # type: ignore field.base_resolver = StrawberryResolver(default_resolver) return super().apply(field) def resolve( self, next_: SyncExtensionResolver, source: Any, info: Info, *, before: str | None = None, after: str | None = None, first: int | None = None, last: int | None = None, **kwargs: Any, ) -> Any: assert self.connection_type is not None nodes = cast("Iterable[relay.Node]", next_(source, info, **kwargs)) # We have a single resolver for both sync and async, so we need to check if # nodes is awaitable or not and resolve it accordingly if inspect.isawaitable(nodes): async def async_resolver(): resolved = self.connection_type.resolve_connection( await nodes, info=info, before=before, after=after, first=first, last=last, max_results=self.max_results, ) if inspect.isawaitable(resolved): resolved = await resolved return resolved return async_resolver() return self.connection_type.resolve_connection( nodes, info=info, before=before, after=after, first=first, last=last, max_results=self.max_results, ) class StrawberryOffsetPaginatedExtension(FieldExtension): paginated_type: type[OffsetPaginated] def apply(self, field: StrawberryField) -> None: if not isinstance(field, StrawberryDjangoField): raise TypeError( "The extension can only be applied to StrawberryDjangoField" ) field.arguments = _get_field_arguments_for_extensions(field) self.paginated_type = cast("type[OffsetPaginated]", field.type) def resolve( self, next_: SyncExtensionResolver, source: Any, info: Info, *, pagination: OffsetPaginationInput | None = None, order: WithStrawberryObjectDefinition | None = None, filters: WithStrawberryObjectDefinition | None = None, **kwargs: Any, ) -> Any: assert self.paginated_type is not None queryset = cast("models.QuerySet", next_(source, info, **kwargs)) def get_queryset(queryset): return cast("StrawberryDjangoField", info._field).get_queryset( queryset, info, pagination=pagination, order=order, filters=filters, ) # We have a single resolver for both sync and async, so we need to check if # nodes is awaitable or not and resolve it accordingly if inspect.isawaitable(queryset): async def async_resolver(queryset=queryset): resolved = self.paginated_type.resolve_paginated( get_queryset(await queryset), info=info, pagination=pagination, **kwargs, ) if inspect.isawaitable(resolved): resolved = await resolved return resolved return async_resolver() return self.paginated_type.resolve_paginated( get_queryset(queryset), info=info, pagination=pagination, **kwargs, ) @overload def field( *, field_cls: type[StrawberryDjangoField] = StrawberryDjangoField, resolver: _RESOLVER_TYPE[_T], name: str | None = None, field_name: str | None = None, is_subscription: bool = False, description: str | None = None, init: Literal[False] = False, permission_classes: list[type[BasePermission]] | None = None, deprecation_reason: str | None = None, default: Any = dataclasses.MISSING, default_factory: Callable[..., object] | object = dataclasses.MISSING, metadata: Mapping[Any, Any] | None = None, directives: Sequence[object] | None = (), graphql_type: Any | None = None, extensions: Sequence[FieldExtension] = (), pagination: bool | UnsetType = UNSET, filters: type | UnsetType | None = UNSET, order: type | UnsetType | None = UNSET, ordering: type | UnsetType | None = UNSET, only: TypeOrSequence[str] | None = None, select_related: TypeOrSequence[str] | None = None, prefetch_related: TypeOrSequence[PrefetchType] | None = None, annotate: TypeOrMapping[AnnotateType] | None = None, disable_optimization: bool = False, ) -> _T: ... @overload def field( *, field_cls: type[StrawberryDjangoField] = StrawberryDjangoField, name: str | None = None, field_name: str | None = None, is_subscription: bool = False, description: str | None = None, init: Literal[True] = True, permission_classes: list[type[BasePermission]] | None = None, deprecation_reason: str | None = None, default: Any = dataclasses.MISSING, default_factory: Callable[..., object] | object = dataclasses.MISSING, metadata: Mapping[Any, Any] | None = None, directives: Sequence[object] | None = (), graphql_type: Any | None = None, extensions: Sequence[FieldExtension] = (), pagination: bool | UnsetType = UNSET, filters: type | UnsetType | None = UNSET, order: type | UnsetType | None = UNSET, ordering: type | UnsetType | None = UNSET, only: TypeOrSequence[str] | None = None, select_related: TypeOrSequence[str] | None = None, prefetch_related: TypeOrSequence[PrefetchType] | None = None, annotate: TypeOrMapping[AnnotateType] | None = None, disable_optimization: bool = False, ) -> Any: ... @overload def field( resolver: _RESOLVER_TYPE[Any], *, field_cls: type[StrawberryDjangoField] = StrawberryDjangoField, name: str | None = None, field_name: str | None = None, is_subscription: bool = False, description: str | None = None, permission_classes: list[type[BasePermission]] | None = None, deprecation_reason: str | None = None, default: Any = dataclasses.MISSING, default_factory: Callable[..., object] | object = dataclasses.MISSING, metadata: Mapping[Any, Any] | None = None, directives: Sequence[object] | None = (), graphql_type: Any | None = None, extensions: Sequence[FieldExtension] = (), pagination: bool | UnsetType = UNSET, filters: type | UnsetType | None = UNSET, order: type | UnsetType | None = UNSET, ordering: type | UnsetType | None = UNSET, only: TypeOrSequence[str] | None = None, select_related: TypeOrSequence[str] | None = None, prefetch_related: TypeOrSequence[PrefetchType] | None = None, annotate: TypeOrMapping[AnnotateType] | None = None, disable_optimization: bool = False, ) -> StrawberryDjangoField: ... def field( resolver: _RESOLVER_TYPE[Any] | None = None, *, field_cls: type[StrawberryDjangoField] = StrawberryDjangoField, name: str | None = None, field_name: str | None = None, is_subscription: bool = False, description: str | None = None, permission_classes: list[type[BasePermission]] | None = None, deprecation_reason: str | None = None, default: Any = dataclasses.MISSING, default_factory: Callable[..., object] | object = dataclasses.MISSING, metadata: Mapping[Any, Any] | None = None, directives: Sequence[object] | None = (), graphql_type: Any | None = None, extensions: Sequence[FieldExtension] = (), pagination: bool | UnsetType = UNSET, filters: type | UnsetType | None = UNSET, order: type | UnsetType | None = UNSET, ordering: type | UnsetType | None = UNSET, only: TypeOrSequence[str] | None = None, select_related: TypeOrSequence[str] | None = None, prefetch_related: TypeOrSequence[PrefetchType] | None = None, annotate: TypeOrMapping[AnnotateType] | None = None, disable_optimization: bool = False, # This init parameter is used by pyright to determine whether this field # is added in the constructor or not. It is not used to change # any behavior at the moment. init: bool | None = None, ) -> Any: """Annotate a method or property as a Django GraphQL field. Examples -------- It can be used both as decorator and as a normal function: >>> @strawberry.django.type >>> class X: ... field_abc: str = strawberry.django.field(description="ABC") ... @strawberry.django.field(description="ABC") ... ... def field_with_resolver(self) -> str: ... return "abc" """ f = field_cls( python_name=None, django_name=field_name, graphql_name=name, type_annotation=StrawberryAnnotation.from_annotation(graphql_type), description=description, is_subscription=is_subscription, permission_classes=permission_classes or [], deprecation_reason=deprecation_reason, default=default, default_factory=default_factory, metadata=metadata, directives=directives, filters=filters, pagination=pagination, order=order, ordering=ordering, extensions=extensions, only=only, select_related=select_related, prefetch_related=prefetch_related, annotate=annotate, disable_optimization=disable_optimization, ) if order: warnings.warn( "strawberry_django.order is deprecated in favor of strawberry_django.ordering.", DeprecationWarning, stacklevel=2, ) if resolver: return f(resolver) return f def node( *, field_cls: type[StrawberryDjangoField] = StrawberryDjangoField, name: str | None = None, field_name: str | None = None, is_subscription: bool = False, description: str | None = None, permission_classes: list[type[BasePermission]] | None = None, deprecation_reason: str | None = None, default: Any = dataclasses.MISSING, default_factory: Callable[..., object] | object = dataclasses.MISSING, metadata: Mapping[Any, Any] | None = None, directives: Sequence[object] | None = (), graphql_type: Any | None = None, extensions: Sequence[FieldExtension] = (), only: TypeOrSequence[str] | None = None, select_related: TypeOrSequence[str] | None = None, prefetch_related: TypeOrSequence[PrefetchType] | None = None, annotate: TypeOrMapping[AnnotateType] | None = None, disable_optimization: bool = False, # This init parameter is used by pyright to determine whether this field # is added in the constructor or not. It is not used to change # any behavior at the moment. init: bool | None = None, ) -> Any: """Annotate a property to create a relay query field. Examples -------- Annotating something like this: >>> @strawberry.type >>> class X: ... some_node: SomeType = relay.node(description="ABC") Will produce a query like this that returns `SomeType` given its id. ``` query { someNode (id: ID) { id ... } } ``` """ extensions = [*extensions, relay.NodeExtension()] return field_cls( python_name=None, django_name=field_name, graphql_name=name, type_annotation=StrawberryAnnotation.from_annotation(graphql_type), description=description, is_subscription=is_subscription, permission_classes=permission_classes or [], deprecation_reason=deprecation_reason, default=default, default_factory=default_factory, metadata=metadata, directives=directives or (), extensions=extensions, ) @overload def connection( graphql_type: type[relay.Connection[relay.NodeType]] | None = None, *, field_cls: type[StrawberryDjangoField] = StrawberryDjangoField, name: str | None = None, field_name: str | None = None, is_subscription: bool = False, description: str | None = None, permission_classes: list[type[BasePermission]] | None = None, deprecation_reason: str | None = None, default: Any = dataclasses.MISSING, default_factory: Callable[..., object] | object = dataclasses.MISSING, metadata: Mapping[Any, Any] | None = None, directives: Sequence[object] | None = (), extensions: Sequence[FieldExtension] = (), max_results: int | None = None, filters: type | None = UNSET, order: type | None = UNSET, ordering: type | None = UNSET, only: TypeOrSequence[str] | None = None, select_related: TypeOrSequence[str] | None = None, prefetch_related: TypeOrSequence[PrefetchType] | None = None, annotate: TypeOrMapping[AnnotateType] | None = None, disable_optimization: bool = False, ) -> Any: ... @overload def connection( graphql_type: type[relay.Connection[relay.NodeType]] | None = None, *, field_cls: type[StrawberryDjangoField] = StrawberryDjangoField, resolver: _RESOLVER_TYPE[NodeIterableType[Any]] | None = None, name: str | None = None, field_name: str | None = None, is_subscription: bool = False, description: str | None = None, init: Literal[True] = True, permission_classes: list[type[BasePermission]] | None = None, deprecation_reason: str | None = None, default: Any = dataclasses.MISSING, default_factory: Callable[..., object] | object = dataclasses.MISSING, metadata: Mapping[Any, Any] | None = None, directives: Sequence[object] | None = (), extensions: Sequence[FieldExtension] = (), max_results: int | None = None, filters: type | None = UNSET, order: type | None = UNSET, ordering: type | None = UNSET, only: TypeOrSequence[str] | None = None, select_related: TypeOrSequence[str] | None = None, prefetch_related: TypeOrSequence[PrefetchType] | None = None, annotate: TypeOrMapping[AnnotateType] | None = None, disable_optimization: bool = False, ) -> Any: ... def connection( graphql_type: type[relay.Connection[relay.NodeType]] | None = None, *, field_cls: type[StrawberryDjangoField] = StrawberryDjangoField, resolver: _RESOLVER_TYPE[NodeIterableType[Any]] | None = None, name: str | None = None, field_name: str | None = None, is_subscription: bool = False, description: str | None = None, permission_classes: list[type[BasePermission]] | None = None, deprecation_reason: str | None = None, default: Any = dataclasses.MISSING, default_factory: Callable[..., object] | object = dataclasses.MISSING, metadata: Mapping[Any, Any] | None = None, directives: Sequence[object] | None = (), extensions: Sequence[FieldExtension] = (), max_results: int | None = None, filters: type | None = UNSET, order: type | None = UNSET, ordering: type | None = UNSET, only: TypeOrSequence[str] | None = None, select_related: TypeOrSequence[str] | None = None, prefetch_related: TypeOrSequence[PrefetchType] | None = None, annotate: TypeOrMapping[AnnotateType] | None = None, disable_optimization: bool = False, # This init parameter is used by pyright to determine whether this field # is added in the constructor or not. It is not used to change # any behavior at the moment. init: bool | None = None, ) -> Any: """Annotate a property or a method to create a relay connection field. Relay connections_ are mostly used for pagination purposes. This decorator helps creating a complete relay endpoint that provides default arguments and has a default implementation for the connection slicing. Note that when setting a resolver to this field, it is expected for this resolver to return an iterable of the expected node type, not the connection itself. That iterable will then be paginated accordingly. So, the main use case for this is to provide a filtered iterable of nodes by using some custom filter arguments. Examples -------- Annotating something like this: >>> @strawberry.type >>> class X: ... some_node: relay.Connection[SomeType] = relay.connection( ... description="ABC", ... ) ... ... @relay.connection(description="ABC") ... def get_some_nodes(self, age: int) -> Iterable[SomeType]: ... ... Will produce a query like this: ``` query { someNode ( before: String after: String first: String after: String age: Int ) { totalCount pageInfo { hasNextPage hasPreviousPage startCursor endCursor } edges { cursor node { id ... } } } } ``` .. _Relay connections: https://relay.dev/graphql/connections.htm """ extensions = [ *extensions, StrawberryDjangoConnectionExtension(max_results=max_results), ] f = field_cls( python_name=None, django_name=field_name, graphql_name=name, type_annotation=StrawberryAnnotation.from_annotation(graphql_type), description=description, is_subscription=is_subscription, permission_classes=permission_classes or [], deprecation_reason=deprecation_reason, default=default, default_factory=default_factory, metadata=metadata, directives=directives or (), filters=filters, order=order, ordering=ordering, extensions=extensions, only=only, select_related=select_related, prefetch_related=prefetch_related, annotate=annotate, disable_optimization=disable_optimization, ) if resolver: f = f(resolver) return f _OFFSET_PAGINATED_RESOLVER_TYPE: TypeAlias = _RESOLVER_TYPE[ Union[ Iterator[models.Model], Iterable[models.Model], AsyncIterator[models.Model], AsyncIterable[models.Model], ] ] @overload def offset_paginated( graphql_type: type[OffsetPaginated] | None = None, *, field_cls: type[StrawberryDjangoField] = StrawberryDjangoField, name: str | None = None, field_name: str | None = None, is_subscription: bool = False, description: str | None = None, permission_classes: list[type[BasePermission]] | None = None, deprecation_reason: str | None = None, default: Any = dataclasses.MISSING, default_factory: Callable[..., object] | object = dataclasses.MISSING, metadata: Mapping[Any, Any] | None = None, directives: Sequence[object] | None = (), extensions: Sequence[FieldExtension] = (), filters: type | None = UNSET, order: type | None = UNSET, ordering: type | None = UNSET, only: TypeOrSequence[str] | None = None, select_related: TypeOrSequence[str] | None = None, prefetch_related: TypeOrSequence[PrefetchType] | None = None, annotate: TypeOrMapping[AnnotateType] | None = None, disable_optimization: bool = False, ) -> Any: ... @overload def offset_paginated( graphql_type: type[OffsetPaginated] | None = None, *, field_cls: type[StrawberryDjangoField] = StrawberryDjangoField, resolver: _OFFSET_PAGINATED_RESOLVER_TYPE | None = None, name: str | None = None, field_name: str | None = None, is_subscription: bool = False, description: str | None = None, init: Literal[True] = True, permission_classes: list[type[BasePermission]] | None = None, deprecation_reason: str | None = None, default: Any = dataclasses.MISSING, default_factory: Callable[..., object] | object = dataclasses.MISSING, metadata: Mapping[Any, Any] | None = None, directives: Sequence[object] | None = (), extensions: Sequence[FieldExtension] = (), filters: type | None = UNSET, order: type | None = UNSET, ordering: type | None = UNSET, only: TypeOrSequence[str] | None = None, select_related: TypeOrSequence[str] | None = None, prefetch_related: TypeOrSequence[PrefetchType] | None = None, annotate: TypeOrMapping[AnnotateType] | None = None, disable_optimization: bool = False, ) -> Any: ... def offset_paginated( graphql_type: type[OffsetPaginated] | None = None, *, field_cls: type[StrawberryDjangoField] = StrawberryDjangoField, resolver: _OFFSET_PAGINATED_RESOLVER_TYPE | None = None, name: str | None = None, field_name: str | None = None, is_subscription: bool = False, description: str | None = None, permission_classes: list[type[BasePermission]] | None = None, deprecation_reason: str | None = None, default: Any = dataclasses.MISSING, default_factory: Callable[..., object] | object = dataclasses.MISSING, metadata: Mapping[Any, Any] | None = None, directives: Sequence[object] | None = (), extensions: Sequence[FieldExtension] = (), filters: type | None = UNSET, order: type | None = UNSET, ordering: type | None = UNSET, only: TypeOrSequence[str] | None = None, select_related: TypeOrSequence[str] | None = None, prefetch_related: TypeOrSequence[PrefetchType] | None = None, annotate: TypeOrMapping[AnnotateType] | None = None, disable_optimization: bool = False, # This init parameter is used by pyright to determine whether this field # is added in the constructor or not. It is not used to change # any behavior at the moment. init: bool | None = None, ) -> Any: """Annotate a property or a method to create a relay connection field. Relay connections_ are mostly used for pagination purposes. This decorator helps creating a complete relay endpoint that provides default arguments and has a default implementation for the connection slicing. Note that when setting a resolver to this field, it is expected for this resolver to return an iterable of the expected node type, not the connection itself. That iterable will then be paginated accordingly. So, the main use case for this is to provide a filtered iterable of nodes by using some custom filter arguments. Examples -------- Annotating something like this: >>> @strawberry.type >>> class X: ... some_node: relay.Connection[SomeType] = relay.connection( ... description="ABC", ... ) ... ... @relay.connection(description="ABC") ... def get_some_nodes(self, age: int) -> Iterable[SomeType]: ... ... Will produce a query like this: ``` query { someNode ( before: String after: String first: String after: String age: Int ) { totalCount pageInfo { hasNextPage hasPreviousPage startCursor endCursor } edges { cursor node { id ... } } } } ``` .. _Relay connections: https://relay.dev/graphql/connections.htm """ extensions = [*extensions, StrawberryOffsetPaginatedExtension()] f = field_cls( python_name=None, django_name=field_name, graphql_name=name, type_annotation=StrawberryAnnotation.from_annotation(graphql_type), description=description, is_subscription=is_subscription, permission_classes=permission_classes or [], deprecation_reason=deprecation_reason, default=default, default_factory=default_factory, metadata=metadata, directives=directives or (), filters=filters, order=order, ordering=ordering, extensions=extensions, only=only, select_related=select_related, prefetch_related=prefetch_related, annotate=annotate, disable_optimization=disable_optimization, ) if resolver: f = f(resolver) return f strawberry-graphql-django-0.62.0/strawberry_django/fields/filter_order.py000066400000000000000000000307631502405145400267360ustar00rootroot00000000000000from __future__ import annotations import dataclasses import inspect from functools import cached_property from typing import TYPE_CHECKING, Any, Final, Literal, Optional, overload from strawberry import UNSET from strawberry.annotation import StrawberryAnnotation from strawberry.exceptions import MissingArgumentsAnnotationsError from strawberry.types.field import StrawberryField from strawberry.types.fields.resolver import ReservedName, StrawberryResolver from typing_extensions import Self from strawberry_django.exceptions import ( ForbiddenFieldArgumentError, MissingFieldArgumentError, ) from strawberry_django.utils.typing import is_auto if TYPE_CHECKING: from collections.abc import Callable, MutableMapping, Sequence from strawberry.extensions.field_extension import FieldExtension from strawberry.types import Info from strawberry.types.field import _RESOLVER_TYPE, T QUERYSET_PARAMSPEC = ReservedName("queryset") PREFIX_PARAMSPEC = ReservedName("prefix") SEQUENCE_PARAMSPEC = ReservedName("sequence") VALUE_PARAM = ReservedName("value") OBJECT_FILTER_NAME: Final[str] = "filter" OBJECT_ORDER_NAME: Final[str] = "order" WITH_NONE_META: Final[str] = "WITH_NONE_META" RESOLVE_VALUE_META: Final[str] = "RESOLVE_VALUE_META" class FilterOrderFieldResolver(StrawberryResolver): RESERVED_PARAMSPEC = ( *StrawberryResolver.RESERVED_PARAMSPEC, QUERYSET_PARAMSPEC, PREFIX_PARAMSPEC, SEQUENCE_PARAMSPEC, VALUE_PARAM, ) def __init__(self, *args, resolver_type: Literal["filter", "order"], **kwargs): super().__init__(*args, **kwargs) self._resolver_type = resolver_type def validate_filter_arguments(self): is_object_filter = self.name == OBJECT_FILTER_NAME is_object_order = self.name == OBJECT_ORDER_NAME if not self.reserved_parameters[PREFIX_PARAMSPEC]: raise MissingFieldArgumentError(PREFIX_PARAMSPEC.name, self) if (is_object_filter or is_object_order) and not self.reserved_parameters[ QUERYSET_PARAMSPEC ]: raise MissingFieldArgumentError(QUERYSET_PARAMSPEC.name, self) if ( self._resolver_type != OBJECT_ORDER_NAME and self.reserved_parameters[SEQUENCE_PARAMSPEC] ): raise ForbiddenFieldArgumentError(self, [SEQUENCE_PARAMSPEC.name]) value_param = self.reserved_parameters[VALUE_PARAM] if value_param: if is_object_filter or is_object_order: raise ForbiddenFieldArgumentError(self, [VALUE_PARAM.name]) annotation = self.strawberry_annotations[value_param] if annotation is None: raise MissingArgumentsAnnotationsError(self, [VALUE_PARAM.name]) elif not is_object_filter and not is_object_order: raise MissingFieldArgumentError(VALUE_PARAM.name, self) parameters = self.signature.parameters.values() reserved_parameters = set(self.reserved_parameters.values()) exta_params = [p for p in parameters if p not in reserved_parameters] if exta_params: raise ForbiddenFieldArgumentError(self, [p.name for p in exta_params]) @cached_property def type_annotation(self) -> StrawberryAnnotation | None: param = self.reserved_parameters[VALUE_PARAM] if param and param is not inspect.Signature.empty: annotation = param.annotation if is_auto(annotation) and self._resolver_type == OBJECT_ORDER_NAME: from strawberry_django import ordering annotation = ordering.Ordering return StrawberryAnnotation(Optional[annotation]) return None def __call__( # type: ignore self, source: Any, info: Info | None, queryset=None, sequence=None, **kwargs: Any, ) -> Any: args = [] if self.self_parameter: args.append(source) if parent_parameter := self.parent_parameter: kwargs[parent_parameter.name] = source if root_parameter := self.root_parameter: kwargs[root_parameter.name] = source if info_parameter := self.info_parameter: assert info is not None kwargs[info_parameter.name] = info if info_parameter := self.reserved_parameters.get(QUERYSET_PARAMSPEC): assert queryset is not None kwargs[info_parameter.name] = queryset if info_parameter := self.reserved_parameters.get(SEQUENCE_PARAMSPEC): assert sequence is not None kwargs[info_parameter.name] = sequence return super().__call__(*args, **kwargs) class FilterOrderField(StrawberryField): base_resolver: FilterOrderFieldResolver | None # type: ignore def __call__(self, resolver: _RESOLVER_TYPE) -> Self | FilterOrderFieldResolver: # type: ignore if not isinstance(resolver, StrawberryResolver): resolver = FilterOrderFieldResolver( resolver, resolver_type=self.metadata["_FIELD_TYPE"] ) elif not isinstance(resolver, FilterOrderFieldResolver): raise TypeError( 'Expected resolver to be instance of "FilterOrderFieldResolver", ' f'found "{type(resolver)}"' ) super().__call__(resolver) self._arguments = [] resolver.validate_filter_arguments() if resolver.name in {OBJECT_FILTER_NAME, OBJECT_ORDER_NAME}: # For object filter we return resolver return resolver self.init = self.compare = self.repr = True return self @overload def filter_field( *, resolver: _RESOLVER_TYPE[T], name: str | None = None, is_subscription: bool = False, description: str | None = None, init: Literal[False] = False, deprecation_reason: str | None = None, default: Any = UNSET, default_factory: Callable[..., object] | object = dataclasses.MISSING, metadata: MutableMapping[Any, Any] | None = None, directives: Sequence[object] = (), extensions: list[FieldExtension] | None = None, filter_none: bool = False, resolve_value: bool = UNSET, ) -> T: ... @overload def filter_field( *, name: str | None = None, is_subscription: bool = False, description: str | None = None, init: Literal[True] = True, deprecation_reason: str | None = None, default: Any = UNSET, default_factory: Callable[..., object] | object = dataclasses.MISSING, metadata: MutableMapping[Any, Any] | None = None, directives: Sequence[object] = (), extensions: list[FieldExtension] | None = None, filter_none: bool = False, resolve_value: bool = UNSET, ) -> Any: ... @overload def filter_field( resolver: _RESOLVER_TYPE[T], *, name: str | None = None, is_subscription: bool = False, description: str | None = None, deprecation_reason: str | None = None, default: Any = UNSET, default_factory: Callable[..., object] | object = dataclasses.MISSING, metadata: MutableMapping[Any, Any] | None = None, directives: Sequence[object] = (), extensions: list[FieldExtension] | None = None, filter_none: bool = False, resolve_value: bool = UNSET, ) -> StrawberryField: ... def filter_field( resolver: _RESOLVER_TYPE[Any] | None = None, *, name: str | None = None, is_subscription: bool = False, description: str | None = None, deprecation_reason: str | None = None, default: Any = UNSET, default_factory: Callable[..., object] | object = dataclasses.MISSING, metadata: MutableMapping[Any, Any] | None = None, directives: Sequence[object] = (), extensions: list[FieldExtension] | None = None, filter_none: bool = False, resolve_value: bool = UNSET, # This init parameter is used by pyright to determine whether this field # is added in the constructor or not. It is not used to change # any behavior at the moment. init: bool | None = None, ) -> Any: """Annotates a method or property as a Django filter field. If using with method, these parameters are required: queryset, value, prefix Additionaly value has to be annotated with type of filter This is normally used inside a type declaration: >>> @strawberry_django.filter_type(SomeModel) >>> class X: >>> field_abc: strawberry.auto = strawberry_django.filter_field() >>> @strawberry.filter_field(description="ABC") >>> def field_with_resolver(self, queryset, info, value: str, prefix): >>> return it can be used both as decorator and as a normal function. """ metadata = metadata or {} metadata["_FIELD_TYPE"] = OBJECT_FILTER_NAME metadata[RESOLVE_VALUE_META] = resolve_value metadata[WITH_NONE_META] = filter_none field_ = FilterOrderField( python_name=None, graphql_name=name, is_subscription=is_subscription, description=description, deprecation_reason=deprecation_reason, default=default, default_factory=default_factory, metadata=metadata, directives=directives, extensions=extensions or [], ) if resolver: return field_(resolver) return field_ @overload def order_field( *, resolver: _RESOLVER_TYPE[T], name: str | None = None, is_subscription: bool = False, description: str | None = None, init: Literal[False] = False, deprecation_reason: str | None = None, default: Any = UNSET, default_factory: Callable[..., object] | object = dataclasses.MISSING, metadata: MutableMapping[Any, Any] | None = None, directives: Sequence[object] = (), extensions: list[FieldExtension] | None = None, order_none: bool = False, ) -> T: ... @overload def order_field( *, name: str | None = None, is_subscription: bool = False, description: str | None = None, init: Literal[True] = True, deprecation_reason: str | None = None, default: Any = UNSET, default_factory: Callable[..., object] | object = dataclasses.MISSING, metadata: MutableMapping[Any, Any] | None = None, directives: Sequence[object] = (), extensions: list[FieldExtension] | None = None, order_none: bool = False, ) -> Any: ... @overload def order_field( resolver: _RESOLVER_TYPE[T], *, name: str | None = None, is_subscription: bool = False, description: str | None = None, deprecation_reason: str | None = None, default: Any = UNSET, default_factory: Callable[..., object] | object = dataclasses.MISSING, metadata: MutableMapping[Any, Any] | None = None, directives: Sequence[object] = (), extensions: list[FieldExtension] | None = None, order_none: bool = False, ) -> StrawberryField: ... def order_field( resolver: _RESOLVER_TYPE[Any] | None = None, *, name: str | None = None, is_subscription: bool = False, description: str | None = None, deprecation_reason: str | None = None, default: Any = UNSET, default_factory: Callable[..., object] | object = dataclasses.MISSING, metadata: MutableMapping[Any, Any] | None = None, directives: Sequence[object] = (), extensions: list[FieldExtension] | None = None, order_none: bool = False, # This init parameter is used by pyright to determine whether this field # is added in the constructor or not. It is not used to change # any behavior at the moment. init: bool | None = None, ) -> Any: """Annotates a method or property as a Django filter field. If using with method, these parameters are required: queryset, value, prefix Additionaly value has to be annotated with type of filter This is normally used inside a type declaration: >>> @strawberry_django.order(SomeModel) >>> class X: >>> field_abc: strawberry.auto = strawberry_django.order_field() >>> @strawberry.order_field(description="ABC") >>> def field_with_resolver(self, queryset, info, value: str, prefix): >>> return it can be used both as decorator and as a normal function. """ metadata = metadata or {} metadata["_FIELD_TYPE"] = OBJECT_ORDER_NAME metadata[WITH_NONE_META] = order_none field_ = FilterOrderField( python_name=None, graphql_name=name, is_subscription=is_subscription, description=description, deprecation_reason=deprecation_reason, default=default, default_factory=default_factory, metadata=metadata, directives=directives, extensions=extensions or [], ) if resolver: return field_(resolver) return field_ strawberry-graphql-django-0.62.0/strawberry_django/fields/filter_types.py000066400000000000000000000076301502405145400267640ustar00rootroot00000000000000import datetime import decimal import uuid from typing import ( Generic, Optional, TypeVar, ) import strawberry from django.db.models import Q from strawberry import UNSET from strawberry_django.filters import resolve_value from .filter_order import filter_field T = TypeVar("T") _SKIP_MSG = "Filter will be skipped on `null` value" @strawberry.input class BaseFilterLookup(Generic[T]): exact: Optional[T] = filter_field(description=f"Exact match. {_SKIP_MSG}") is_null: Optional[bool] = filter_field(description=f"Assignment test. {_SKIP_MSG}") in_list: Optional[list[T]] = filter_field( description=f"Exact match of items in a given list. {_SKIP_MSG}" ) @strawberry.input class RangeLookup(Generic[T]): start: Optional[T] = None end: Optional[T] = None @filter_field def filter(self, queryset, prefix: str): return queryset, Q(**{ prefix[:-2]: [resolve_value(self.start), resolve_value(self.end)] }) @strawberry.input class ComparisonFilterLookup(BaseFilterLookup[T]): gt: Optional[T] = filter_field(description=f"Greater than. {_SKIP_MSG}") gte: Optional[T] = filter_field( description=f"Greater than or equal to. {_SKIP_MSG}" ) lt: Optional[T] = filter_field(description=f"Less than. {_SKIP_MSG}") lte: Optional[T] = filter_field(description=f"Less than or equal to. {_SKIP_MSG}") range: Optional[RangeLookup[T]] = filter_field( description="Inclusive range test (between)" ) @strawberry.input class FilterLookup(BaseFilterLookup[T]): i_exact: Optional[T] = filter_field( description=f"Case-insensitive exact match. {_SKIP_MSG}" ) contains: Optional[T] = filter_field( description=f"Case-sensitive containment test. {_SKIP_MSG}" ) i_contains: Optional[T] = filter_field( description=f"Case-insensitive containment test. {_SKIP_MSG}" ) starts_with: Optional[T] = filter_field( description=f"Case-sensitive starts-with. {_SKIP_MSG}" ) i_starts_with: Optional[T] = filter_field( description=f"Case-insensitive starts-with. {_SKIP_MSG}" ) ends_with: Optional[T] = filter_field( description=f"Case-sensitive ends-with. {_SKIP_MSG}" ) i_ends_with: Optional[T] = filter_field( description=f"Case-insensitive ends-with. {_SKIP_MSG}" ) regex: Optional[T] = filter_field( description=f"Case-sensitive regular expression match. {_SKIP_MSG}" ) i_regex: Optional[T] = filter_field( description=f"Case-insensitive regular expression match. {_SKIP_MSG}" ) @strawberry.input class DateFilterLookup(ComparisonFilterLookup[T]): year: Optional[ComparisonFilterLookup[int]] = UNSET month: Optional[ComparisonFilterLookup[int]] = UNSET day: Optional[ComparisonFilterLookup[int]] = UNSET week_day: Optional[ComparisonFilterLookup[int]] = UNSET iso_week_day: Optional[ComparisonFilterLookup[int]] = UNSET week: Optional[ComparisonFilterLookup[int]] = UNSET iso_year: Optional[ComparisonFilterLookup[int]] = UNSET quarter: Optional[ComparisonFilterLookup[int]] = UNSET @strawberry.input class TimeFilterLookup(ComparisonFilterLookup[T]): hour: Optional[ComparisonFilterLookup[int]] = UNSET minute: Optional[ComparisonFilterLookup[int]] = UNSET second: Optional[ComparisonFilterLookup[int]] = UNSET date: Optional[ComparisonFilterLookup[int]] = UNSET time: Optional[ComparisonFilterLookup[int]] = UNSET @strawberry.input class DatetimeFilterLookup(DateFilterLookup[T], TimeFilterLookup[T]): pass type_filter_map = { strawberry.ID: BaseFilterLookup, bool: BaseFilterLookup, datetime.date: DateFilterLookup, datetime.datetime: DatetimeFilterLookup, datetime.time: TimeFilterLookup, decimal.Decimal: ComparisonFilterLookup, float: ComparisonFilterLookup, int: ComparisonFilterLookup, str: FilterLookup, uuid.UUID: FilterLookup, } strawberry-graphql-django-0.62.0/strawberry_django/fields/types.py000066400000000000000000000475261502405145400254270ustar00rootroot00000000000000import datetime import decimal import enum import inspect import re import uuid from types import FunctionType from typing import ( TYPE_CHECKING, Any, Generic, NewType, Optional, TypeVar, Union, cast, ) import django import strawberry from django.core.exceptions import FieldDoesNotExist, ImproperlyConfigured from django.db.models import Field, Model, fields from django.db.models.fields import files, json, related, reverse_related from strawberry import UNSET, relay from strawberry.file_uploads.scalars import Upload from strawberry.scalars import JSON from strawberry.types.enum import EnumValueDefinition from strawberry.utils.str_converters import capitalize_first, to_camel_case from strawberry_django import filters from strawberry_django.fields import filter_types from strawberry_django.settings import strawberry_django_settings as django_settings try: from django_choices_field import IntegerChoicesField, TextChoicesField except ImportError: # pragma: no cover IntegerChoicesField = None TextChoicesField = None try: from django.contrib.postgres.fields import ArrayField except (ImportError, ModuleNotFoundError): # pragma: no cover # ArrayField will not be importable if psycopg2 is not installed ArrayField = None if django.VERSION >= (5, 0): from django.db.models import GeneratedField # type: ignore else: GeneratedField = None if TYPE_CHECKING: from collections.abc import Iterable from strawberry_django.type import StrawberryDjangoDefinition K = TypeVar("K") @strawberry.type class DjangoFileType: name: str path: str size: int url: str @strawberry.type class DjangoImageType(DjangoFileType): width: int height: int @strawberry.type class DjangoModelType: pk: strawberry.ID @strawberry.input class OneToOneInput: set: Optional[strawberry.ID] @strawberry.input class OneToManyInput: set: Optional[strawberry.ID] @strawberry.input class ManyToOneInput: add: Optional[list[strawberry.ID]] = UNSET remove: Optional[list[strawberry.ID]] = UNSET set: Optional[list[strawberry.ID]] = UNSET @strawberry.input class ManyToManyInput: add: Optional[list[strawberry.ID]] = UNSET remove: Optional[list[strawberry.ID]] = UNSET set: Optional[list[strawberry.ID]] = UNSET @strawberry.input( description="Input of an object that implements the `Node` interface.", ) class NodeInput: id: relay.GlobalID def __eq__(self, other: object): if not isinstance(other, NodeInput): return NotImplemented return self.id == other.id def __hash__(self): return hash((self.__class__, self.id)) @strawberry.input( description="Input of an object that implements the `Node` interface.", ) class NodeInputPartial(NodeInput): # FIXME: Without this pyright will not let any class inherit from this and define # a field that doesn't contain a default value... if TYPE_CHECKING: id: Optional[relay.GlobalID] # type: ignore else: id: Optional[relay.GlobalID] = UNSET @strawberry.input(description="Add/remove/set the selected nodes.") class ListInput(Generic[K]): """Add/remove/set the selected nodes. Notes ----- To pass data to an intermediate model, type the input in a `throught_defaults` key inside the input object. """ # FIXME: Without this pyright will not let any class inheric from this and define # a field that doesn't contain a default value... if TYPE_CHECKING: set: Optional[list[K]] add: Optional[list[K]] remove: Optional[list[K]] else: set: Optional[list[K]] = UNSET add: Optional[list[K]] = UNSET remove: Optional[list[K]] = UNSET def __eq__(self, other: object): if not isinstance(other, ListInput): return NotImplemented return self._hash_fields() == other._hash_fields() def __hash__(self): return hash((self.__class__, *self._hash_fields())) def _hash_fields(self): return ( tuple(self.set) if isinstance(self.set, list) else self.set, tuple(self.add) if isinstance(self.add, list) else self.add, tuple(self.remove) if isinstance(self.remove, list) else self.remove, ) @strawberry.type class OperationMessage: """An error that happened while executing an operation.""" @strawberry.enum(name="OperationMessageKind") class Kind(enum.Enum): """The kind of the returned message.""" INFO = "info" WARNING = "warning" ERROR = "error" PERMISSION = "permission" VALIDATION = "validation" kind: Kind = strawberry.field(description="The kind of this message.") message: str = strawberry.field(description="The error message.") field: Optional[str] = strawberry.field( description=( "The field that caused the error, or `null` if it " "isn't associated with any particular field." ), default=None, ) code: Optional[str] = strawberry.field( description="The error code, or `null` if no error code was set.", default=None, ) def __eq__(self, other: object): if not isinstance(other, OperationMessage): return NotImplemented return ( self.kind == other.kind and self.message == other.message and self.field == other.field and self.code == other.code ) def __hash__(self): return hash((self.__class__, self.kind, self.message, self.field, self.code)) @strawberry.type class OperationInfo: """Multiple messages returned by an operation.""" messages: list[OperationMessage] = strawberry.field( description="List of messages returned by the operation.", ) def __eq__(self, other: object): if not isinstance(other, OperationInfo): return NotImplemented return self.messages == other.messages def __hash__(self): return hash((self.__class__, *tuple(self.messages))) field_type_map: dict[ Union[ type[fields.Field], type[related.RelatedField], type[reverse_related.ForeignObjectRel], ], Union[type, FunctionType], ] = { fields.AutoField: strawberry.ID, fields.BigAutoField: strawberry.ID, fields.BigIntegerField: int, fields.BooleanField: bool, fields.CharField: str, fields.DateField: datetime.date, fields.DateTimeField: datetime.datetime, fields.DecimalField: decimal.Decimal, fields.EmailField: str, fields.FilePathField: str, fields.FloatField: float, fields.GenericIPAddressField: str, fields.IntegerField: int, fields.PositiveIntegerField: int, fields.PositiveSmallIntegerField: int, fields.PositiveBigIntegerField: int, fields.SlugField: str, fields.SmallAutoField: strawberry.ID, fields.SmallIntegerField: int, fields.TextField: str, fields.TimeField: datetime.time, fields.URLField: str, fields.UUIDField: uuid.UUID, json.JSONField: JSON, files.FileField: DjangoFileType, files.ImageField: DjangoImageType, related.ForeignKey: DjangoModelType, related.ManyToManyField: list[DjangoModelType], related.OneToOneField: DjangoModelType, reverse_related.ManyToManyRel: list[DjangoModelType], reverse_related.ManyToOneRel: list[DjangoModelType], reverse_related.OneToOneRel: DjangoModelType, } try: from django.contrib.gis import geos from django.contrib.gis.db import models as geos_fields except ImproperlyConfigured: # If gdal is not available, skip. Point = None LineString = None LinearRing = None Polygon = None MultiPoint = None MultilineString = None MultiPolygon = None Geometry = None else: Point = strawberry.scalar( cast("type", NewType("Point", tuple[float, float, Optional[float]])), serialize=lambda v: v.tuple if isinstance(v, geos.Point) else v, parse_value=geos.Point, description="Represents a point as `(x, y, z)` or `(x, y)`.", ) LineString = strawberry.scalar( cast("type", NewType("LineString", tuple[Point])), serialize=lambda v: v.tuple if isinstance(v, geos.LineString) else v, parse_value=geos.LineString, description=( "A geographical line that gets multiple 'x, y' or 'x, y, z'" " tuples to form a line." ), ) LinearRing = strawberry.scalar( cast("type", NewType("LinearRing", tuple[Point])), serialize=lambda v: v.tuple if isinstance(v, geos.LinearRing) else v, parse_value=geos.LinearRing, description=( "A geographical line that gets multiple 'x, y' or 'x, y, z' " "tuples to form a line. It must be a circle. " "E.g. It maps back to itself." ), ) Polygon = strawberry.scalar( cast("type", NewType("Polygon", tuple[LinearRing])), serialize=lambda v: v.tuple if isinstance(v, geos.Polygon) else v, parse_value=lambda v: geos.Polygon(*[geos.LinearRing(x) for x in v]), description=( "A geographical object that gets 1 or 2 LinearRing objects" " as external and internal rings." ), ) MultiPoint = strawberry.scalar( cast("type", NewType("MultiPoint", tuple[Point])), serialize=lambda v: v.tuple if isinstance(v, geos.MultiPoint) else v, parse_value=lambda v: geos.MultiPoint(*[geos.Point(x) for x in v]), description="A geographical object that contains multiple Points.", ) MultiLineString = strawberry.scalar( cast("type", NewType("MultiLineString", tuple[LineString])), serialize=lambda v: v.tuple if isinstance(v, geos.MultiLineString) else v, parse_value=lambda v: geos.MultiLineString(*[geos.LineString(x) for x in v]), description="A geographical object that contains multiple line strings.", ) MultiPolygon = strawberry.scalar( cast("type", NewType("MultiPolygon", tuple[Polygon])), serialize=lambda v: v.tuple if isinstance(v, geos.MultiPolygon) else v, parse_value=lambda v: geos.MultiPolygon( *[geos.Polygon(*list(x)) for x in v], ), description="A geographical object that contains multiple polygons.", ) Geometry = strawberry.scalar( cast("type", NewType("Geometry", geos.GEOSGeometry)), serialize=lambda v: v.tuple if isinstance(v, geos.GEOSGeometry) else v, # type: ignore parse_value=lambda v: geos.GeometryCollection, description=( "An arbitrary geographical object. One of Point, " "LineString, LinearRing, Polygon, MultiPoint, MultiLineString, MultiPolygon." ), ) field_type_map.update( { geos_fields.PointField: Point, geos_fields.LineStringField: LineString, geos_fields.PolygonField: Polygon, geos_fields.MultiPointField: MultiPoint, geos_fields.MultiLineStringField: MultiLineString, geos_fields.MultiPolygonField: MultiPolygon, geos_fields.GeometryField: Geometry, }, ) input_field_type_map: dict[ Union[ type[fields.Field], type[related.RelatedField], type[reverse_related.ForeignObjectRel], ], type, ] = { files.FileField: Upload, files.ImageField: Upload, related.ForeignKey: OneToManyInput, related.ManyToManyField: ManyToManyInput, related.OneToOneField: OneToOneInput, reverse_related.ManyToManyRel: ManyToManyInput, reverse_related.ManyToOneRel: ManyToOneInput, reverse_related.OneToOneRel: OneToOneInput, } relay_field_type_map: dict[ Union[ type[fields.Field], type[related.RelatedField], type[reverse_related.ForeignObjectRel], ], type, ] = { fields.AutoField: relay.GlobalID, fields.BigAutoField: relay.GlobalID, related.ForeignKey: relay.Node, related.ManyToManyField: list[relay.Node], related.OneToOneField: relay.Node, reverse_related.ManyToManyRel: list[relay.Node], reverse_related.ManyToOneRel: list[relay.Node], reverse_related.OneToOneRel: relay.Node, } relay_input_field_type_map: dict[ Union[ type[fields.Field], type[related.RelatedField], type[reverse_related.ForeignObjectRel], ], type, ] = { related.ForeignKey: NodeInput, related.ManyToManyField: ListInput[NodeInput], related.OneToOneField: NodeInput, reverse_related.ManyToManyRel: ListInput[NodeInput], reverse_related.ManyToOneRel: ListInput[NodeInput], reverse_related.OneToOneRel: NodeInput, } def _resolve_array_field_type(model_field: Field): assert ArrayField is not None if isinstance(model_field, ArrayField): return list[_resolve_array_field_type(model_field.base_field)] base_field = field_type_map.get(type(model_field), NotImplemented) if base_field is NotImplemented: raise NotImplementedError( f"GraphQL type for model field '{model_field}' has not been implemented", ) return base_field def resolve_model_field_type( model_field: Union[Field, reverse_related.ForeignObjectRel], django_type: "StrawberryDjangoDefinition", ): settings = django_settings() # Django choices field if ( TextChoicesField is not None and IntegerChoicesField is not None and isinstance( model_field, (TextChoicesField, IntegerChoicesField), ) ): field_type = model_field.choices_enum enum_def = getattr(field_type, "_enum_definition", None) if enum_def is None: doc = ( inspect.cleandoc(field_type.__doc__) if settings["TYPE_DESCRIPTION_FROM_MODEL_DOCSTRING"] and field_type.__doc__ else None ) enum_def = strawberry.enum(field_type, description=doc)._enum_definition field_type = enum_def.wrapped_cls # Auto enum elif ( settings["GENERATE_ENUMS_FROM_CHOICES"] and isinstance(model_field, Field) and getattr(model_field, "choices", None) and not isinstance( getattr(model_field, "choices", [])[0][0], int, ) # Exclude IntegerChoices ): field_type = getattr(model_field, "_strawberry_enum", None) if field_type is None: meta = model_field.model._meta enum_choices = {} for c in cast("Iterable[tuple[str | None, str]]", model_field.choices): # Skip empty choice (__empty__) if not c[0]: continue # replace chars not compatible with GraphQL naming convention choice_name = re.sub(r"^[^_a-zA-Z]|[^_a-zA-Z0-9]", "_", c[0]) # use str() to trigger eventual django's gettext_lazy string choice_value = EnumValueDefinition(value=c[0], description=str(c[1])) while choice_name in enum_choices: choice_name += "_" enum_choices[choice_name] = choice_value field_type = strawberry.enum( # type: ignore enum.Enum( # type: ignore "".join( ( capitalize_first(to_camel_case(meta.app_label)), str(meta.object_name), capitalize_first(to_camel_case(model_field.name)), "Enum", ), ), enum_choices, ), description=( f"{meta.verbose_name} | {model_field.verbose_name}" if settings["FIELD_DESCRIPTION_FROM_HELP_TEXT"] else None ), ) model_field._strawberry_enum = field_type # type: ignore # Generated fields elif GeneratedField is not None and isinstance(model_field, GeneratedField): model_field_type = type(model_field.output_field) # type: ignore field_type = field_type_map.get(model_field_type, NotImplemented) elif ArrayField is not None and isinstance(model_field, ArrayField): field_type = _resolve_array_field_type(model_field) # Every other Field possibility else: force_global_id = settings["MAP_AUTO_ID_AS_GLOBAL_ID"] model_field_type = type(model_field) field_type: Any = None if django_type.is_filter and model_field.is_relation: field_type = ( NodeInput if force_global_id else filters.get_django_model_filter_input_type() ) elif django_type.is_input: input_type_map = input_field_type_map if force_global_id: input_type_map = {**input_type_map, **relay_input_field_type_map} field_type = input_type_map.get(model_field_type, None) if field_type is None: type_map = field_type_map if force_global_id: type_map = {**type_map, **relay_field_type_map} field_type = type_map.get(model_field_type, NotImplemented) if field_type is NotImplemented: raise NotImplementedError( f"GraphQL type for model field '{model_field}' has not been implemented", ) # TODO: could this be moved into filters.py using_old_filters = settings["USE_DEPRECATED_FILTERS"] if ( django_type.is_filter == "lookups" and not model_field.is_relation and (field_type is not bool or not using_old_filters) ): if using_old_filters: field_type = filters.FilterLookup[field_type] else: field_type = filter_types.type_filter_map.get( # type: ignore field_type, filter_types.FilterLookup )[field_type] return field_type def resolve_model_field_name( model_field: Union[Field, reverse_related.ForeignObjectRel], is_input: bool = False, is_filter: bool = False, is_fk_id: bool = False, ): if isinstance(model_field, reverse_related.ForeignObjectRel): return model_field.get_accessor_name() if is_fk_id or (is_input and not is_filter): return model_field.attname return model_field.name def get_model_field(model: type[Model], field_name: str): try: return model._meta.get_field(field_name) except FieldDoesNotExist as e: model_field_names = [] # we need to iterate through all the fields because reverse relation # fields cannot be accessed by get_field method for field in model._meta.get_fields(): model_field_name = resolve_model_field_name(field) if field_name == model_field_name: return field model_field_names.append(model_field_name) e.args = ( "{}, did you mean {}?".format( e.args[0], ", ".join([f"'{n}'" for n in model_field_names]), ), ) raise def is_optional( model_field: Union[Field, reverse_related.ForeignObjectRel], is_input: bool, partial: bool, ): if partial: return True if not model_field: return False if is_input: if isinstance(model_field, fields.AutoField): return True if isinstance(model_field, reverse_related.OneToOneRel): return model_field.null if model_field.many_to_many or model_field.one_to_many: return True if ( getattr(model_field, "blank", None) or getattr(model_field, "default", None) is not fields.NOT_PROVIDED ): return True if not isinstance( model_field, (reverse_related.ManyToManyRel, reverse_related.ManyToOneRel), ) or isinstance(model_field, reverse_related.OneToOneRel): # OneToOneRel is the subclass of ManyToOneRel, so additional check is needed return model_field.null return False strawberry-graphql-django-0.62.0/strawberry_django/filters.py000066400000000000000000000320121502405145400244450ustar00rootroot00000000000000from __future__ import annotations import functools import inspect import operator import warnings from enum import Enum from types import FunctionType from typing import ( TYPE_CHECKING, Any, Generic, TypeVar, cast, ) import strawberry from django.db.models import Q, QuerySet from strawberry import UNSET, relay from strawberry.tools import create_type from strawberry.types import has_object_definition from strawberry.types.base import WithStrawberryObjectDefinition from strawberry.types.field import StrawberryField, field from strawberry.types.unset import UnsetType from typing_extensions import Self, assert_never, dataclass_transform, deprecated from strawberry_django.fields.filter_order import ( RESOLVE_VALUE_META, WITH_NONE_META, FilterOrderField, FilterOrderFieldResolver, ) from strawberry_django.utils.typing import ( WithStrawberryDjangoObjectDefinition, has_django_definition, ) from .arguments import argument from .fields.base import StrawberryDjangoFieldBase from .settings import strawberry_django_settings if TYPE_CHECKING: from collections.abc import Callable, Sequence from types import FunctionType from django.db.models import Model from strawberry.types import Info from strawberry.types.arguments import StrawberryArgument T = TypeVar("T") _T = TypeVar("_T", bound=type) _QS = TypeVar("_QS", bound="QuerySet") FILTERS_ARG = "filters" _DjangoModelFilterInput: Any = None def get_django_model_filter_input_type(): global _DjangoModelFilterInput # noqa: PLW0603 if _DjangoModelFilterInput is None: settings = strawberry_django_settings() def _get_id(root) -> str: return root.pk id_field_name = settings["DEFAULT_PK_FIELD_NAME"] id_field = field( name=id_field_name, graphql_type=strawberry.ID, resolver=_get_id ) _DjangoModelFilterInput = create_type( "DjangoModelFilterInput", [id_field], # type: ignore is_input=True, ) return _DjangoModelFilterInput @strawberry.input class FilterLookup(Generic[T]): exact: T | None = UNSET i_exact: T | None = UNSET contains: T | None = UNSET i_contains: T | None = UNSET in_list: list[T] | None = UNSET gt: T | None = UNSET gte: T | None = UNSET lt: T | None = UNSET lte: T | None = UNSET starts_with: T | None = UNSET i_starts_with: T | None = UNSET ends_with: T | None = UNSET i_ends_with: T | None = UNSET range: list[T] | None = UNSET is_null: bool | None = UNSET regex: str | None = UNSET i_regex: str | None = UNSET lookup_name_conversion_map = { "i_exact": "iexact", "i_contains": "icontains", "in_list": "in", "starts_with": "startswith", "i_starts_with": "istartswith", "ends_with": "endswith", "i_ends_with": "iendswith", "is_null": "isnull", "i_regex": "iregex", } def resolve_value(value: Any) -> Any: if isinstance(value, list): return [resolve_value(v) for v in value] if isinstance(value, relay.GlobalID): return value.node_id if isinstance(value, Enum): return value.value return value @functools.lru_cache(maxsize=256) def _function_allow_passing_info(filter_method: FunctionType) -> bool: argspec = inspect.getfullargspec(filter_method) return "info" in getattr(argspec, "args", []) or "info" in getattr( argspec, "kwargs", [], ) def _process_deprecated_filter( filter_method: FunctionType, info: Info | None, queryset: _QS ) -> _QS: kwargs = {} if _function_allow_passing_info( # Pass the original __func__ which is always the same getattr(filter_method, "__func__", filter_method), ): kwargs["info"] = info return filter_method(queryset=queryset, **kwargs) def process_filters( filters: WithStrawberryObjectDefinition, queryset: _QS, info: Info | None, prefix: str = "", skip_object_filter_method: bool = False, ) -> tuple[_QS, Q]: using_old_filters = strawberry_django_settings()["USE_DEPRECATED_FILTERS"] q = Q() if not skip_object_filter_method and ( filter_method := getattr(filters, "filter", None) ): # Dedicated function for object if isinstance(filter_method, FilterOrderFieldResolver): return filter_method(filters, info, queryset=queryset, prefix=prefix) if using_old_filters: return _process_deprecated_filter(filter_method, info, queryset), q # This loop relies on the filter field order that is not quaranteed for GQL input objects: # "filter" has to be first since it overrides filtering for entire object # DISTINCT has to be last and OR has to be after because it must be # applied agains all other since default connector is AND for f in sorted( filters.__strawberry_definition__.fields, key=lambda x: len(x.name) if x.name in {"OR", "DISTINCT"} else 0, ): field_value = getattr(filters, f.name) # None is still acceptable for v1 (backwards compatibility) and filters that support it via metadata if field_value is UNSET or ( field_value is None and not f.metadata.get(WITH_NONE_META, using_old_filters) ): continue should_resolve = f.metadata.get(RESOLVE_VALUE_META, UNSET) field_name = lookup_name_conversion_map.get(f.name, f.name) if field_name == "DISTINCT": if field_value: queryset = queryset.distinct() elif field_name in ("AND", "OR", "NOT"): # noqa: PLR6201 values = field_value if isinstance(field_value, list) else [field_value] all_q = [Q()] for value in values: assert has_object_definition(value) queryset, sub_q_for_value = process_filters( cast("WithStrawberryObjectDefinition", value), queryset, info, prefix, ) all_q.append(sub_q_for_value) if field_name == "AND": sub_q = functools.reduce(operator.and_, all_q) q &= sub_q elif field_name == "OR": sub_q = functools.reduce(operator.or_, all_q) if isinstance(field_value, list): # The behavior of AND/OR/NOT with a list of values means AND/OR/NOT # with respect to the list members but AND with respect to other # filters q &= sub_q else: q |= sub_q elif field_name == "NOT": # Whether this is an AND or OR operation is undefined in the spec and implementation specific sub_q = functools.reduce(operator.or_, all_q) q &= ~sub_q else: assert_never(field_name) elif isinstance(f, FilterOrderField) and f.base_resolver: res = f.base_resolver( filters, info, value=(resolve_value(field_value) if should_resolve else field_value), queryset=queryset, prefix=prefix, ) if isinstance(res, tuple): queryset, sub_q = res else: sub_q = res q &= sub_q elif using_old_filters and ( filter_method := getattr(filters, f"filter_{field_name}", None) ): queryset = _process_deprecated_filter(filter_method, info, queryset) elif has_object_definition(field_value): queryset, sub_q = process_filters( cast("WithStrawberryObjectDefinition", field_value), queryset, info, f"{prefix}{field_name}__", ) q &= sub_q else: q &= Q(**{ f"{prefix}{field_name}": ( resolve_value(field_value) if should_resolve or should_resolve is UNSET else field_value ) }) return queryset, q def apply( filters: object | None, queryset: _QS, info: Info | None = None, pk: Any | None = None, ) -> _QS: if pk not in (None, strawberry.UNSET): # noqa: PLR6201 settings = strawberry_django_settings() pk_field_name = settings["DEFAULT_PK_FIELD_NAME"] queryset = queryset.filter(**{pk_field_name: pk}) if filters in (None, strawberry.UNSET) or not has_django_definition(filters): # noqa: PLR6201 return queryset queryset, q = process_filters( cast("WithStrawberryObjectDefinition", filters), queryset, info ) if q: queryset = queryset.filter(q) return queryset class StrawberryDjangoFieldFilters(StrawberryDjangoFieldBase): def __init__(self, filters: type | UnsetType | None = UNSET, **kwargs): if filters and not has_object_definition(filters): raise TypeError("filters needs to be a strawberry type") self.filters = filters super().__init__(**kwargs) def __copy__(self) -> Self: new_field = super().__copy__() new_field.filters = self.filters return new_field @property def arguments(self) -> list[StrawberryArgument]: arguments = [] if self.base_resolver is None and not self.is_model_property: filters = self.get_filters() origin = cast("WithStrawberryObjectDefinition", self.origin) is_root_query = origin.__strawberry_definition__.name == "Query" if ( self.django_model and is_root_query and isinstance(self.django_type, relay.Node) ): arguments.append( ( argument("ids", list[relay.GlobalID]) if self.is_list else argument("id", relay.GlobalID) ), ) if ( self.django_model and is_root_query and not self.is_list and not self.is_connection and not self.is_paginated ): settings = strawberry_django_settings() arguments.append( argument( settings["DEFAULT_PK_FIELD_NAME"], cast("type", strawberry.ID) ) ) elif filters is not None and self.is_list: is_optional = True from .mutations.fields import DjangoMutationBase if isinstance(self, DjangoMutationBase): settings = strawberry_django_settings() is_optional = settings["ALLOW_MUTATIONS_WITHOUT_FILTERS"] arguments.append( argument(FILTERS_ARG, filters, is_optional=is_optional) ) return super().arguments + arguments @arguments.setter def arguments(self, value: list[StrawberryArgument]): args_prop = super(StrawberryDjangoFieldFilters, self.__class__).arguments return args_prop.fset(self, value) # type: ignore def get_filters(self) -> type[WithStrawberryObjectDefinition] | None: filters = self.filters if filters is None: return None if isinstance(filters, UnsetType): django_type = self.django_type filters = ( django_type.__strawberry_django_definition__.filters if django_type is not None else None ) return filters if filters is not UNSET else None def get_queryset( self, queryset: _QS, info: Info, *, filters: WithStrawberryDjangoObjectDefinition | None = None, **kwargs, ) -> _QS: settings = strawberry_django_settings() pk = kwargs.get(settings["DEFAULT_PK_FIELD_NAME"], None) queryset = super().get_queryset(queryset, info, **kwargs) return apply(filters, queryset, info, pk) @dataclass_transform( order_default=True, field_specifiers=( StrawberryField, field, ), ) def filter_type( model: type[Model], *, name: str | None = None, description: str | None = None, directives: Sequence[object] | None = (), lookups: bool = False, ) -> Callable[[_T], _T]: from .type import input # noqa: A004 return input( model, name=name, description=description, directives=directives, is_filter="lookups" if lookups else True, partial=True, ) if TYPE_CHECKING: filter = deprecated("`filter` is deprecated, use `filter_type` instead.")( # noqa: A001 filter_type ) def __getattr__(name: str) -> Any: if name == "filter": warnings.warn( "`filter` is deprecated, use `filter_type` instead.", DeprecationWarning, stacklevel=2, ) return filter_type raise AttributeError(f"module {__name__} has no attribute {name}") strawberry-graphql-django-0.62.0/strawberry_django/integrations/000077500000000000000000000000001502405145400251335ustar00rootroot00000000000000strawberry-graphql-django-0.62.0/strawberry_django/integrations/__init__.py000066400000000000000000000000001502405145400272320ustar00rootroot00000000000000strawberry-graphql-django-0.62.0/strawberry_django/integrations/guardian.py000066400000000000000000000023471502405145400273050ustar00rootroot00000000000000import contextlib import dataclasses from typing import Union, cast from django.contrib.auth import get_user_model from django.db import models from guardian.conf import settings as guardian_settings from guardian.models.models import GroupObjectPermissionBase, UserObjectPermissionBase from guardian.utils import get_anonymous_user as _get_anonymous_user from guardian.utils import get_group_obj_perms_model, get_user_obj_perms_model from strawberry_django.utils.typing import UserType @dataclasses.dataclass class ObjectPermissionModels: user: UserObjectPermissionBase group: GroupObjectPermissionBase def get_object_permission_models( model: Union[models.Model, type[models.Model]], ) -> ObjectPermissionModels: return ObjectPermissionModels( user=cast("UserObjectPermissionBase", get_user_obj_perms_model(model)), group=cast("GroupObjectPermissionBase", get_group_obj_perms_model(model)), ) def get_user_or_anonymous(user: UserType) -> UserType: username = guardian_settings.ANONYMOUS_USER_NAME or "" if user.is_anonymous and user.get_username() != username: with contextlib.suppress(get_user_model().DoesNotExist): return cast("UserType", _get_anonymous_user()) return user strawberry-graphql-django-0.62.0/strawberry_django/management/000077500000000000000000000000001502405145400245415ustar00rootroot00000000000000strawberry-graphql-django-0.62.0/strawberry_django/management/__init__.py000066400000000000000000000000001502405145400266400ustar00rootroot00000000000000strawberry-graphql-django-0.62.0/strawberry_django/management/commands/000077500000000000000000000000001502405145400263425ustar00rootroot00000000000000strawberry-graphql-django-0.62.0/strawberry_django/management/commands/__init__.py000066400000000000000000000000001502405145400304410ustar00rootroot00000000000000strawberry-graphql-django-0.62.0/strawberry_django/management/commands/export_schema.py000066400000000000000000000023221502405145400315540ustar00rootroot00000000000000import pathlib from django.core.management.base import BaseCommand, CommandError from strawberry import Schema from strawberry.printer import print_schema from strawberry.utils.importer import import_module_symbol class Command(BaseCommand): help = "Export the graphql schema" def add_arguments(self, parser): parser.add_argument("schema", nargs=1, type=str, help="The schema location") parser.add_argument( "--path", nargs="?", type=str, help="Optional path to export", ) def handle(self, *args, **options): try: schema_symbol = import_module_symbol( options["schema"][0], default_symbol_name="schema", ) except (ImportError, AttributeError) as e: raise CommandError(str(e)) from e if not isinstance(schema_symbol, Schema): raise CommandError("The `schema` must be an instance of strawberry.Schema") schema_output = print_schema(schema_symbol) path = options.get("path") if path: pathlib.Path(path).write_text(schema_output, encoding="utf-8") else: self.stdout.write(schema_output) strawberry-graphql-django-0.62.0/strawberry_django/middlewares/000077500000000000000000000000001502405145400247255ustar00rootroot00000000000000strawberry-graphql-django-0.62.0/strawberry_django/middlewares/__init__.py000066400000000000000000000000001502405145400270240ustar00rootroot00000000000000strawberry-graphql-django-0.62.0/strawberry_django/middlewares/debug_toolbar.py000066400000000000000000000161361502405145400301160ustar00rootroot00000000000000# Based on https://github.com/flavors/django-graphiql-debug-toolbar import asyncio import collections import contextlib import json import weakref from typing import Optional from asgiref.sync import iscoroutinefunction, markcoroutinefunction, sync_to_async from debug_toolbar import VERSION as _DEBUG_TOOLBAR_VERSION from debug_toolbar.middleware import ( DebugToolbarMiddleware as _DebugToolbarMiddleware, ) from debug_toolbar.middleware import get_show_toolbar from debug_toolbar.panels.sql.panel import SQLPanel from debug_toolbar.panels.templates import TemplatesPanel from debug_toolbar.toolbar import DebugToolbar from django.core.exceptions import SynchronousOnlyOperation from django.core.serializers.json import DjangoJSONEncoder from django.http.request import HttpRequest from django.http.response import HttpResponse from django.template.loader import render_to_string from django.utils.encoding import force_str from strawberry.django.views import BaseView _HTML_TYPES = {"text/html", "application/xhtml+xml"} _store_cache = weakref.WeakKeyDictionary() _debug_toolbar_51plus = [ int("".join(filter(str.isdigit, n)) or "0") for n in _DEBUG_TOOLBAR_VERSION.split(".") ] >= [5, 1] _debug_toolbar_map: "weakref.WeakKeyDictionary[HttpRequest, DebugToolbar]" = ( weakref.WeakKeyDictionary() ) _original_store = DebugToolbar.store _original_debug_toolbar_init = DebugToolbar.__init__ _original_store_template_info = TemplatesPanel._store_template_info def _is_websocket(request: HttpRequest): return ( request.META.get("HTTP_UPGRADE") == "websocket" and request.META.get("HTTP_CONNECTION") == "Upgrade" ) def _debug_toolbar_init(self, request, *args, **kwargs): _debug_toolbar_map[request] = self _original_debug_toolbar_init(self, request, *args, **kwargs) self.config["RENDER_PANELS"] = False self.config["SKIP_TEMPLATE_PREFIXES"] = ( *tuple(self.config.get("SKIP_TEMPLATE_PREFIXES", [])), "graphql/", ) def _store(toolbar: DebugToolbar): _debug_toolbar_map[toolbar.request] = toolbar _original_store(toolbar) _store_cache[toolbar.request] = toolbar.store_id def _store_template_info(*args, **kwargs): with contextlib.suppress(SynchronousOnlyOperation): return _original_store_template_info(*args, **kwargs) def _get_payload(request: HttpRequest, response: HttpResponse): store_id = _store_cache.get(request) if not store_id: return None toolbar: Optional[DebugToolbar] = DebugToolbar.fetch(store_id) if not toolbar: return None content = force_str(response.content, encoding=response.charset) payload = json.loads(content, object_pairs_hook=collections.OrderedDict) payload["debugToolbar"] = collections.OrderedDict( [("panels", collections.OrderedDict())], ) payload["debugToolbar"]["storeId"] = toolbar.store_id for p in reversed(toolbar.enabled_panels): if p.panel_id == "TemplatesPanel": continue title = p.title if p.has_content else None sub = p.nav_subtitle payload["debugToolbar"]["panels"][p.panel_id] = { "title": title() if callable(title) else title, "subtitle": sub() if callable(sub) else sub, } return payload DebugToolbar.__init__ = _debug_toolbar_init DebugToolbar.store = _store # type: ignore TemplatesPanel._store_template_info = _store_template_info class DebugToolbarMiddleware(_DebugToolbarMiddleware): sync_capable = True async_capable = True def __init__(self, get_response): self._original_get_response = get_response if iscoroutinefunction(get_response): markcoroutinefunction(self) def _get_response(request): toolbar = _debug_toolbar_map.pop(request, None) for panel in toolbar.enabled_panels if toolbar else []: if isinstance(panel, SQLPanel): sql_panel = panel break else: sql_panel = None async def _inner_get_response(): if sql_panel: await sync_to_async(sql_panel.enable_instrumentation)() try: return await self._original_get_response(request) finally: if sql_panel: await sync_to_async(sql_panel.disable_instrumentation)() return asyncio.run(_inner_get_response()) get_response = _get_response super().__init__(get_response) def __call__(self, request: HttpRequest): if iscoroutinefunction(self): return self.__acall__(request) if _is_websocket(request): return self._original_get_response(request) return self.process_request(request) async def __acall__(self, request: HttpRequest): # noqa: PLW3201 if _is_websocket(request): return await self._original_get_response(request) return await sync_to_async(self.process_request, thread_sensitive=False)( request, ) def process_request(self, request: HttpRequest): response = super().__call__(request) if _debug_toolbar_51plus: # async mode is handled on our side show_toolbar = get_show_toolbar(async_mode=False) else: show_toolbar = get_show_toolbar() if ( callable(show_toolbar) and not show_toolbar(request) ) or DebugToolbar.is_toolbar_request(request): return response content_type = response.get("Content-Type", "").split(";")[0] is_html = content_type in _HTML_TYPES is_graphiql = getattr(request, "_is_graphiql", False) if is_html and is_graphiql and response.status_code == 200: # noqa: PLR2004 template = render_to_string("strawberry_django/debug_toolbar.html") response.write(template) if "Content-Length" in response: response["Content-Length"] = len(response.content) if is_html or not is_graphiql or content_type != "application/json": return response try: operation_name = json.loads(request.body).get("operationName") except Exception: # noqa: BLE001 operation_name = None # Do not return the payload for introspection queries, otherwise IDEs such as # apollo sandbox that query the introspection all the time will remove older # results from the history. payload = ( _get_payload(request, response) if operation_name != "IntrospectionQuery" else None ) if payload is None: return response response.content = json.dumps(payload, cls=DjangoJSONEncoder) if "Content-Length" in response: response["Content-Length"] = len(response.content) return response def process_view(self, request: HttpRequest, view_func, *args, **kwargs): view = getattr(view_func, "view_class", None) request._is_graphiql = bool(view and issubclass(view, BaseView)) # type: ignore strawberry-graphql-django-0.62.0/strawberry_django/mutations/000077500000000000000000000000001502405145400244505ustar00rootroot00000000000000strawberry-graphql-django-0.62.0/strawberry_django/mutations/__init__.py000066400000000000000000000002471502405145400265640ustar00rootroot00000000000000from .mutations import create, delete, input_mutation, mutation, update __all__ = [ "create", "delete", "input_mutation", "mutation", "update", ] strawberry-graphql-django-0.62.0/strawberry_django/mutations/fields.py000066400000000000000000000332271502405145400262770ustar00rootroot00000000000000from __future__ import annotations import inspect from typing import TYPE_CHECKING, Annotated, Any, TypeVar, Union import strawberry from django.core.exceptions import ( NON_FIELD_ERRORS, ObjectDoesNotExist, PermissionDenied, ValidationError, ) from django.db import models, transaction from strawberry import UNSET, relay from strawberry.annotation import StrawberryAnnotation from strawberry.types.field import UNRESOLVED from strawberry.utils.str_converters import capitalize_first, to_camel_case from strawberry_django.arguments import argument from strawberry_django.fields.field import ( StrawberryDjangoFieldBase, StrawberryDjangoFieldFilters, ) from strawberry_django.fields.types import OperationInfo, OperationMessage from strawberry_django.optimizer import DjangoOptimizerExtension, optimize from strawberry_django.permissions import filter_with_perms, get_with_perms from strawberry_django.resolvers import django_resolver from strawberry_django.settings import strawberry_django_settings from strawberry_django.utils.inspect import get_possible_types from . import resolvers if TYPE_CHECKING: from collections.abc import Iterable from graphql.pyutils import AwaitableOrValue from strawberry.types import Info from strawberry.types.arguments import StrawberryArgument from strawberry.types.base import ( StrawberryObjectDefinition, StrawberryType, WithStrawberryObjectDefinition, ) from typing_extensions import Literal, Self from .types import FullCleanOptions _T = TypeVar("_T", bound="models.Model | list[models.Model]") def _get_validaton_error_message(error: ValidationError): if not error.message: return "Unknown error" return error.message % error.params if error.params else error.message def _get_validation_errors(error: Exception): if isinstance(error, PermissionDenied): kind = OperationMessage.Kind.PERMISSION elif isinstance(error, ValidationError): kind = OperationMessage.Kind.VALIDATION elif isinstance(error, ObjectDoesNotExist): kind = OperationMessage.Kind.ERROR else: kind = OperationMessage.Kind.ERROR if isinstance(error, ValidationError) and hasattr(error, "error_dict"): # convert field errors for field, field_errors in (error.error_dict or {}).items(): for e in field_errors: yield OperationMessage( kind=kind, field=to_camel_case(field) if field != NON_FIELD_ERRORS else None, message=_get_validaton_error_message(e), code=getattr(e, "code", None), ) elif isinstance(error, ValidationError) and hasattr(error, "error_list"): # convert non-field errors for e in error.error_list or []: yield OperationMessage( kind=kind, message=_get_validaton_error_message(e), code=getattr(error, "code", None), ) else: msg = getattr(error, "msg", None) if msg is None: msg = str(error) yield OperationMessage( kind=kind, message=msg, code=getattr(error, "code", None), ) def _handle_exception(error: Exception): if isinstance(error, (ValidationError, PermissionDenied, ObjectDoesNotExist)): return OperationInfo( messages=list(_get_validation_errors(error)), ) raise error class DjangoMutationBase(StrawberryDjangoFieldBase): def __init__( self, *args, handle_django_errors: bool | None = None, **kwargs, ): self._resolved_return_type: bool = False if handle_django_errors is None: settings = strawberry_django_settings() handle_django_errors = settings["MUTATIONS_DEFAULT_HANDLE_ERRORS"] self.handle_errors = handle_django_errors super().__init__(*args, **kwargs) def __copy__(self) -> Self: new_field = super().__copy__() new_field.handle_errors = self.handle_errors return new_field def resolve_type( self, *, type_definition: StrawberryObjectDefinition | None = None, ) -> ( StrawberryType | type[WithStrawberryObjectDefinition] | Literal[UNRESOLVED] # type: ignore ): resolved = super().resolve_type(type_definition=type_definition) if resolved is UNRESOLVED: return resolved if self.handle_errors and not self._resolved_return_type: types_ = tuple(get_possible_types(resolved)) if OperationInfo not in types_: types_ = (*types_, OperationInfo) name = capitalize_first(to_camel_case(self.python_name)) resolved = Annotated[ Union[types_], strawberry.union(f"{name}Payload"), ] self.type_annotation = StrawberryAnnotation( resolved, namespace=getattr(self.type_annotation, "namespace", None), ) self._resolved_return_type = True return resolved def get_result( self, source: Any, info: Info | None, args: list[Any], kwargs: dict[str, Any], ) -> AwaitableOrValue[Any]: if not self.handle_errors: return self.resolver(source, info, args, kwargs) # TODO: Any other exception types that we should capture here? try: resolved = self.resolver(source, info, args, kwargs) except Exception as e: # noqa: BLE001 return _handle_exception(e) if inspect.isawaitable(resolved): async def async_resolver(): try: return await resolved except Exception as e: # noqa: BLE001 return _handle_exception(e) return async_resolver() return resolved class DjangoMutationCUD(DjangoMutationBase): def __init__( self, input_type: type | None = None, full_clean: bool | FullCleanOptions = True, argument_name: str | None = None, key_attr: str | None = None, **kwargs, ): self.full_clean = full_clean self.input_type = input_type if key_attr is None: settings = strawberry_django_settings() key_attr = settings["DEFAULT_PK_FIELD_NAME"] self.key_attr = key_attr if argument_name is None: settings = strawberry_django_settings() argument_name = settings["MUTATIONS_DEFAULT_ARGUMENT_NAME"] self.argument_name = argument_name super().__init__(**kwargs) def __copy__(self) -> Self: new_field = super().__copy__() new_field.input_type = self.input_type new_field.full_clean = self.full_clean return new_field @property def arguments(self): arguments = super().arguments if not self.input_type: return arguments return [ *arguments, argument( self.argument_name, self.input_type, ), ] @arguments.setter def arguments(self, value: list[StrawberryArgument]): args_prop = super(DjangoMutationBase, self.__class__).arguments return args_prop.fset(self, value) # type: ignore def refetch(self, resolved: _T, *, info: Info | None) -> _T: if not DjangoOptimizerExtension.enabled.get() or info is None: return resolved if isinstance(resolved, list) and resolved: model = type(resolved[0]) if issubclass(model, models.Model): original_order = {r.pk: i for i, r in enumerate(resolved)} resolved_qs = optimize( model._default_manager.filter(pk__in=[r.pk for r in resolved]), info=info, ) # sort the resolved objects in the order they were given resolved = sorted( # type: ignore resolved_qs, key=lambda r: original_order[r.pk], ) elif isinstance(resolved, models.Model): model = type(resolved) resolved = optimize( model._default_manager.filter(pk=resolved.pk), info=info, ).get() return resolved class DjangoCreateMutation(DjangoMutationCUD): @django_resolver @transaction.atomic def resolver( self, source: Any, info: Info | None, args: list[Any], kwargs: dict[str, Any], ) -> Any: assert info is not None data: list[Any] | Any = kwargs.get(self.argument_name) # Do not optimize anything while retrieving the object to create with DjangoOptimizerExtension.disabled(): if self.is_list: assert isinstance(data, list) resolved = [ self.create( resolvers.parse_input(info, vars(d), key_attr=self.key_attr), info=info, ) for d in data ] else: assert not isinstance(data, list) resolved = self.create( resolvers.parse_input(info, vars(data), key_attr=self.key_attr) if data is not None else {}, info=info, ) return self.refetch(resolved, info=info) def create(self, data: dict[str, Any], *, info: Info): model = self.django_model assert model is not None return resolvers.create( info, model, data, key_attr=self.key_attr, full_clean=self.full_clean, ) def get_vdata(data: Any) -> dict[str, Any]: return vars(data).copy() if data is not None else {} def get_pk( data: dict[str, Any], *, key_attr: str | None = None, ) -> strawberry.ID | relay.GlobalID | Literal[UNSET] | None: # type: ignore if key_attr is None: settings = strawberry_django_settings() key_attr = settings["DEFAULT_PK_FIELD_NAME"] pk = data.pop(key_attr, UNSET) if pk is UNSET: pk = data.pop("id", UNSET) return pk class DjangoUpdateMutation(DjangoMutationCUD, StrawberryDjangoFieldFilters): @django_resolver @transaction.atomic def resolver( self, source: Any, info: Info | None, args: list[Any], kwargs: dict[str, Any], ) -> Any: assert info is not None data: list[Any] | Any = kwargs.get(self.argument_name) # Do not optimize anything while retrieving the object to update with DjangoOptimizerExtension.disabled(): if isinstance(data, list): resolved = [self.instance_level_update(info, kwargs, d) for d in data] else: resolved = self.instance_level_update(info, kwargs, data) return self.refetch(resolved, info=info) def instance_level_update( self, info: Info, kwargs: dict[str, Any], data: Any, ) -> Any: model = self.django_model assert model is not None vdata = get_vdata(data) pk = get_pk(vdata, key_attr=self.key_attr) if pk not in (None, UNSET): # noqa: PLR6201 instance = get_with_perms( pk, info, required=True, model=model, key_attr=self.key_attr, ) else: instance = filter_with_perms( self.get_queryset( queryset=model._default_manager.all(), info=info, **kwargs, ), info, ) return self.update( info, instance, resolvers.parse_input(info, vdata, key_attr=self.key_attr) ) def update( self, info: Info, instance: models.Model | Iterable[models.Model], data: dict[str, Any], ): return resolvers.update( info, instance, data, key_attr=self.key_attr, full_clean=self.full_clean, ) class DjangoDeleteMutation( DjangoMutationCUD, DjangoMutationBase, StrawberryDjangoFieldFilters, ): @django_resolver @transaction.atomic def resolver( self, source: Any, info: Info | None, args: list[Any], kwargs: dict[str, Any], ) -> Any: assert info is not None model = self.django_model assert model is not None data: Any = kwargs.get(self.argument_name) vdata = get_vdata(data) pk = get_pk(vdata, key_attr=self.key_attr) if pk not in (None, UNSET): # noqa: PLR6201 instance = get_with_perms( pk, info, required=True, model=model, key_attr=self.key_attr, ) else: instance = filter_with_perms( self.get_queryset( queryset=model._default_manager.all(), info=info, **kwargs, ), info, ) return self.delete( info, instance, resolvers.parse_input(info, vdata, key_attr=self.key_attr), ) def delete( self, info: Info, instance: models.Model | Iterable[models.Model], data: dict[str, Any] | None = None, ): return resolvers.delete( info, instance, data=data, ) strawberry-graphql-django-0.62.0/strawberry_django/mutations/mutations.py000066400000000000000000000334751502405145400270610ustar00rootroot00000000000000import dataclasses from collections.abc import Callable, Mapping, Sequence from typing import ( Any, Optional, TypeVar, Union, overload, ) from strawberry.annotation import StrawberryAnnotation from strawberry.extensions.field_extension import FieldExtension from strawberry.field_extensions import InputMutationExtension from strawberry.permission import BasePermission from strawberry.types.fields.resolver import StrawberryResolver from strawberry.types.unset import UNSET, UnsetType from typing_extensions import Literal from .fields import ( DjangoCreateMutation, DjangoDeleteMutation, DjangoMutationBase, DjangoUpdateMutation, ) from .types import FullCleanOptions _T = TypeVar("_T") @overload def mutation( *, resolver: Callable[[], _T], name: Optional[str] = None, field_name: Optional[str] = None, is_subscription: bool = False, description: Optional[str] = None, init: Literal[False] = False, permission_classes: Optional[list[type[BasePermission]]] = None, deprecation_reason: Optional[str] = None, default: Any = dataclasses.MISSING, default_factory: Union[Callable[..., object], object] = dataclasses.MISSING, metadata: Optional[Mapping[Any, Any]] = None, directives: Optional[Sequence[object]] = (), graphql_type: Optional[Any] = None, extensions: Optional[list[FieldExtension]] = None, handle_django_errors: Optional[bool] = None, ) -> _T: ... @overload def mutation( *, name: Optional[str] = None, field_name: Optional[str] = None, is_subscription: bool = False, description: Optional[str] = None, init: Literal[True] = True, permission_classes: Optional[list[type[BasePermission]]] = None, deprecation_reason: Optional[str] = None, default: Any = dataclasses.MISSING, default_factory: Union[Callable[..., object], object] = dataclasses.MISSING, metadata: Optional[Mapping[Any, Any]] = None, directives: Optional[Sequence[object]] = (), graphql_type: Optional[Any] = None, extensions: Optional[list[FieldExtension]] = None, handle_django_errors: Optional[bool] = None, ) -> Any: ... @overload def mutation( resolver: Union[StrawberryResolver, Callable, staticmethod, classmethod], *, name: Optional[str] = None, field_name: Optional[str] = None, is_subscription: bool = False, description: Optional[str] = None, permission_classes: Optional[list[type[BasePermission]]] = None, deprecation_reason: Optional[str] = None, default: Any = dataclasses.MISSING, default_factory: Union[Callable[..., object], object] = dataclasses.MISSING, metadata: Optional[Mapping[Any, Any]] = None, directives: Optional[Sequence[object]] = (), graphql_type: Optional[Any] = None, extensions: Optional[list[FieldExtension]] = None, handle_django_errors: Optional[bool] = None, ) -> DjangoMutationBase: ... def mutation( resolver=None, *, name: Optional[str] = None, field_name: Optional[str] = None, is_subscription: bool = False, description: Optional[str] = None, permission_classes: Optional[list[type[BasePermission]]] = None, deprecation_reason: Optional[str] = None, default: Any = dataclasses.MISSING, default_factory: Union[Callable[..., object], object] = dataclasses.MISSING, metadata: Optional[Mapping[Any, Any]] = None, directives: Optional[Sequence[object]] = (), graphql_type: Optional[Any] = None, extensions: Optional[list[FieldExtension]] = None, handle_django_errors: Optional[bool] = None, # This init parameter is used by pyright to determine whether this field # is added in the constructor or not. It is not used to change # any behavior at the moment. init: Optional[bool] = None, ) -> Any: """Annotate a property or a method to create a mutation field.""" f = DjangoMutationBase( python_name=None, django_name=field_name, graphql_name=name, type_annotation=StrawberryAnnotation.from_annotation(graphql_type), description=description, is_subscription=is_subscription, permission_classes=permission_classes or [], deprecation_reason=deprecation_reason, default=default, default_factory=default_factory, metadata=metadata, directives=directives, extensions=extensions or (), handle_django_errors=handle_django_errors, ) if resolver is not None: return f(resolver) return f @overload def input_mutation( *, resolver: Callable[[], _T], name: Optional[str] = None, field_name: Optional[str] = None, is_subscription: bool = False, description: Optional[str] = None, init: Literal[False] = False, permission_classes: Optional[list[type[BasePermission]]] = None, deprecation_reason: Optional[str] = None, default: Any = dataclasses.MISSING, default_factory: Union[Callable[..., object], object] = dataclasses.MISSING, metadata: Optional[Mapping[Any, Any]] = None, directives: Optional[Sequence[object]] = (), graphql_type: Optional[Any] = None, extensions: Optional[list[FieldExtension]] = None, handle_django_errors: Optional[bool] = None, ) -> _T: ... @overload def input_mutation( *, name: Optional[str] = None, field_name: Optional[str] = None, is_subscription: bool = False, description: Optional[str] = None, init: Literal[True] = True, permission_classes: Optional[list[type[BasePermission]]] = None, deprecation_reason: Optional[str] = None, default: Any = dataclasses.MISSING, default_factory: Union[Callable[..., object], object] = dataclasses.MISSING, metadata: Optional[Mapping[Any, Any]] = None, directives: Optional[Sequence[object]] = (), graphql_type: Optional[Any] = None, extensions: Optional[list[FieldExtension]] = None, handle_django_errors: Optional[bool] = None, ) -> Any: ... @overload def input_mutation( resolver: Union[StrawberryResolver, Callable, staticmethod, classmethod], *, name: Optional[str] = None, field_name: Optional[str] = None, is_subscription: bool = False, description: Optional[str] = None, permission_classes: Optional[list[type[BasePermission]]] = None, deprecation_reason: Optional[str] = None, default: Any = dataclasses.MISSING, default_factory: Union[Callable[..., object], object] = dataclasses.MISSING, metadata: Optional[Mapping[Any, Any]] = None, directives: Optional[Sequence[object]] = (), graphql_type: Optional[Any] = None, extensions: Optional[list[FieldExtension]] = None, handle_django_errors: Optional[bool] = None, ) -> DjangoMutationBase: ... def input_mutation( resolver=None, *, name: Optional[str] = None, field_name: Optional[str] = None, is_subscription: bool = False, description: Optional[str] = None, permission_classes: Optional[list[type[BasePermission]]] = None, deprecation_reason: Optional[str] = None, default: Any = dataclasses.MISSING, default_factory: Union[Callable[..., object], object] = dataclasses.MISSING, metadata: Optional[Mapping[Any, Any]] = None, directives: Optional[Sequence[object]] = (), graphql_type: Optional[Any] = None, extensions: Optional[list[FieldExtension]] = None, handle_django_errors: Optional[bool] = None, # This init parameter is used by pyright to determine whether this field # is added in the constructor or not. It is not used to change # any behavior at the moment. init: Optional[bool] = None, ) -> Any: """Annotate a property or a method to create an input mutation field.""" extensions = [*(extensions or []), InputMutationExtension()] f = DjangoMutationBase( python_name=None, django_name=field_name, graphql_name=name, type_annotation=StrawberryAnnotation.from_annotation(graphql_type), description=description, is_subscription=is_subscription, permission_classes=permission_classes or [], deprecation_reason=deprecation_reason, default=default, default_factory=default_factory, metadata=metadata, directives=directives, extensions=extensions, handle_django_errors=handle_django_errors, ) if resolver is not None: return f(resolver) return f def create( input_type: Optional[type] = None, *, name: Optional[str] = None, field_name: Optional[str] = None, is_subscription: bool = False, description: Optional[str] = None, init: Literal[True] = True, permission_classes: Optional[list[type[BasePermission]]] = None, deprecation_reason: Optional[str] = None, default: Any = dataclasses.MISSING, default_factory: Union[Callable[..., object], object] = dataclasses.MISSING, metadata: Optional[Mapping[Any, Any]] = None, directives: Optional[Sequence[object]] = (), graphql_type: Optional[Any] = None, extensions: Optional[list[FieldExtension]] = None, argument_name: Optional[str] = None, handle_django_errors: Optional[bool] = None, full_clean: Union[bool, FullCleanOptions] = True, ) -> Any: """Create mutation for django input fields. Automatically create data for django input fields. Examples -------- >>> @strawberry.django.input ... class ProductInput: ... name: strawberry.auto ... price: strawberry.auto ... >>> @strawberry.mutation >>> class Mutation: ... create_product: ProductType = strawberry.django.create_mutation( ... ProductInput ... ) """ return DjangoCreateMutation( input_type, python_name=None, django_name=field_name, graphql_name=name, type_annotation=StrawberryAnnotation.from_annotation(graphql_type), description=description, is_subscription=is_subscription, permission_classes=permission_classes or [], deprecation_reason=deprecation_reason, default=default, default_factory=default_factory, metadata=metadata, directives=directives, extensions=extensions or (), argument_name=argument_name, handle_django_errors=handle_django_errors, full_clean=full_clean, ) def update( input_type: Optional[type] = None, *, name: Optional[str] = None, field_name: Optional[str] = None, filters: Union[type, UnsetType, None] = UNSET, is_subscription: bool = False, description: Optional[str] = None, init: Literal[True] = True, permission_classes: Optional[list[type[BasePermission]]] = None, deprecation_reason: Optional[str] = None, default: Any = dataclasses.MISSING, default_factory: Union[Callable[..., object], object] = dataclasses.MISSING, metadata: Optional[Mapping[Any, Any]] = None, directives: Optional[Sequence[object]] = (), graphql_type: Optional[Any] = None, extensions: Optional[list[FieldExtension]] = None, argument_name: Optional[str] = None, handle_django_errors: Optional[bool] = None, key_attr: Optional[str] = None, full_clean: Union[bool, FullCleanOptions] = True, ) -> Any: """Update mutation for django input fields. Examples -------- >>> @strawberry.django.input ... class ProductInput(IdInput): ... name: strawberry.auto ... price: strawberry.auto ... >>> @strawberry.mutation >>> class Mutation: ... create_product: ProductType = strawberry.django.update_mutation( ... ProductInput ... ) """ return DjangoUpdateMutation( input_type, python_name=None, django_name=field_name, graphql_name=name, type_annotation=StrawberryAnnotation.from_annotation(graphql_type), description=description, is_subscription=is_subscription, permission_classes=permission_classes or [], deprecation_reason=deprecation_reason, default=default, default_factory=default_factory, metadata=metadata, directives=directives, filters=filters, extensions=extensions or (), argument_name=argument_name, handle_django_errors=handle_django_errors, key_attr=key_attr, full_clean=full_clean, ) def delete( input_type: Optional[type] = None, *, name: Optional[str] = None, field_name: Optional[str] = None, filters: Union[type, UnsetType, None] = UNSET, is_subscription: bool = False, description: Optional[str] = None, init: Literal[True] = True, permission_classes: Optional[list[type[BasePermission]]] = None, deprecation_reason: Optional[str] = None, default: Any = dataclasses.MISSING, default_factory: Union[Callable[..., object], object] = dataclasses.MISSING, metadata: Optional[Mapping[Any, Any]] = None, directives: Optional[Sequence[object]] = (), extensions: Optional[list[FieldExtension]] = None, graphql_type: Optional[Any] = None, argument_name: Optional[str] = None, handle_django_errors: Optional[bool] = None, key_attr: Optional[str] = None, full_clean: Union[bool, FullCleanOptions] = True, ) -> Any: return DjangoDeleteMutation( input_type=input_type, python_name=None, django_name=field_name, graphql_name=name, type_annotation=StrawberryAnnotation.from_annotation(graphql_type), description=description, is_subscription=is_subscription, permission_classes=permission_classes or [], deprecation_reason=deprecation_reason, default=default, default_factory=default_factory, metadata=metadata, directives=directives, filters=filters, extensions=extensions or (), argument_name=argument_name, handle_django_errors=handle_django_errors, key_attr=key_attr, full_clean=full_clean, ) strawberry-graphql-django-0.62.0/strawberry_django/mutations/resolvers.py000066400000000000000000000562051502405145400270560ustar00rootroot00000000000000from __future__ import annotations import dataclasses from collections.abc import Callable, Iterable from enum import Enum from typing import ( TYPE_CHECKING, Any, TypeVar, cast, overload, ) import strawberry from django.db import models, transaction from django.db.models.base import Model from django.db.models.fields import Field from django.db.models.fields.related import ManyToManyField from django.db.models.fields.reverse_related import ( ForeignObjectRel, ManyToManyRel, ManyToOneRel, OneToOneRel, ) from django.utils.functional import LazyObject from strawberry import UNSET, relay from strawberry_django.fields.types import ( ListInput, ManyToManyInput, ManyToOneInput, NodeInput, OneToManyInput, OneToOneInput, ) from strawberry_django.settings import strawberry_django_settings from strawberry_django.utils.inspect import get_model_fields from .types import ( FullCleanOptions, InputListTypes, ParsedObject, ParsedObjectList, ) if TYPE_CHECKING: from django.db.models.manager import ( BaseManager, ManyToManyRelatedManager, RelatedManager, ) from strawberry.types.info import Info _T = TypeVar("_T") _M = TypeVar("_M", bound=Model) def _parse_pk( value: ParsedObject | strawberry.ID | _M | None, model: type[_M], *, key_attr: str | None = None, ) -> tuple[_M | None, dict[str, Any] | None]: if value is None: return None, None if isinstance(value, Model): return value, None if isinstance(value, ParsedObject): return value.parse(model) if isinstance(value, dict): if key_attr is None: settings = strawberry_django_settings() key_attr = settings["DEFAULT_PK_FIELD_NAME"] if key_attr in value: obj_pk = value[key_attr] if obj_pk is not strawberry.UNSET: return model._default_manager.get(pk=obj_pk), value return None, value return model._default_manager.get(pk=value), None def _parse_data( info: Info, model: type[_M], value: Any, *, key_attr: str | None = None, full_clean: bool | FullCleanOptions = True, ): obj, data = _parse_pk(value, model, key_attr=key_attr) parsed_data = {} if data: for k, v in data.items(): if v is UNSET and k != key_attr: continue if isinstance(v, ParsedObject): if v.pk in {None, UNSET}: related_field = cast("Field", get_model_fields(model).get(k)) related_model = related_field.related_model v = create( # noqa: PLW2901 info, cast("type[Model]", related_model), v.data or {}, key_attr=key_attr, full_clean=full_clean, exclude_m2m=[related_field.name], ) elif isinstance(v.pk, models.Model) and v.data: v = update( # noqa: PLW2901 info, v.pk, v.data, key_attr=key_attr, full_clean=full_clean ) else: v = v.pk # noqa: PLW2901 if k == "through_defaults" or not obj or getattr(obj, k) != v: parsed_data[k] = v return obj, parsed_data @overload def parse_input( info: Info, data: dict[str, _T], *, key_attr: str | None = None, ) -> dict[str, _T]: ... @overload def parse_input( info: Info, data: list[_T], *, key_attr: str | None = None, ) -> list[_T]: ... @overload def parse_input( info: Info, data: relay.GlobalID, *, key_attr: str | None = None, ) -> relay.Node: ... @overload def parse_input( info: Info, data: Any, *, key_attr: str | None = None, ) -> Any: ... def parse_input( info: Info, data: Any, *, key_attr: str | None = None, ): if isinstance(data, dict): return {k: parse_input(info, v, key_attr=key_attr) for k, v in data.items()} if isinstance(data, list): return [parse_input(info, v, key_attr=key_attr) for v in data] if isinstance(data, relay.GlobalID): return data.resolve_node_sync(info, required=True) if isinstance(data, NodeInput): pk = cast( "Any", parse_input(info, getattr(data, "id", UNSET), key_attr=key_attr) ) parsed = {} for field in dataclasses.fields(data): if field.name == "id": continue parsed[field.name] = parse_input( info, getattr(data, field.name), key_attr=key_attr ) return ParsedObject( pk=pk, data=parsed or None, ) if isinstance(data, (OneToOneInput, OneToManyInput)): return ParsedObject( pk=parse_input(info, data.set, key_attr=key_attr), ) if isinstance(data, (ManyToOneInput, ManyToManyInput, ListInput)): d = getattr(data, "data", None) if dataclasses.is_dataclass(d): d = { f.name: parse_input(info, getattr(data, f.name), key_attr=key_attr) for f in dataclasses.fields(d) } return ParsedObjectList( add=cast( "list[InputListTypes]", parse_input(info, data.add, key_attr=key_attr) ), remove=cast( "list[InputListTypes]", parse_input(info, data.remove, key_attr=key_attr), ), set=cast( "list[InputListTypes]", parse_input(info, data.set, key_attr=key_attr) ), ) if isinstance(data, Enum): return data.value if dataclasses.is_dataclass(data): return { f.name: parse_input(info, getattr(data, f.name), key_attr=key_attr) for f in dataclasses.fields(data) } return data def prepare_create_update( *, info: Info, instance: Model, data: dict[str, Any], key_attr: str | None = None, full_clean: bool | FullCleanOptions = True, exclude_m2m: list[str] | None = None, ) -> tuple[ Model, dict[str, object], list[tuple[ManyToManyField | ForeignObjectRel, Any]], ]: """Prepare data for updates and creates. This method is a helper function for the create and update resolver methods. It's to prepare the data for updating or creating. """ model = instance.__class__ fields = get_model_fields(model) m2m: list[tuple[ManyToManyField | ForeignObjectRel, Any]] = [] direct_field_values: dict[str, object] = {} exclude_m2m = exclude_m2m or [] if dataclasses.is_dataclass(data): data = vars(data) for name, value in data.items(): field = fields.get(name) direct_field_value = True if field is None or value is UNSET: # Dont use these, fallback to model defaults. direct_field_value = False elif isinstance(field, models.FileField): if value is None and instance.pk is not None: # We want to reset the file field value when None was passed in the # input, but `FileField.save_form_data` ignores None values. In that # case we manually pass False which clears the file # (but only if the instance is already saved and we are updating it) value = False # noqa: PLW2901 elif isinstance(field, (ManyToManyField, ForeignObjectRel)): if name in exclude_m2m: continue # m2m will be processed later m2m.append((field, value)) direct_field_value = False elif isinstance(field, models.ForeignKey) and isinstance( value, # We are using str here because strawberry.ID can't be used for isinstance (ParsedObject, str), ): value, value_data = _parse_data( # noqa: PLW2901 info, cast("type[Model]", field.related_model), value, key_attr=key_attr, full_clean=full_clean, ) if value is None and not value_data: value = None # noqa: PLW2901 # If foreign object is not found, then create it elif value in (None, UNSET): # noqa: PLR6201 value = create( # noqa: PLW2901 info, field.related_model, value_data, key_attr=key_attr, full_clean=full_clean, ) # If foreign object does not need updating, then skip it elif isinstance(value_data, dict) and not value_data: pass else: update( info, value, value_data, full_clean=full_clean, key_attr=key_attr, ) if direct_field_value: # We want to return the direct fields for processing # sepperatly when we're creating objects. # You can see this in the create() function direct_field_values.update({name: value}) # Make sure you dont pass Many2Many and FileFields # to your update_field function. This will not work. update_field(info, instance, field, value) # type: ignore return instance, direct_field_values, m2m @overload def create( info: Info, model: type[_M], data: dict[str, Any], *, key_attr: str | None = None, full_clean: bool | FullCleanOptions = True, pre_save_hook: Callable[[_M], None] | None = None, exclude_m2m: list[str] | None = None, ) -> _M: ... @overload def create( info: Info, model: type[_M], data: list[dict[str, Any]], *, key_attr: str | None = None, full_clean: bool | FullCleanOptions = True, pre_save_hook: Callable[[_M], None] | None = None, exclude_m2m: list[str] | None = None, ) -> list[_M]: ... def create( info: Info, model: type[_M], data: dict[str, Any] | list[dict[str, Any]], *, key_attr: str | None = None, full_clean: bool | FullCleanOptions = True, pre_save_hook: Callable[[_M], None] | None = None, exclude_m2m: list[str] | None = None, ) -> list[_M] | _M: return _create( info, model._default_manager, data, key_attr=key_attr, full_clean=full_clean, pre_save_hook=pre_save_hook, exclude_m2m=exclude_m2m, ) @transaction.atomic def _create( info: Info, manager: BaseManager, data: dict[str, Any] | list[dict[str, Any]], *, key_attr: str | None = None, full_clean: bool | FullCleanOptions = True, pre_save_hook: Callable[[_M], None] | None = None, exclude_m2m: list[str] | None = None, ) -> list[_M] | _M: model = manager.model # Before creating your instance, verify this is not a bulk create # if so, add them one by one. Otherwise, get to work. if isinstance(data, list): return [ create( info, model, d, key_attr=key_attr, full_clean=full_clean, exclude_m2m=exclude_m2m, ) for d in data ] # Also, the approach below will use the manager to create the instance # rather than manually creating it. If you have a pre_save_hook # use the update method instead. if pre_save_hook: return update( info, model(), data, key_attr=key_attr, full_clean=full_clean, pre_save_hook=pre_save_hook, ) # We will use a dummy-instance to trigger form validation # However, this instance should not be saved as it will # circumvent the manager create method. dummy_instance = model() _, create_kwargs, m2m = prepare_create_update( info=info, instance=dummy_instance, data=data, full_clean=full_clean, key_attr=key_attr, exclude_m2m=exclude_m2m, ) # Creating the instance directly via create() without full-clean will # raise ugly error messages. To generate user-friendly ones, we want # full-clean() to trigger form-validation style error messages. full_clean_options = full_clean if isinstance(full_clean, dict) else {} if full_clean: dummy_instance.full_clean(**full_clean_options) # Create the instance using the manager create method to respect # manager create overrides. This also ensures support for proxy-models. instance = manager.create(**create_kwargs) for field, value in m2m: update_m2m(info, instance, field, value, key_attr) return instance @overload def update( info: Info, instance: _M, data: dict[str, Any], *, key_attr: str | None = None, full_clean: bool | FullCleanOptions = True, pre_save_hook: Callable[[_M], None] | None = None, exclude_m2m: list[str] | None = None, ) -> _M: ... @overload def update( info: Info, instance: Iterable[_M], data: dict[str, Any], *, key_attr: str | None = None, full_clean: bool | FullCleanOptions = True, pre_save_hook: Callable[[_M], None] | None = None, exclude_m2m: list[str] | None = None, ) -> list[_M]: ... @transaction.atomic def update( info: Info, instance: _M | Iterable[_M], data: dict[str, Any], *, key_attr: str | None = None, full_clean: bool | FullCleanOptions = True, pre_save_hook: Callable[[_M], None] | None = None, exclude_m2m: list[str] | None = None, ) -> _M | list[_M]: # Unwrap lazy objects since they have a proxy __iter__ method that will make # them iterables even if the wrapped object isn't if isinstance(instance, LazyObject): instance = cast("_M", instance.__reduce__()[1][0]) if isinstance(instance, Iterable): instances = list(instance) return [ update( info, instance, data, key_attr=key_attr, full_clean=full_clean, pre_save_hook=pre_save_hook, exclude_m2m=exclude_m2m, ) for instance in instances ] instance, _, m2m = prepare_create_update( info=info, instance=instance, data=data, key_attr=key_attr, full_clean=full_clean, exclude_m2m=exclude_m2m, ) if pre_save_hook is not None: pre_save_hook(instance) full_clean_options = full_clean if isinstance(full_clean, dict) else {} if full_clean: instance.full_clean(**full_clean_options) # type: ignore instance.save() if m2m: for field, value in m2m: update_m2m(info, instance, field, value, key_attr, full_clean) instance.refresh_from_db() return instance @overload def delete( info: Info, instance: _M, *, data: dict[str, Any] | None = None, ) -> _M: ... @overload def delete( info: Info, instance: Iterable[_M], *, data: dict[str, Any] | None = None, ) -> list[_M]: ... @transaction.atomic def delete(info: Info, instance: _M | Iterable[_M], *, data=None) -> _M | list[_M]: # Unwrap lazy objects since they have a proxy __iter__ method that will make # them iterables even if the wrapped object isn't if isinstance(instance, LazyObject): instance = cast("_M", instance.__reduce__()[1][0]) if isinstance(instance, Iterable): many = True instances = list(instance) else: many = False instances = [instance] for instance in instances: pk = instance.pk instance.delete() # After the instance is deleted, set its ID to the original database's # ID so that the success response contains ID of the deleted object. instance.pk = pk return instances if many else instances[0] def update_field(info: Info, instance: Model, field: models.Field, value: Any): if value is UNSET: return data = None if ( value and isinstance(field, models.ForeignObject) and not isinstance(value, Model) ): value, data = _parse_pk(value, cast("type[Model]", field.related_model)) field.save_form_data(instance, value) # If data was passed to the foreign key, update it recursively if data and value: update(info, value, data) def update_m2m( info: Info, instance: Model, field: ManyToManyField | ForeignObjectRel, value: Any, key_attr: str | None = None, full_clean: bool | FullCleanOptions = True, ): if value in (None, UNSET): # noqa: PLR6201 return # FIXME / NOTE: Should this be here? # The field can only be ManyToManyField | ForeignObjectRel according to the definition # so why are there checks for OneTOneRel? if isinstance(field, OneToOneRel): remote_field = field.remote_field value, data = _parse_pk(value, remote_field.model, key_attr=key_attr) if value is None: value = getattr(instance, field.name) else: remote_field.save_form_data(value, instance) value.save() # If data was passed to the field, update it recursively if data: update(info, value, data) return # END FIXME use_remove = True if isinstance(field, ManyToManyField): manager = cast("RelatedManager", getattr(instance, field.attname)) reverse_field_name = field.remote_field.related_name # type: ignore else: assert isinstance(field, (ManyToManyRel, ManyToOneRel)) accessor_name = field.get_accessor_name() reverse_field_name = field.field.name assert accessor_name manager = cast("RelatedManager", getattr(instance, accessor_name)) if field.one_to_many: # remove if field is nullable, otherwise delete use_remove = field.remote_field.null is True # Create a data dict containing the reference to the instance and exclude it from # nested m2m creation (to break circular references) ref_instance_data = {reverse_field_name: instance} exclude_m2m = [reverse_field_name] to_add = [] to_remove = [] to_delete = [] need_remove_cache = False full_clean_options = full_clean if isinstance(full_clean, dict) else {} values = value.set if isinstance(value, ParsedObjectList) else value if isinstance(values, list): if isinstance(value, ParsedObjectList) and getattr(value, "add", None): raise ValueError("'add' cannot be used together with 'set'") if isinstance(value, ParsedObjectList) and getattr(value, "remove", None): raise ValueError("'remove' cannot be used together with 'set'") existing = set(manager.all()) need_remove_cache = need_remove_cache or bool(values) for v in values: obj, data = _parse_data( info, cast("type[Model]", manager.model), v, key_attr=key_attr, full_clean=full_clean, ) if obj: data.pop(key_attr, None) through_defaults = data.pop("through_defaults", {}) if data: for k, inner_value in data.items(): setattr(obj, k, inner_value) if full_clean: obj.full_clean(**full_clean_options) obj.save() if hasattr(manager, "through"): manager = cast("ManyToManyRelatedManager", manager) intermediate_model = manager.through try: im = intermediate_model._default_manager.get( **{ manager.source_field_name: instance, # type: ignore manager.target_field_name: obj, # type: ignore }, ) except intermediate_model.DoesNotExist: im = intermediate_model( **{ manager.source_field_name: instance, # type: ignore manager.target_field_name: obj, # type: ignore }, ) for k, inner_value in through_defaults.items(): setattr(im, k, inner_value) if full_clean: im.full_clean(**full_clean_options) im.save() elif obj not in existing: to_add.append(obj) existing.discard(obj) else: # If we've reached here, the key_attr should be UNSET or missing. So # let's remove it if it is there. data.pop(key_attr, None) obj = _create( info, manager, data | ref_instance_data, key_attr=key_attr, full_clean=full_clean, exclude_m2m=exclude_m2m, ) existing.discard(obj) for remaining in existing: if use_remove: to_remove.append(remaining) else: to_delete.append(remaining) else: need_remove_cache = need_remove_cache or bool(value.add) for v in value.add or []: obj, data = _parse_data( info, cast("type[Model]", manager.model), v, key_attr=key_attr, full_clean=full_clean, ) if obj and data: data.pop(key_attr, None) if full_clean: obj.full_clean(**full_clean_options) manager.add(obj, **data) elif obj: # Do this later in a bulk data.pop(key_attr, None) to_add.append(obj) elif data: # If we've reached here, the key_attr should be UNSET or missing. So # let's remove it if it is there. data.pop(key_attr, None) _create( info, manager, data | ref_instance_data, key_attr=key_attr, full_clean=full_clean, exclude_m2m=exclude_m2m, ) else: raise AssertionError need_remove_cache = need_remove_cache or bool(value.remove) for v in value.remove or []: obj, data = _parse_data( info, cast("type[Model]", manager.model), v, key_attr=key_attr, full_clean=full_clean, ) data.pop(key_attr, None) assert not data to_remove.append(obj) if to_add: manager.add(*to_add) if to_remove: manager.remove(*to_remove) if to_delete: manager.filter(pk__in=[item.pk for item in to_delete]).delete() if need_remove_cache: manager._remove_prefetched_objects() # type: ignore strawberry-graphql-django-0.62.0/strawberry_django/mutations/types.py000066400000000000000000000022431502405145400261670ustar00rootroot00000000000000from __future__ import annotations import dataclasses from typing import Any, TypeVar, Union import strawberry from django.db import models from django.db.models import Model from strawberry import UNSET from typing_extensions import TypeAlias, TypedDict _T = TypeVar("_T") # noqa: PYI018 _M = TypeVar("_M", bound=Model) InputListTypes: TypeAlias = Union[strawberry.ID, "ParsedObject"] class FullCleanOptions(TypedDict, total=False): exclude: list[str] validate_unique: bool validate_constraints: bool @dataclasses.dataclass class ParsedObject: pk: strawberry.ID | Model | None data: dict[str, Any] | None = None def parse(self, model: type[_M]) -> tuple[_M | None, dict[str, Any] | None]: if self.pk is None or self.pk is UNSET: return None, self.data if isinstance(self.pk, models.Model): assert isinstance(self.pk, model) return self.pk, self.data return model._default_manager.get(pk=self.pk), self.data @dataclasses.dataclass class ParsedObjectList: add: list[InputListTypes] | None = None remove: list[InputListTypes] | None = None set: list[InputListTypes] | None = None strawberry-graphql-django-0.62.0/strawberry_django/optimizer.py000066400000000000000000001567101502405145400250330ustar00rootroot00000000000000from __future__ import annotations import contextlib import contextvars import copy import dataclasses import itertools from collections import Counter from collections.abc import Callable from typing import ( TYPE_CHECKING, Any, Optional, TypeVar, cast, ) from django.db import models from django.db.models import Prefetch from django.db.models.constants import LOOKUP_SEP from django.db.models.expressions import BaseExpression, Combinable from django.db.models.fields.reverse_related import ( ManyToManyRel, ManyToOneRel, OneToOneRel, ) from django.db.models.manager import BaseManager from django.db.models.query import QuerySet from graphql import ( FieldNode, GraphQLInterfaceType, GraphQLObjectType, GraphQLOutputType, GraphQLWrappingType, get_argument_values, ) from graphql.execution.collect_fields import collect_sub_fields from graphql.language.ast import OperationType from graphql.type.definition import GraphQLResolveInfo, get_named_type from strawberry import relay from strawberry.extensions import SchemaExtension from strawberry.relay.utils import SliceMetadata from strawberry.schema.schema import Schema from strawberry.schema.schema_converter import get_arguments from strawberry.types import get_object_definition, has_object_definition from strawberry.types.base import StrawberryContainer from strawberry.types.info import Info from strawberry.types.object_type import StrawberryObjectDefinition from typing_extensions import assert_never, assert_type from strawberry_django.fields.types import resolve_model_field_name from strawberry_django.pagination import OffsetPaginated, apply_window_pagination from strawberry_django.queryset import get_queryset_config, run_type_get_queryset from strawberry_django.relay.list_connection import DjangoListConnection from strawberry_django.resolvers import django_fetch from .descriptors import ModelProperty from .utils.inspect import ( PrefetchInspector, get_model_field, get_model_fields, get_possible_concrete_types, get_possible_type_definitions, is_inheritance_manager, is_inheritance_qs, is_polymorphic_model, ) from .utils.typing import ( AnnotateCallable, AnnotateType, PrefetchCallable, PrefetchType, TypeOrMapping, TypeOrSequence, WithStrawberryDjangoObjectDefinition, get_django_definition, has_django_definition, unwrap_type, ) if TYPE_CHECKING: from collections.abc import Generator from django.contrib.contenttypes.fields import GenericRelation from strawberry.types.execution import ExecutionContext from strawberry.types.field import StrawberryField from strawberry.utils.await_maybe import AwaitableOrValue __all__ = [ "DjangoOptimizerExtension", "OptimizerConfig", "OptimizerStore", "PrefetchType", "optimize", ] _M = TypeVar("_M", bound=models.Model) _sentinel = object() _annotate_placeholder = "__annotated_placeholder__" @dataclasses.dataclass class OptimizerConfig: """Django optimization configuration. Attributes ---------- enable_only: Enable `QuerySet.only` optimizations enable_select_related: Enable `QuerySet.select_related` optimizations enable_prefetch_related: Enable `QuerySet.prefetch_related` optimizations enable_annotate: Enable `QuerySet.annotate` optimizations enable_nested_relations_prefetch: Enable prefetch of nested relations optimizations. prefetch_custom_queryset: Use custom instead of _base_manager for prefetch querysets """ enable_only: bool = dataclasses.field(default=True) enable_select_related: bool = dataclasses.field(default=True) enable_prefetch_related: bool = dataclasses.field(default=True) enable_annotate: bool = dataclasses.field(default=True) enable_nested_relations_prefetch: bool = dataclasses.field(default=True) prefetch_custom_queryset: bool = dataclasses.field(default=False) @dataclasses.dataclass class OptimizerStore: """Django optimization store. Attributes ---------- only: Set of values to optimize using `QuerySet.only` selected: Set of values to optimize using `QuerySet.select_related` prefetch_related: Set of values to optimize using `QuerySet.prefetch_related` annotate: Dict of values to use in `QuerySet.annotate` """ only: list[str] = dataclasses.field(default_factory=list) select_related: list[str] = dataclasses.field(default_factory=list) prefetch_related: list[PrefetchType] = dataclasses.field(default_factory=list) annotate: dict[str, AnnotateType] = dataclasses.field(default_factory=dict) def __bool__(self): return any( [self.only, self.select_related, self.prefetch_related, self.annotate], ) def __ior__(self, other: OptimizerStore): self.only.extend(other.only) self.select_related.extend(other.select_related) self.prefetch_related.extend(other.prefetch_related) self.annotate.update(other.annotate) return self def __or__(self, other: OptimizerStore): new = self.copy() new |= other return new def copy(self): """Create a shallow copy of the store.""" return self.__class__( only=self.only[:], select_related=self.select_related[:], prefetch_related=self.prefetch_related[:], annotate=self.annotate.copy(), ) @classmethod def with_hints( cls, *, only: TypeOrSequence[str] | None = None, select_related: TypeOrSequence[str] | None = None, prefetch_related: TypeOrSequence[PrefetchType] | None = None, annotate: TypeOrMapping[AnnotateType] | None = None, ): """Create a new store with the given hints.""" return cls( only=[only] if isinstance(only, str) else list(only or []), select_related=( [select_related] if isinstance(select_related, str) else list(select_related or []) ), prefetch_related=( [prefetch_related] if isinstance(prefetch_related, (str, Prefetch, Callable)) else list(prefetch_related or []) ), annotate=( # placeholder here, # because field name is evaluated later on .annotate call: {_annotate_placeholder: annotate} if isinstance(annotate, (BaseExpression, Combinable, Callable)) else dict(annotate or {}) ), ) def with_resolved_callables(self, info: GraphQLResolveInfo): """Resolve any prefetch/annotate callables using the provided info and return a new store. This is used to resolve callables using the correct info object, scoped to their respective fields. """ if not any(callable(p) for p in self.prefetch_related) and not any( callable(a) for a in self.annotate.values() ): return self prefetch_related: list[PrefetchType] = [ p(info) if callable(p) else p for p in self.prefetch_related ] annotate: dict[str, AnnotateType] = { label: annotation(info) if callable(annotation) else annotation for label, annotation in self.annotate.items() } return self.__class__( only=self.only, select_related=self.select_related, prefetch_related=prefetch_related, annotate=annotate, ) def with_prefix(self, prefix: str, *, info: GraphQLResolveInfo): """Create a copy of this store with the given prefix. This is useful when we need to apply the same store to a nested field. `prefix` will be prepended to all fields in the store. Any callables will be resolved, just like with_resolved_callables, to apply the prefix to their results. """ prefetch_related = [] for p in self.prefetch_related: if isinstance(p, Callable): assert_type(p, PrefetchCallable) p = p(info) # noqa: PLW2901 if isinstance(p, str): prefetch_related.append(f"{prefix}{LOOKUP_SEP}{p}") elif isinstance(p, Prefetch): # add_prefix modifies the field's prefetch object, so we copy it before p_copy = copy.copy(p) p_copy.add_prefix(prefix) prefetch_related.append(p_copy) else: # pragma:nocover assert_never(p) annotate = {} for k, v in self.annotate.items(): if isinstance(v, Callable): assert_type(v, AnnotateCallable) v = v(info) # noqa: PLW2901 annotate[f"{prefix}{LOOKUP_SEP}{k}"] = v return self.__class__( only=[f"{prefix}{LOOKUP_SEP}{i}" for i in self.only], select_related=[f"{prefix}{LOOKUP_SEP}{i}" for i in self.select_related], prefetch_related=prefetch_related, annotate=annotate, ) def apply( self, qs: QuerySet[_M], *, info: GraphQLResolveInfo, config: OptimizerConfig | None = None, ) -> QuerySet[_M]: """Apply this store optimizations to the given queryset.""" config = config or OptimizerConfig() qs = self._apply_prefetch_related( qs, info=info, config=config, ) qs, extra_only_set = self._apply_select_related( qs, info=info, config=config, ) qs = self._apply_only( qs, info=info, config=config, extra_only_set=extra_only_set, ) qs = self._apply_annotate( qs, info=info, config=config, ) return qs # noqa: RET504 def _apply_prefetch_related( self, qs: QuerySet[_M], *, info: GraphQLResolveInfo, config: OptimizerConfig, ) -> QuerySet[_M]: if not config.enable_prefetch_related or not self.prefetch_related: return qs abort_only = set() prefetch_lists = [ qs._prefetch_related_lookups, # type: ignore self.prefetch_related, ] # Add all str at the same time to make it easier to handle Prefetch below to_prefetch: dict[str, str | Prefetch] = { p: p for p in itertools.chain(*prefetch_lists) if isinstance(p, str) } # Merge already existing prefetches together for p in itertools.chain(*prefetch_lists): # Already added above if isinstance(p, str): continue if isinstance(p, Callable): assert_type(p, PrefetchCallable) p = p(info) # noqa: PLW2901 path = p.prefetch_to existing = to_prefetch.get(path) # The simplest case. The prefetch doesn't exist or is a string. # In this case, just replace it. if not existing or isinstance(existing, str): to_prefetch[path] = p if isinstance(existing, str): abort_only.add(path) continue p1 = PrefetchInspector(existing) p2 = PrefetchInspector(p) if getattr(existing, "_optimizer_sentinel", None) is _sentinel: ret = p1.merge(p2, allow_unsafe_ops=True) elif getattr(p, "_optimizer_sentinel", None) is _sentinel: ret = p2.merge(p1, allow_unsafe_ops=True) else: # The order here doesn't matter ret = p1.merge(p2) to_prefetch[path] = ret.prefetch # Abort only optimization if one prefetch related was made for everything for ao in abort_only: # cast is safe as the loop above only adds Prefetch instances as abort_only prefetch = cast("Prefetch", to_prefetch[ao]) if prefetch.queryset is not None: # type: ignore - queryset can be None prefetch.queryset.query.deferred_loading = ( set(), True, ) # First prefetch_related(None) to clear all existing prefetches, and then # add ours, which also contains them. This is to avoid the # "lookup was already seen with a different queryset" error return qs.prefetch_related(None).prefetch_related(*to_prefetch.values()) def _apply_select_related( self, qs: QuerySet[_M], *, info: GraphQLResolveInfo, config: OptimizerConfig, ) -> tuple[QuerySet[_M], set[str]]: only_set = set(self.only) extra_only_set = set() select_related_set = set(self.select_related) # inspect the queryset to find any existing select_related fields def get_related_fields_with_prefix( queryset_select_related: dict[str, Any], prefix: str = "", ): for parent, nested in queryset_select_related.items(): current_path = f"{prefix}{parent}" yield current_path if nested: # If there are nested relations, dive deeper yield from get_related_fields_with_prefix( nested, prefix=f"{current_path}{LOOKUP_SEP}", ) if isinstance(qs.query.select_related, dict): select_related_set.update( get_related_fields_with_prefix(qs.query.select_related) ) if config.enable_select_related and select_related_set: qs = qs.select_related(*select_related_set) # Update our extra_select_related_only_set with the fields that were # selected by select_related to make sure they actually get selected for select_related in select_related_set: if select_related in only_set: continue if not any(only.startswith(select_related) for only in only_set): extra_only_set.add(select_related) return qs, extra_only_set def _apply_only( self, qs: QuerySet[_M], *, info: GraphQLResolveInfo, config: OptimizerConfig, extra_only_set: set[str], ) -> QuerySet[_M]: only_set = set(self.only) | extra_only_set if config.enable_only and only_set: qs = qs.only(*only_set) return qs def _apply_annotate( self, qs: QuerySet[_M], *, info: GraphQLResolveInfo, config: OptimizerConfig, ) -> QuerySet[_M]: if not config.enable_annotate or not self.annotate: return qs to_annotate = {} for k, v in self.annotate.items(): if isinstance(v, Callable): assert_type(v, AnnotateCallable) v = v(info) # noqa: PLW2901 to_annotate[k] = v return qs.annotate(**to_annotate) def _get_django_type( field: StrawberryField, ) -> type[WithStrawberryDjangoObjectDefinition] | None: f_type = unwrap_type(field.type) return f_type if has_django_definition(f_type) else None def _get_prefetch_queryset( remote_model: type[models.Model], schema: Schema, field: StrawberryField, parent_type: GraphQLObjectType | GraphQLInterfaceType, field_node: FieldNode, *, config: OptimizerConfig | None, info: GraphQLResolveInfo, related_field_id: str | None = None, ) -> QuerySet: # We usually want to use the `_base_manager` for prefetching, as it is what django # itself states we should be using: # https://docs.djangoproject.com/en/5.0/topics/db/managers/#base-managers # But in case prefetch_custom_queryset is enabled, we use the custom queryset # from _default_manager instead. if config and config.prefetch_custom_queryset: qs = remote_model._default_manager.all() else: qs = remote_model._base_manager.all() # type: ignore if f_type := _get_django_type(field): qs = run_type_get_queryset( qs, f_type, info=Info( _raw_info=info, _field=field, ), ) return _optimize_prefetch_queryset( qs, schema, field, parent_type, field_node, config=config, info=info, related_field_id=related_field_id, ) def _optimize_prefetch_queryset( qs: QuerySet[_M], schema: Schema, field: StrawberryField, parent_type: GraphQLObjectType | GraphQLInterfaceType, field_node: FieldNode, *, config: OptimizerConfig | None, info: GraphQLResolveInfo, related_field_id: str | None = None, ) -> QuerySet[_M]: from strawberry_django.fields.field import ( StrawberryDjangoConnectionExtension, StrawberryDjangoField, ) from strawberry_django.relay.cursor_connection import ( DjangoCursorConnection, apply_cursor_pagination, ) if ( not config or not config.enable_nested_relations_prefetch or related_field_id is None or not isinstance(field, StrawberryDjangoField) or is_optimized_by_prefetching(qs) ): return qs mark_optimized = True strawberry_schema = cast("Schema", info.schema._strawberry_schema) # type: ignore field_name = strawberry_schema.config.name_converter.from_field(field) field_info = Info( _raw_info=info, _field=field, ) _field_args, field_kwargs = get_arguments( field=field, source=None, info=field_info, kwargs=get_argument_values( parent_type.fields[field_name], field_node, info.variable_values, ), config=strawberry_schema.config, scalar_registry=strawberry_schema.schema_converter.scalar_registry, ) field_kwargs.pop("info", None) # Disable the optimizer to avoid doing double optimization while running get_queryset with DjangoOptimizerExtension.disabled(): qs = field.get_queryset( qs, field_info, _strawberry_related_field_id=related_field_id, **field_kwargs, ) connection_extension = next( ( e for e in field.extensions if isinstance(e, StrawberryDjangoConnectionExtension) ), None, ) if connection_extension is not None: connection_type_def = get_object_definition( connection_extension.connection_type, strict=True, ) connection_type = ( connection_type_def.concrete_of and connection_type_def.concrete_of.origin ) if ( connection_type is relay.ListConnection or connection_type is DjangoListConnection ): slice_metadata = SliceMetadata.from_arguments( Info(_raw_info=info, _field=field), first=field_kwargs.get("first"), last=field_kwargs.get("last"), before=field_kwargs.get("before"), after=field_kwargs.get("after"), ) qs = apply_window_pagination( qs, related_field_id=related_field_id, offset=slice_metadata.start, limit=slice_metadata.end - slice_metadata.start, max_results=connection_extension.max_results, ) elif connection_type is DjangoCursorConnection: qs, _ = apply_cursor_pagination( qs, related_field_id=related_field_id, info=Info(_raw_info=info, _field=field), first=field_kwargs.get("first"), last=field_kwargs.get("last"), before=field_kwargs.get("before"), after=field_kwargs.get("after"), max_results=connection_extension.max_results, ) else: mark_optimized = False if isinstance(field.type, type) and issubclass(field.type, OffsetPaginated): pagination = field_kwargs.get("pagination") qs = apply_window_pagination( qs, related_field_id=related_field_id, offset=pagination.offset if pagination else 0, limit=pagination.limit if pagination else -1, ) if mark_optimized: qs = mark_optimized_by_prefetching(qs) return qs def _get_selections( info: GraphQLResolveInfo, parent_type: GraphQLObjectType | GraphQLInterfaceType, ) -> dict[str, list[FieldNode]]: return collect_sub_fields( info.schema, info.fragments, info.variable_values, cast("GraphQLObjectType", parent_type), info.field_nodes, ) def _generate_selection_resolve_info( info: GraphQLResolveInfo, field_nodes: list[FieldNode], return_type: GraphQLOutputType, parent_type: GraphQLObjectType | GraphQLInterfaceType, ): field_node = field_nodes[0] return GraphQLResolveInfo( field_name=field_node.name.value, field_nodes=field_nodes, return_type=return_type, parent_type=cast("GraphQLObjectType", parent_type), path=info.path.add_key(0).add_key(field_node.name.value, parent_type.name), schema=info.schema, fragments=info.fragments, root_value=info.root_value, operation=info.operation, variable_values=info.variable_values, context=info.context, is_awaitable=info.is_awaitable, ) def _get_field_data( selections: list[FieldNode], object_definition: StrawberryObjectDefinition, schema: Schema, *, parent_type: GraphQLObjectType | GraphQLInterfaceType, info: GraphQLResolveInfo, ) -> tuple[StrawberryField, GraphQLObjectType, FieldNode, GraphQLResolveInfo] | None: selection = selections[0] field_name = selection.name.value for field in object_definition.fields: if schema.config.name_converter.get_graphql_name(field) == field_name: break else: return None # Do not optimize the field if the user asked not to if getattr(field, "disable_optimization", False): return None definition = parent_type.fields[selection.name.value].type while isinstance(definition, GraphQLWrappingType): definition = definition.of_type field_info = _generate_selection_resolve_info( info, selections, definition, parent_type, ) return field, definition, selection, field_info def _get_hints_from_field( field: StrawberryField, *, f_info: GraphQLResolveInfo, prefix: str = "", ) -> OptimizerStore | None: if not ( field_store := cast("Optional[OptimizerStore]", getattr(field, "store", None)) ): return None if len(field_store.annotate) == 1 and _annotate_placeholder in field_store.annotate: # This is a special case where we need to update the field name, # because when field_store was created on __init__, # the field name wasn't available. # This allows for annotate expressions to be declared as: # total: int = gql.django.field(annotate=Sum("price")) # noqa: ERA001 # Instead of the more redundant: # total: int = gql.django.field(annotate={"total": Sum("price")}) # noqa: ERA001 field_store.annotate = { field.name: field_store.annotate[_annotate_placeholder], } # with_prefix also resolves callables, so we only need one or the other return ( field_store.with_prefix(prefix, info=f_info) if prefix else field_store.with_resolved_callables(f_info) ) def _get_hints_from_model_property( field: StrawberryField, model: type[models.Model], *, f_info: GraphQLResolveInfo, prefix: str = "", ) -> OptimizerStore | None: model_attr = getattr(model, field.python_name, None) if ( model_attr is not None and isinstance(model_attr, ModelProperty) and model_attr.store ): attr_store = model_attr.store # with_prefix also resolves callables, so we only need one or the other store = ( attr_store.with_prefix(prefix, info=f_info) if prefix else attr_store.with_resolved_callables(f_info) ) else: store = None return store def _must_use_prefetch_related( config: OptimizerConfig, field: StrawberryField, model_field: models.ForeignKey | OneToOneRel, ) -> bool: f_type = _get_django_type(field) # - If the field has a get_queryset method, use Prefetch so it will be respected # - If the model is using django-polymorphic, # use Prefetch so its custom queryset will be used, returning polymorphic models return ( (f_type and hasattr(f_type, "get_queryset")) or is_polymorphic_model(model_field.related_model) or is_inheritance_manager( model_field.related_model._default_manager if config.prefetch_custom_queryset else model_field.related_model._base_manager # type: ignore ) ) def _get_hints_from_django_foreign_key( field: StrawberryField, field_definition: GraphQLObjectType, field_selection: FieldNode, model_field: models.ForeignKey | OneToOneRel, model_fieldname: str, schema: Schema, *, config: OptimizerConfig, parent_type: GraphQLObjectType | GraphQLInterfaceType, field_info: GraphQLResolveInfo, path: str, cache: dict[type[models.Model], list[tuple[int, OptimizerStore]]], level: int = 0, ) -> OptimizerStore: if _must_use_prefetch_related(config, field, model_field): store = _get_hints_from_django_relation( field, field_selection=field_selection, model_field=model_field, model_fieldname=model_fieldname, schema=schema, config=config, parent_type=parent_type, field_info=field_info, path=path, cache=cache, level=level, ) store.only.append(path) return store store = OptimizerStore.with_hints( only=[path], select_related=[path], ) # If adding a reverse relation, make sure to select its pointer to us, # or else this might causa a refetch from the database if isinstance(model_field, OneToOneRel): remote_field = model_field.remote_field store.only.append( f"{path}{LOOKUP_SEP}{resolve_model_field_name(remote_field)}", ) for f_type_def in get_possible_type_definitions(field.type): f_model = model_field.related_model f_store = _get_model_hints( f_model, schema, f_type_def, parent_type=field_definition, info=field_info, config=config, cache=cache, level=level + 1, ) if f_store is not None: cache.setdefault(f_model, []).append((level, f_store)) store |= f_store.with_prefix(path, info=field_info) return store def _get_hints_from_django_relation( field: StrawberryField, field_selection: FieldNode, model_field: ( models.ManyToManyField | ManyToManyRel | ManyToOneRel | GenericRelation | OneToOneRel | models.ForeignKey ), model_fieldname: str, schema: Schema, *, config: OptimizerConfig, parent_type: GraphQLObjectType | GraphQLInterfaceType, field_info: GraphQLResolveInfo, path: str, cache: dict[type[models.Model], list[tuple[int, OptimizerStore]]], level: int = 0, ) -> OptimizerStore: try: from django.contrib.contenttypes.fields import GenericRelation except (ImportError, RuntimeError): # pragma: no cover GenericRelation = None # noqa: N806 store = OptimizerStore() f_types = list(get_possible_type_definitions(field.type)) if len(f_types) > 1: # This might be a generic foreign key. # In this case, just prefetch it store.prefetch_related.append(model_fieldname) return store field_store = getattr(field, "store", None) if field_store and field_store.prefetch_related: # Skip optimization if 'prefetch_related' is present in the field's store. # This is necessary because 'prefetch_related' likely modifies the queryset # with filtering or annotating, making the optimization redundant and # potentially causing an extra unused query. return store remote_field = model_field.remote_field remote_model = remote_field.model field_store = None f_type = f_types[0] subclasses = [] for concrete_field_type in get_possible_concrete_types( remote_model, schema, f_type ): django_definition = get_django_definition(concrete_field_type.origin) if ( django_definition and django_definition.model != remote_model and not django_definition.model._meta.abstract and issubclass(django_definition.model, remote_model) ): subclasses.append(django_definition.model) concrete_store = _get_model_hints( remote_model, schema, concrete_field_type, parent_type=_get_gql_definition(schema, concrete_field_type), info=field_info, config=config, cache=cache, level=level + 1, ) if concrete_store is not None: field_store = ( concrete_store if field_store is None else field_store | concrete_store ) if field_store is None: return store related_field_id = getattr(remote_field, "attname", None) or getattr( remote_field, "name", None ) if ( config.enable_only and field_store.only and not isinstance(remote_field, ManyToManyRel) ): # If adding a reverse relation, make sure to select its # pointer to us, or else this might causa a refetch from # the database if GenericRelation is not None and isinstance( model_field, GenericRelation, ): field_store.only.append(model_field.object_id_field_name) field_store.only.append(model_field.content_type_field_name) elif related_field_id is not None: field_store.only.append(related_field_id) path_lookup = f"{path}{LOOKUP_SEP}" if store.only and field_store.only: extra_only = [o for o in store.only or [] if o.startswith(path_lookup)] store.only = [o for o in store.only if o not in extra_only] field_store.only.extend(o[len(path_lookup) :] for o in extra_only) if store.select_related and field_store.select_related: extra_sr = [o for o in store.select_related or [] if o.startswith(path_lookup)] store.select_related = [o for o in store.select_related if o not in extra_sr] field_store.select_related.extend(o[len(path_lookup) :] for o in extra_sr) cache.setdefault(remote_model, []).append((level, field_store)) base_qs = _get_prefetch_queryset( remote_model, schema, field, parent_type, field_selection, config=config, info=field_info, related_field_id=related_field_id, ) if is_inheritance_qs(base_qs): base_qs = base_qs.select_subclasses(*subclasses) field_qs = field_store.apply(base_qs, info=field_info, config=config) field_prefetch = Prefetch(path, queryset=field_qs) field_prefetch._optimizer_sentinel = _sentinel # type: ignore store.prefetch_related.append(field_prefetch) return store def _get_hints_from_django_field( field: StrawberryField, field_definition: GraphQLObjectType, field_selection: FieldNode, model: type[models.Model], schema: Schema, *, config: OptimizerConfig, parent_type: GraphQLObjectType | GraphQLInterfaceType, field_info: GraphQLResolveInfo, prefix: str = "", cache: dict[type[models.Model], list[tuple[int, OptimizerStore]]], level: int = 0, ) -> OptimizerStore | None: try: from django.contrib.contenttypes.fields import ( GenericForeignKey, GenericRelation, ) except (ImportError, RuntimeError): # pragma: no cover GenericForeignKey = None # noqa: N806 GenericRelation = None # noqa: N806 relation_fields = (models.ManyToManyField, ManyToManyRel, ManyToOneRel) else: relation_fields = ( models.ManyToManyField, ManyToManyRel, ManyToOneRel, GenericRelation, ) # If the field has a base resolver, don't try to optimize it. The user should # be defining custom hints in this case, which should already be in the store # GlobalID and special cases setting `can_optimize` are ok though, as those resolvers # are auto generated by us if ( field.base_resolver is not None and field.type != relay.GlobalID and not getattr(field.base_resolver.wrapped_func, "can_optimize", False) ): return None model_fieldname: str = getattr(field, "django_name", None) or field.python_name if (model_field := get_model_field(model, model_fieldname)) is None: return None lookup_prefix = prefix + LOOKUP_SEP if prefix else "" path = f"{lookup_prefix}{model_fieldname}" if isinstance(model_field, (models.ForeignKey, OneToOneRel)): store = _get_hints_from_django_foreign_key( field, field_definition=field_definition, field_selection=field_selection, model_field=model_field, model_fieldname=model_fieldname, schema=schema, config=config, parent_type=parent_type, field_info=field_info, path=path, cache=cache, level=level, ) elif GenericForeignKey and isinstance(model_field, GenericForeignKey): # There's not much we can do to optimize generic foreign keys regarding # only/select_related because they can be anything. # Just prefetch_related them store = OptimizerStore.with_hints(prefetch_related=[model_fieldname]) elif isinstance(model_field, relation_fields): store = _get_hints_from_django_relation( field, field_selection=field_selection, model_field=model_field, model_fieldname=model_fieldname, schema=schema, config=config, parent_type=parent_type, field_info=field_info, path=path, cache=cache, level=level, ) else: store = OptimizerStore.with_hints(only=[path]) return store def _get_model_hints( model: type[models.Model], schema: Schema, object_definition: StrawberryObjectDefinition, *, parent_type: GraphQLObjectType | GraphQLInterfaceType, info: GraphQLResolveInfo, config: OptimizerConfig | None = None, prefix: str = "", cache: dict[type[models.Model], list[tuple[int, OptimizerStore]]] | None = None, level: int = 0, subclass_collection: set[type[models.Model]] | None = None, ) -> OptimizerStore | None: cache = cache or {} # In case this is a relay field, the selected fields are inside edges -> node selection if issubclass(object_definition.origin, relay.Connection): return _get_model_hints_from_connection( model, schema, object_definition, parent_type=parent_type, info=info, config=config, prefix=prefix, cache=cache, level=level, subclass_collection=subclass_collection, ) # In case this is a Paginated field, the selected fields are inside results selection if issubclass(object_definition.origin, OffsetPaginated): return _get_model_hints_from_paginated( model, schema, object_definition, parent_type=parent_type, info=info, config=config, prefix=prefix, cache=cache, level=level, subclass_collection=subclass_collection, ) store = OptimizerStore() config = config or OptimizerConfig() dj_definition = get_django_definition(object_definition.origin) if dj_definition is None or dj_definition.disable_optimization: return None if not issubclass(model, dj_definition.model): # If this is a PolymorphicModel, also try to optimize fields in subclasses # of the current model. if not dj_definition.model._meta.abstract and issubclass( dj_definition.model, model ): if subclass_collection is not None: subclass_collection.add(dj_definition.model) if is_polymorphic_model(model): # These must be prefixed with app_label__ModelName___ (note three underscores) # This is a special syntax for django-polymorphic: # https://django-polymorphic.readthedocs.io/en/stable/advanced.html#polymorphic-filtering-for-fields-in-inherited-classes # "prefix" however is written in terms of not including the final LOOKUP_SEP (i.e. "__") # So we don't include the final __ here. return _get_model_hints( dj_definition.model, schema, object_definition, parent_type=parent_type, info=info, config=config, prefix=f"{prefix}{dj_definition.model._meta.app_label}__{dj_definition.model._meta.model_name}_", ) if is_inheritance_manager(model._default_manager) and ( path_from_parent := dj_definition.model._meta.get_path_from_parent( model ) ): prefix = LOOKUP_SEP.join( p.join_field.get_accessor_name() for p in path_from_parent ) return _get_model_hints( dj_definition.model, schema, object_definition, parent_type=parent_type, info=info, config=config, prefix=prefix, ) return None dj_type_store = getattr(dj_definition, "store", None) if dj_type_store: store |= dj_type_store lookup_prefix = prefix + LOOKUP_SEP if prefix else "" # Make sure that the model's pk is always selected when using only pk = model._meta.pk if pk is not None: store.only.append(lookup_prefix + pk.attname) # If this is a polymorphic Model, make sure to select its content type if is_polymorphic_model(model): store.only.extend( lookup_prefix + f for f in model.polymorphic_internal_model_fields ) selections = [ field_data for f_selection in _get_selections(info, parent_type).values() if ( field_data := _get_field_data( f_selection, object_definition, schema, parent_type=parent_type, info=info, ) ) is not None ] fields_counter = Counter(field_data[0] for field_data in selections) for field, f_definition, f_selection, f_info in selections: # If a field is selected more than once in the query, that means it is being # aliased. In this case, optimizing it would make one query to affect the other, # resulting in wrong results for both. if fields_counter[field] > 1: continue # Add annotations from the field if they exist if field_store := _get_hints_from_field(field, f_info=f_info, prefix=prefix): store |= field_store # Then from the model property if one is defined if model_property_store := _get_hints_from_model_property( field, model, f_info=f_info, prefix=prefix, ): store |= model_property_store # Lastly, from the django field itself if model_field_store := _get_hints_from_django_field( field, f_definition, f_selection, model, schema, config=config, parent_type=parent_type, field_info=f_info, prefix=prefix, cache=cache, level=level, ): store |= model_field_store # Django keeps track of known fields. That means that if one model select_related or # prefetch_related another one, and later another one select_related or # prefetch_related the model again, if the used fields there where not optimized in # this call django would have to fetch those again. By mergint those with us we are # making sure to avoid that for inner_level, inner_store in cache.get(model, []): if inner_level > level and inner_store: # We only want the only/select_related from this. prefetch_related is # something else store.only.extend(inner_store.only) store.select_related.extend(inner_store.select_related) # In case we skipped optimization for a relation, we might end up with a new QuerySet # which would not select its parent relation field on `.only()`, causing n+1 issues. # Make sure that in this case we also select it. if level == 0 and store.only and info.path.prev: own_fk_fields = [ field for field in get_model_fields(model).values() if isinstance(field, models.ForeignKey) ] path = info.path while path := path.prev: type_ = schema.get_type_by_name(path.typename) if not isinstance(type_, StrawberryObjectDefinition): continue if not (strawberry_django_type := get_django_definition(type_.origin)): continue for field in own_fk_fields: if field.related_model is strawberry_django_type.model: store.only.append(field.attname) break return store def _get_gql_definition( schema: Schema, definition: StrawberryObjectDefinition, ) -> GraphQLInterfaceType | GraphQLObjectType: if definition.is_interface: return schema.schema_converter.from_interface(definition) return schema.schema_converter.from_object(definition) def _get_model_hints_from_connection( model: type[models.Model], schema: Schema, object_definition: StrawberryObjectDefinition, *, parent_type: GraphQLObjectType | GraphQLInterfaceType, info: GraphQLResolveInfo, config: OptimizerConfig | None = None, prefix: str = "", cache: dict[type[models.Model], list[tuple[int, OptimizerStore]]] | None = None, level: int = 0, subclass_collection: set[type[models.Model]] | None = None, ) -> OptimizerStore | None: store = None n_type = object_definition.type_var_map.get("NodeType") if n_type is None: specialized_type_var_map = object_definition.specialized_type_var_map or {} n_type = specialized_type_var_map["NodeType"] n_type = unwrap_type(n_type) n_definition = get_object_definition(n_type, strict=True) for edges in _get_selections(info, parent_type).values(): edge = edges[0] if edge.name.value != "edges": continue e_field = object_definition.get_field("edges") if e_field is None: break e_definition = e_field.type while isinstance(e_definition, StrawberryContainer): e_definition = e_definition.of_type if has_object_definition(e_definition): e_definition = get_object_definition(e_definition, strict=True) assert isinstance(e_definition, StrawberryObjectDefinition) e_gql_definition = _get_gql_definition( schema, e_definition, ) assert isinstance(e_gql_definition, (GraphQLObjectType, GraphQLInterfaceType)) e_info = _generate_selection_resolve_info( info, edges, e_gql_definition, parent_type, ) for nodes in _get_selections(e_info, e_gql_definition).values(): node = nodes[0] if node.name.value != "node": continue for concrete_n_type in get_possible_concrete_types( model, schema, n_definition ): n_gql_definition = _get_gql_definition(schema, concrete_n_type) assert isinstance( n_gql_definition, (GraphQLObjectType, GraphQLInterfaceType), ) n_info = _generate_selection_resolve_info( info, nodes, n_gql_definition, e_gql_definition, ) concrete_store = _get_model_hints( model=model, schema=schema, object_definition=concrete_n_type, parent_type=n_gql_definition, info=n_info, config=config, prefix=prefix, cache=cache, level=level, subclass_collection=subclass_collection, ) if concrete_store is not None: store = concrete_store if store is None else store | concrete_store return store def _get_model_hints_from_paginated( model: type[models.Model], schema: Schema, object_definition: StrawberryObjectDefinition, *, parent_type: GraphQLObjectType | GraphQLInterfaceType, info: GraphQLResolveInfo, config: OptimizerConfig | None = None, prefix: str = "", cache: dict[type[models.Model], list[tuple[int, OptimizerStore]]] | None = None, level: int = 0, subclass_collection: set[type[models.Model]] | None = None, ) -> OptimizerStore | None: store = None n_type = unwrap_type(object_definition.type_var_map.get("NodeType")) n_definition = get_object_definition(n_type, strict=True) for selections in _get_selections(info, parent_type).values(): selection = selections[0] if selection.name.value != "results": continue for concrete_n_type in get_possible_concrete_types(model, schema, n_definition): n_gql_definition = _get_gql_definition( schema, concrete_n_type, ) assert isinstance( n_gql_definition, (GraphQLObjectType, GraphQLInterfaceType) ) n_info = _generate_selection_resolve_info( info, selections, n_gql_definition, n_gql_definition, ) concrete_store = _get_model_hints( model=model, schema=schema, object_definition=concrete_n_type, parent_type=n_gql_definition, info=n_info, config=config, prefix=prefix, cache=cache, level=level, subclass_collection=subclass_collection, ) if concrete_store is not None: store = concrete_store if store is None else store | concrete_store return store def optimize( qs: QuerySet[_M] | BaseManager[_M], info: GraphQLResolveInfo | Info, *, config: OptimizerConfig | None = None, store: OptimizerStore | None = None, ) -> QuerySet[_M]: """Optimize the given queryset considering the gql info. This will look through the gql selections, fields and model hints and apply `only`, `select_related`, `prefetch_related` and `annotate` optimizations according those on the `QuerySet`_. Note: ---- This do not execute the queryset, it only optimizes it for when it is actually executed. It will also avoid doing any extra optimization if the queryset already has cached results in it, to avoid triggering extra queries later. Args: ---- qs: The queryset to be optimized info: The current field execution info config: Optional config to use when doing the optimization store: Optional initial store to use for the optimization Returns: ------- The optimized queryset .. _QuerySet: https://docs.djangoproject.com/en/dev/ref/models/querysets/ """ if isinstance(qs, BaseManager): qs = cast("QuerySet[_M]", qs.all()) if isinstance(qs, list): # return sliced queryset as-is return qs # Avoid optimizing twice and also modify an already resolved queryset if is_optimized(qs) or qs._result_cache is not None: # type: ignore return qs if isinstance(info, Info): info = info._raw_info config = config or OptimizerConfig() store = store or OptimizerStore() schema = cast("Schema", info.schema._strawberry_schema) # type: ignore gql_type = get_named_type(info.return_type) strawberry_type = schema.get_type_by_name(gql_type.name) if strawberry_type is None: return qs inheritance_qs = is_inheritance_qs(qs) subclasses = set() if inheritance_qs else None for inner_object_definition in get_possible_concrete_types( qs.model, schema, strawberry_type ): parent_type = _get_gql_definition(schema, inner_object_definition) new_store = _get_model_hints( qs.model, schema, inner_object_definition, parent_type=parent_type, info=info, config=config, subclass_collection=subclasses, ) if new_store is not None: store |= new_store if store: if inheritance_qs and subclasses: qs = qs.select_subclasses(*subclasses) qs = store.apply(qs, info=info, config=config) qs_config = get_queryset_config(qs) qs_config.optimized = True return qs def is_optimized(qs: QuerySet) -> bool: config = get_queryset_config(qs) return config.optimized or config.optimized_by_prefetching def mark_optimized_by_prefetching(qs: QuerySet[_M]) -> QuerySet[_M]: get_queryset_config(qs).optimized_by_prefetching = True return qs def is_optimized_by_prefetching(qs: QuerySet) -> bool: return get_queryset_config(qs).optimized_by_prefetching optimizer: contextvars.ContextVar[DjangoOptimizerExtension | None] = ( contextvars.ContextVar( "optimizer_ctx", default=None, ) ) class DjangoOptimizerExtension(SchemaExtension): """Automatically optimize returned querysets from internal resolvers. Attributes ---------- enable_only_optimization: Enable `QuerySet.only` optimizations enable_select_related_optimization: Enable `QuerySet.select_related` optimizations enable_prefetch_related_optimization: Enable `QuerySet.prefetch_related` optimizations enable_nested_relations_prefetch: Enable prefetch of nested relations. This will allow for nested relations to be prefetched even when using filters/ordering/pagination. Note however that for connections, it will only work when for the `ListConnection` and `DjangoListConnection` types, as this optimization is not safe to be applied automatically for custom connections. enable_annotate_optimization: Enable `QuerySet.annotate` optimizations Examples -------- Add the following to your schema configuration. >>> import strawberry >>> from strawberry_django_plus.optimizer import DjangoOptimizerExtension ... >>> schema = strawberry.Schema( ... Query, ... extensions=[ ... DjangoOptimizerExtension(), ... ] ... ) """ enabled: contextvars.ContextVar[bool] = contextvars.ContextVar( "optimizer_enabled_ctx", default=True, ) def __init__( self, *, enable_only_optimization: bool = True, enable_select_related_optimization: bool = True, enable_prefetch_related_optimization: bool = True, enable_annotate_optimization: bool = True, enable_nested_relations_prefetch: bool = True, execution_context: ExecutionContext | None = None, prefetch_custom_queryset: bool = False, ): super().__init__(execution_context=execution_context) self.enable_only = enable_only_optimization self.enable_select_related = enable_select_related_optimization self.enable_prefetch_related = enable_prefetch_related_optimization self.enable_annotate_optimization = enable_annotate_optimization self.enable_nested_relations_prefetch = enable_nested_relations_prefetch self.prefetch_custom_queryset = prefetch_custom_queryset if enable_nested_relations_prefetch: from strawberry_django.utils.patches import apply_pagination_fix apply_pagination_fix() def on_execute(self) -> Generator[None]: token = optimizer.set(self) try: yield finally: optimizer.reset(token) def resolve( self, next_: Callable, root: Any, info: GraphQLResolveInfo, *args, **kwargs, ) -> AwaitableOrValue[Any]: ret = next_(root, info, *args, **kwargs) if not self.enabled.get(): return ret if isinstance(ret, BaseManager): ret = ret.all() if isinstance(ret, QuerySet) and ret._result_cache is None: # type: ignore config = OptimizerConfig( enable_only=( self.enable_only and info.operation.operation == OperationType.QUERY ), enable_select_related=self.enable_select_related, enable_prefetch_related=self.enable_prefetch_related, enable_annotate=self.enable_annotate_optimization, prefetch_custom_queryset=self.prefetch_custom_queryset, enable_nested_relations_prefetch=self.enable_nested_relations_prefetch, ) ret = django_fetch(optimize(qs=ret, info=info, config=config)) return ret @classmethod @contextlib.contextmanager def disabled(cls): token = cls.enabled.set(False) try: yield finally: cls.enabled.reset(token) def optimize( self, qs: QuerySet[_M] | BaseManager[_M], info: GraphQLResolveInfo | Info, *, store: OptimizerStore | None = None, ) -> QuerySet[_M]: if not self.enabled.get(): return qs config = OptimizerConfig( enable_only=self.enable_only and info.operation.operation == OperationType.QUERY, enable_select_related=self.enable_select_related, enable_prefetch_related=self.enable_prefetch_related, enable_annotate=self.enable_annotate_optimization, prefetch_custom_queryset=self.prefetch_custom_queryset, ) return optimize(qs, info, config=config, store=store) strawberry-graphql-django-0.62.0/strawberry_django/ordering.py000066400000000000000000000331711502405145400246150ustar00rootroot00000000000000from __future__ import annotations import dataclasses import enum from typing import ( TYPE_CHECKING, Any, Optional, TypeVar, cast, ) import strawberry from django.db.models import F, OrderBy, QuerySet from graphql.language.ast import ObjectValueNode from strawberry import UNSET from strawberry.types import has_object_definition from strawberry.types.base import WithStrawberryObjectDefinition from strawberry.types.field import StrawberryField, field from strawberry.types.unset import UnsetType from strawberry.utils.str_converters import to_camel_case from typing_extensions import Self, dataclass_transform, deprecated from strawberry_django.fields.base import StrawberryDjangoFieldBase from strawberry_django.fields.filter_order import ( WITH_NONE_META, FilterOrderField, FilterOrderFieldResolver, ) from strawberry_django.utils.typing import is_auto, unwrap_type from .arguments import argument if TYPE_CHECKING: from collections.abc import Callable, Collection, Sequence from django.db.models import Model from strawberry.types import Info from strawberry.types.arguments import StrawberryArgument _T = TypeVar("_T") _QS = TypeVar("_QS", bound="QuerySet") _SFT = TypeVar("_SFT", bound=StrawberryField) ORDER_ARG = "order" ORDERING_ARG = "ordering" @dataclasses.dataclass class OrderSequence: seq: int = 0 children: dict[str, OrderSequence] | None = None @classmethod def get_graphql_name(cls, info: Info | None, field: StrawberryField) -> str: if info is None: if field.graphql_name: return field.graphql_name return to_camel_case(field.python_name) return info.schema.config.name_converter.get_graphql_name(field) @classmethod def sorted( cls, info: Info | None, sequence: dict[str, OrderSequence] | None, fields: list[_SFT], ) -> list[_SFT]: if info is None: return fields sequence = sequence or {} def sort_key(f: _SFT) -> int: if not (seq := sequence.get(cls.get_graphql_name(info, f))): return 0 return seq.seq return sorted(fields, key=sort_key) @strawberry.enum class Ordering(enum.Enum): ASC = "ASC" ASC_NULLS_FIRST = "ASC_NULLS_FIRST" ASC_NULLS_LAST = "ASC_NULLS_LAST" DESC = "DESC" DESC_NULLS_FIRST = "DESC_NULLS_FIRST" DESC_NULLS_LAST = "DESC_NULLS_LAST" def resolve(self, value: str) -> OrderBy: nulls_first = True if "NULLS_FIRST" in self.name else None nulls_last = True if "NULLS_LAST" in self.name else None if "ASC" in self.name: return F(value).asc(nulls_first=nulls_first, nulls_last=nulls_last) return F(value).desc(nulls_first=nulls_first, nulls_last=nulls_last) def process_order( order: WithStrawberryObjectDefinition, info: Info | None, queryset: _QS, *, sequence: dict[str, OrderSequence] | None = None, prefix: str = "", skip_object_order_method: bool = False, ) -> tuple[_QS, Collection[F | OrderBy | str]]: sequence = sequence or {} args = [] if not skip_object_order_method and isinstance( order_method := getattr(order, "order", None), FilterOrderFieldResolver, ): return order_method( order, info, queryset=queryset, prefix=prefix, sequence=sequence ) for f in OrderSequence.sorted( info, sequence, order.__strawberry_definition__.fields ): f_value = getattr(order, f.name, UNSET) if f_value is UNSET or (f_value is None and not f.metadata.get(WITH_NONE_META)): continue if isinstance(f, FilterOrderField) and f.base_resolver: res = f.base_resolver( order, info, value=f_value, queryset=queryset, prefix=prefix, sequence=( (seq := sequence.get(OrderSequence.get_graphql_name(info, f))) and seq.children ), ) if isinstance(res, tuple): queryset, subargs = res else: subargs = res args.extend(subargs) elif isinstance(f_value, Ordering): args.append(f_value.resolve(f"{prefix}{f.name}")) else: queryset, subargs = process_order( f_value, info, queryset, prefix=f"{prefix}{f.name}__", sequence=( (seq := sequence.get(OrderSequence.get_graphql_name(info, f))) and seq.children ), ) args.extend(subargs) return queryset, args def apply( order: object | None, queryset: _QS, info: Info | None = None, ) -> _QS: if order in (None, strawberry.UNSET) or not has_object_definition(order): # noqa: PLR6201 return queryset sequence: dict[str, OrderSequence] = {} if info is not None and info._raw_info.field_nodes: field_node = info._raw_info.field_nodes[0] for arg in field_node.arguments: if arg.name.value != ORDER_ARG or not isinstance( arg.value, ObjectValueNode ): continue def parse_and_fill(field: ObjectValueNode, seq: dict[str, OrderSequence]): for i, f in enumerate(field.fields): f_sequence: dict[str, OrderSequence] = {} if isinstance(f.value, ObjectValueNode): parse_and_fill(f.value, f_sequence) seq[f.name.value] = OrderSequence(seq=i, children=f_sequence) parse_and_fill(arg.value, sequence) queryset, args = process_order( cast("WithStrawberryObjectDefinition", order), info, queryset, sequence=sequence ) if not args: return queryset return queryset.order_by(*args) def process_ordering_default( ordering: Any, info: Info | None, queryset: _QS, prefix: str = "", ) -> tuple[_QS, Collection[F | OrderBy | str]]: if ordering is None or not has_object_definition(ordering): return queryset, () args = [] for f in ordering.__strawberry_definition__.fields: f_value = getattr(ordering, f.name, UNSET) if f_value is UNSET or (f_value is None and not f.metadata.get(WITH_NONE_META)): continue if isinstance(f, FilterOrderField) and f.base_resolver: res = f.base_resolver( ordering, info, value=f_value, queryset=queryset, prefix=prefix, ) if isinstance(res, tuple): queryset, subargs = res else: subargs = res args.extend(subargs) elif isinstance(f_value, Ordering): args.append(f_value.resolve(f"{prefix}{f.name}")) else: ordering_cls = unwrap_type(f.type) assert isinstance(ordering_cls, type) assert has_object_definition(ordering_cls) queryset, subargs = process_ordering( ordering_cls, (f_value,), info, queryset, prefix=f"{prefix}{f.name}__", ) args.extend(subargs) return queryset, args def process_ordering( ordering_cls: type[WithStrawberryObjectDefinition], ordering: Collection[WithStrawberryObjectDefinition] | None, info: Info | None, queryset: _QS, prefix: str = "", ) -> tuple[_QS, Collection[F | OrderBy | str]]: if not ordering: return queryset, () if not isinstance( order_method := getattr(ordering_cls, "order", None), FilterOrderFieldResolver ): order_method = process_ordering_default args = [] for o in ordering: queryset, new_args = order_method(o, info, queryset=queryset, prefix=prefix) args.extend(new_args) return queryset, args def apply_ordering( ordering_cls: type[WithStrawberryObjectDefinition], ordering: Collection[WithStrawberryObjectDefinition] | None, info: Info | None, queryset: _QS, ) -> _QS: queryset, args = process_ordering(ordering_cls, ordering, info, queryset) if args: queryset = queryset.order_by(*args) return queryset class StrawberryDjangoFieldOrdering(StrawberryDjangoFieldBase): def __init__( self, order: type | UnsetType | None = UNSET, ordering: type | UnsetType | None = UNSET, **kwargs, ): if order and not has_object_definition(order): raise TypeError("order needs to be a strawberry type") if ordering and not has_object_definition(ordering): raise TypeError("ordering needs to be a strawberry type") self.order = order self.ordering = ordering super().__init__(**kwargs) def __copy__(self) -> Self: new_field = super().__copy__() new_field.order = self.order new_field.ordering = self.ordering return new_field @property def arguments(self) -> list[StrawberryArgument]: arguments = [] if self.base_resolver is None and self.is_list and not self.is_model_property: order = self.get_order() if order and order is not UNSET: arguments.append(argument("order", order, is_optional=True)) if self.base_resolver is None and self.is_list and not self.is_model_property: ordering = self.get_ordering() if ordering is not None: arguments.append( argument("ordering", ordering, is_list=True, default=[]) ) return super().arguments + arguments @arguments.setter def arguments(self, value: list[StrawberryArgument]): args_prop = super(StrawberryDjangoFieldOrdering, self.__class__).arguments return args_prop.fset(self, value) # type: ignore def get_order(self) -> type[WithStrawberryObjectDefinition] | None: order = self.order if order is None: return None if isinstance(order, UnsetType): django_type = self.django_type order = ( django_type.__strawberry_django_definition__.order if django_type is not None else None ) return order if order is not UNSET else None def get_ordering(self) -> type[WithStrawberryObjectDefinition] | None: ordering = self.ordering if ordering is None: return None if isinstance(ordering, UnsetType): django_type = self.django_type ordering = ( django_type.__strawberry_django_definition__.ordering if django_type is not None and django_type is not UNSET else None ) return ordering def get_queryset( self, queryset: _QS, info: Info, *, order: WithStrawberryObjectDefinition | None = None, ordering: list[WithStrawberryObjectDefinition] | None = None, **kwargs, ) -> _QS: if order and ordering: raise ValueError("Only one of `ordering` or `order` must be given.") queryset = super().get_queryset(queryset, info, **kwargs) queryset = apply(order, queryset, info=info) if ordering_cls := self.get_ordering(): queryset = apply_ordering(ordering_cls, ordering, info, queryset) return queryset @dataclass_transform( order_default=True, field_specifiers=( StrawberryField, field, ), ) def order_type( model: type[Model], *, name: str | None = None, one_of: bool = True, description: str | None = None, directives: Sequence[object] | None = (), ) -> Callable[[_T], _T]: def wrapper(cls): try: cls.__annotations__ # noqa: B018 except AttributeError: # FIXME: Manual creation for python 3.9 (remove when 3.9 is dropped) cls.__annotations__ = {} for fname, type_ in cls.__annotations__.items(): if is_auto(type_): type_ = Ordering # noqa: PLW2901 cls.__annotations__[fname] = Optional[type_] field_ = cls.__dict__.get(fname) if not isinstance(field_, StrawberryField): setattr(cls, fname, UNSET) return strawberry.input( cls, name=name, one_of=one_of, description=description, directives=directives, ) return wrapper @dataclass_transform( order_default=True, field_specifiers=( StrawberryField, field, ), ) @deprecated( "strawberry_django.order is deprecated in favor of strawberry_django.ordering." ) def order( model: type[Model], *, name: str | None = None, description: str | None = None, directives: Sequence[object] | None = (), ) -> Callable[[_T], _T]: def wrapper(cls): try: cls.__annotations__ # noqa: B018 except AttributeError: # FIXME: Manual creation for python 3.9 (remove when 3.9 is dropped) cls.__annotations__ = {} for fname, type_ in cls.__annotations__.items(): if is_auto(type_): type_ = Ordering # noqa: PLW2901 cls.__annotations__[fname] = Optional[type_] field_ = cls.__dict__.get(fname) if not isinstance(field_, StrawberryField): setattr(cls, fname, UNSET) return strawberry.input( cls, name=name, description=description, directives=directives, ) return wrapper strawberry-graphql-django-0.62.0/strawberry_django/pagination.py000066400000000000000000000301531502405145400251320ustar00rootroot00000000000000import sys import warnings from typing import Generic, Optional, TypeVar, Union, cast import strawberry from django.db import DEFAULT_DB_ALIAS from django.db.models import Count, QuerySet, Window from django.db.models.functions import RowNumber from django.db.models.query import MAX_GET_RESULTS # type: ignore from strawberry.types import Info from strawberry.types.arguments import StrawberryArgument from strawberry.types.unset import UNSET, UnsetType from typing_extensions import Self from strawberry_django.fields.base import StrawberryDjangoFieldBase from strawberry_django.resolvers import django_resolver from .arguments import argument from .settings import strawberry_django_settings NodeType = TypeVar("NodeType") _QS = TypeVar("_QS", bound=QuerySet) PAGINATION_ARG = "pagination" @strawberry.type class OffsetPaginationInfo: offset: int = 0 limit: Optional[int] = UNSET @strawberry.input class OffsetPaginationInput(OffsetPaginationInfo): ... @strawberry.type class OffsetPaginated(Generic[NodeType]): queryset: strawberry.Private[Optional[QuerySet]] pagination: strawberry.Private[OffsetPaginationInput] @strawberry.field def page_info(self) -> OffsetPaginationInfo: return OffsetPaginationInfo( limit=self.pagination.limit, offset=self.pagination.offset, ) @strawberry.field(description="Total count of existing results.") @django_resolver def total_count(self) -> int: return self.get_total_count() @strawberry.field(description="List of paginated results.") @django_resolver def results(self) -> list[NodeType]: paginated_queryset = self.get_paginated_queryset() return cast( "list[NodeType]", paginated_queryset if paginated_queryset is not None else [], ) @classmethod def resolve_paginated( cls, queryset: QuerySet, *, info: Info, pagination: Optional[OffsetPaginationInput] = None, **kwargs, ) -> Self: """Resolve the paginated queryset. Args: queryset: The queryset to be paginated. info: The strawberry execution info resolve the type name from. pagination: The pagination input to be applied. kwargs: Additional arguments passed to the resolver. Returns: The resolved `OffsetPaginated` """ return cls( queryset=queryset, pagination=pagination or OffsetPaginationInput(), ) def get_total_count(self) -> int: """Retrieve tht total count of the queryset without pagination.""" return get_total_count(self.queryset) if self.queryset is not None else 0 def get_paginated_queryset(self) -> Optional[QuerySet]: """Retrieve the queryset with pagination applied. This will apply the paginated arguments to the queryset and return it. To use the original queryset, access `.queryset` directly. """ from strawberry_django.optimizer import is_optimized_by_prefetching if self.queryset is None: return None return ( self.queryset._result_cache # type: ignore if is_optimized_by_prefetching(self.queryset) else apply(self.pagination, self.queryset) ) def apply( pagination: Optional[object], queryset: _QS, *, related_field_id: Optional[str] = None, ) -> _QS: """Apply pagination to a queryset. Args: ---- pagination: The pagination input. queryset: The queryset to apply pagination to. related_field_id: The related field id to apply pagination to. When provided, the pagination will be applied using window functions instead of slicing the queryset. Useful for prefetches, as those cannot be sliced after being filtered """ if pagination in (None, strawberry.UNSET): # noqa: PLR6201 return queryset if not isinstance(pagination, OffsetPaginationInput): raise TypeError(f"Don't know how to resolve pagination {pagination!r}") if related_field_id is not None: queryset = apply_window_pagination( queryset, related_field_id=related_field_id, offset=pagination.offset, limit=pagination.limit, ) else: start = pagination.offset limit = pagination.limit if limit is UNSET: settings = strawberry_django_settings() limit = settings["PAGINATION_DEFAULT_LIMIT"] if limit is not None and limit >= 0: stop = start + limit queryset = queryset[start:stop] else: queryset = queryset[start:] return queryset class _PaginationWindow(Window): """Window function to be used for pagination. This is the same as django's `Window` function, but we can easily identify it in case we need to remove it from the queryset, as there might be other window functions in the queryset and no other way to identify ours. """ def apply_window_pagination( queryset: _QS, *, related_field_id: str, offset: int = 0, limit: Optional[int] = UNSET, max_results: Optional[int] = None, ) -> _QS: """Apply pagination using window functions. Useful for prefetches, as those cannot be sliced after being filtered. This is based on the same solution that Django uses, which was implemented in https://github.com/django/django/pull/15957 Args: ---- queryset: The queryset to apply pagination to. related_field_id: The related field id to apply pagination to. offset: The offset to start the pagination from. limit: The limit of items to return. """ order_by = [ expr for expr, _ in queryset.query.get_compiler( using=queryset._db or DEFAULT_DB_ALIAS # type: ignore ).get_order_by() ] queryset = queryset.annotate( _strawberry_row_number=_PaginationWindow( RowNumber(), partition_by=related_field_id, order_by=order_by, ), _strawberry_total_count=_PaginationWindow( Count(1), partition_by=related_field_id, ), ) if offset: queryset = queryset.filter(_strawberry_row_number__gt=offset) if limit is UNSET: settings = strawberry_django_settings() limit = ( max_results if max_results is not None else settings["PAGINATION_DEFAULT_LIMIT"] ) # Limit == -1 means no limit. sys.maxsize is set by relay when paginating # from the end to as a way to mimic a "not limit" as well if limit is not None and limit >= 0 and limit != sys.maxsize: queryset = queryset.filter(_strawberry_row_number__lte=offset + limit) return queryset def remove_window_pagination(queryset: _QS) -> _QS: """Remove pagination window functions from a queryset. Utility function to remove the pagination `WHERE` clause added by the `apply_window_pagination` function. Args: ---- queryset: The queryset to apply pagination to. """ queryset = queryset._chain() # type: ignore queryset.query.where.children = [ child for child in queryset.query.where.children if (not hasattr(child, "lhs") or not isinstance(child.lhs, _PaginationWindow)) ] return queryset def get_total_count(queryset: QuerySet) -> int: """Get the total count of a queryset. Try to get the total count from the queryset cache, if it's optimized by prefetching. Otherwise, fallback to the `QuerySet.count()` method. """ from strawberry_django.optimizer import is_optimized_by_prefetching if is_optimized_by_prefetching(queryset): results = queryset._result_cache # type: ignore if results: try: return results[0]._strawberry_total_count except AttributeError: warnings.warn( ( "Pagination annotations not found, falling back to QuerySet resolution. " "This might cause n+1 issues..." ), RuntimeWarning, stacklevel=2, ) # If we have no results, we can't get the total count from the cache. # In this case we will remove the pagination filter to be able to `.count()` # the whole queryset with its original filters. queryset = remove_window_pagination(queryset) return queryset.count() class StrawberryDjangoPagination(StrawberryDjangoFieldBase): def __init__(self, pagination: Union[bool, UnsetType] = UNSET, **kwargs): self.pagination = pagination super().__init__(**kwargs) def __copy__(self) -> Self: new_field = super().__copy__() new_field.pagination = self.pagination return new_field def _has_pagination(self) -> bool: if isinstance(self.pagination, bool): return self.pagination if self.is_paginated: return True django_type = self.django_type if django_type is not None and not issubclass( django_type, strawberry.relay.Node ): return django_type.__strawberry_django_definition__.pagination return False @property def arguments(self) -> list[StrawberryArgument]: arguments = [] if ( self.base_resolver is None and (self.is_list or self.is_paginated) and not self.is_model_property ): pagination = self.get_pagination() if pagination is not None: arguments.append( argument("pagination", OffsetPaginationInput, is_optional=True), ) return super().arguments + arguments @arguments.setter def arguments(self, value: list[StrawberryArgument]): args_prop = super(StrawberryDjangoPagination, self.__class__).arguments return args_prop.fset(self, value) # type: ignore def get_pagination(self) -> Optional[type]: return OffsetPaginationInput if self._has_pagination() else None def apply_pagination( self, queryset: _QS, pagination: Optional[object] = None, *, related_field_id: Optional[str] = None, ) -> _QS: return apply(pagination, queryset, related_field_id=related_field_id) def get_queryset( self, queryset: _QS, info: Info, *, pagination: Optional[OffsetPaginationInput] = None, _strawberry_related_field_id: Optional[str] = None, **kwargs, ) -> _QS: queryset = super().get_queryset(queryset, info, **kwargs) # If the queryset is not ordered, and this field is either going to return # multiple records, or call `.first()`, then order by the primary key to ensure # deterministic results. if not queryset.ordered and ( self.is_list or self.is_paginated or self.is_connection or self.is_optional ): queryset = queryset.order_by("pk") # This is counter intuitive, but in case we are returning a `Paginated` # result, we want to set the original queryset _as is_ as it will apply # the pagination later on when resolving its `.results` field. # Check `get_wrapped_result` below for more details. if self.is_paginated: return queryset # Add implicit pagination if this field is not a list # that way when first() / get() is called on the QuerySet it does not cause extra queries # and we don't prefetch more than necessary if ( not pagination and not (self.is_list or self.is_paginated or self.is_connection) and not _strawberry_related_field_id ): if self.is_optional: pagination = OffsetPaginationInput(offset=0, limit=1) else: pagination = OffsetPaginationInput(offset=0, limit=MAX_GET_RESULTS) return self.apply_pagination( queryset, pagination, related_field_id=_strawberry_related_field_id, ) strawberry-graphql-django-0.62.0/strawberry_django/permissions.py000066400000000000000000000653211502405145400253610ustar00rootroot00000000000000import abc import contextlib import contextvars import copy import dataclasses import enum import functools import inspect from collections.abc import Callable, Hashable, Iterable from typing import ( TYPE_CHECKING, Any, ClassVar, Optional, TypeVar, Union, cast, overload, ) import strawberry from asgiref.sync import sync_to_async from django.core.exceptions import PermissionDenied from django.db.models import Model, QuerySet from graphql.pyutils import AwaitableOrValue from strawberry import relay, schema_directive from strawberry.extensions.field_extension import ( AsyncExtensionResolver, FieldExtension, SyncExtensionResolver, ) from strawberry.schema_directive import Location from strawberry.types.base import StrawberryList, StrawberryOptional from strawberry.types.field import StrawberryField from strawberry.types.info import Info from strawberry.types.union import StrawberryUnion from typing_extensions import Literal, assert_never from strawberry_django.auth.utils import aget_current_user, get_current_user from strawberry_django.fields.types import OperationInfo, OperationMessage from strawberry_django.pagination import OffsetPaginated from strawberry_django.resolvers import django_resolver from .utils.query import filter_for_user from .utils.typing import UserType if TYPE_CHECKING: from strawberry.django.context import StrawberryDjangoContext from strawberry_django.fields.field import StrawberryDjangoField _M = TypeVar("_M", bound=Model) @functools.lru_cache def _get_user_or_anonymous_getter() -> Optional[Callable[[UserType], UserType]]: try: from .integrations.guardian import get_user_or_anonymous except (ImportError, RuntimeError): # pragma: no cover return None return get_user_or_anonymous @dataclasses.dataclass class PermContext: is_safe_list: list[bool] = dataclasses.field(default_factory=list) checkers: list["HasPerm"] = dataclasses.field(default_factory=list) def __copy__(self): return self.__class__( is_safe_list=self.is_safe_list[:], checkers=self.checkers[:], ) @property def is_safe(self): return bool(self.is_safe_list and all(self.is_safe_list)) perm_context: contextvars.ContextVar[PermContext] = contextvars.ContextVar( "perm-safe", default=PermContext(), # noqa: B039 ) @contextlib.contextmanager def with_perm_checker(checker: "HasPerm"): context = copy.copy(perm_context.get()) context.checkers.append(checker) token = perm_context.set(context) try: yield finally: perm_context.reset(token) def set_perm_safe(value: bool): perm_context.get().is_safe_list.append(value) def filter_with_perms(qs: QuerySet[_M], info: Info) -> QuerySet[_M]: context = perm_context.get() if not context.checkers or context.is_safe: return qs if isinstance(qs, list): # return sliced queryset as-is return qs # Do not do anything is results are cached if qs._result_cache is not None: # type: ignore set_perm_safe(False) return qs user = cast("StrawberryDjangoContext", info.context).request.user # If the user is anonymous, we can't filter object permissions for it if user.is_anonymous: set_perm_safe(False) return qs.none() for check in context.checkers: if check.target != PermTarget.RETVAL: continue qs = filter_for_user( qs, user, [p.perm for p in check.perms], any_perm=check.any_perm, with_superuser=check.with_superuser, ) set_perm_safe(True) return qs @overload def get_with_perms( pk: strawberry.ID, info: Info, *, required: Literal[True], model: type[_M], key_attr: str = ..., ) -> _M: ... @overload def get_with_perms( pk: strawberry.ID, info: Info, *, required: bool = ..., model: type[_M], key_attr: str = ..., ) -> Optional[_M]: ... @overload def get_with_perms( pk: relay.GlobalID, info: Info, *, required: Literal[True], model: type[_M], key_attr: str = ..., ) -> _M: ... @overload def get_with_perms( pk: relay.GlobalID, info: Info, *, required: bool = ..., model: type[_M], key_attr: str = ..., ) -> Optional[_M]: ... @overload def get_with_perms( pk: relay.GlobalID, info: Info, *, required: Literal[True], key_attr: str = ..., ) -> Any: ... @overload def get_with_perms( pk: relay.GlobalID, info: Info, *, required: bool = ..., key_attr: str = ..., ) -> Optional[Any]: ... def get_with_perms( pk, info, *, required=False, model=None, key_attr="pk", ): if isinstance(pk, relay.GlobalID): instance = pk.resolve_node_sync(info, required=required, ensure_type=model) else: assert model instance = model._default_manager.get(**{key_attr: pk}) if instance is None: return None context = perm_context.get() if not context.checkers or context.is_safe: return instance user = cast("StrawberryDjangoContext", info.context).request.user if user and (get_user_or_anonymous := _get_user_or_anonymous_getter()) is not None: user = get_user_or_anonymous(user) for check in context.checkers: f = any if check.any_perm else all checker = check.obj_perm_checker(info, user) if not f(checker(p, instance) for p in check.perms): raise PermissionDenied(check.message) return instance _return_condition = """\ When the condition fails, the following can be returned (following this priority): 1) `OperationInfo`/`OperationMessage` if those types are allowed at the return type 2) `null` in case the field is not mandatory (e.g. `String` or `[String]`) 3) An empty list in case the field is a list (e.g. `[String]!`) 4) An empty `Connection` in case the return type is a relay connection 2) Otherwise, an error will be raised """ def _desc(desc): return f"{desc}\n\n{_return_condition.strip()}" class DjangoNoPermission(Exception): # noqa: N818 """Raise to identify that the user doesn't have perms for a given retval.""" class DjangoPermissionExtension(FieldExtension, abc.ABC): """Base django permission extension.""" DEFAULT_ERROR_MESSAGE: ClassVar[str] = "User does not have permission." SCHEMA_DIRECTIVE_LOCATIONS: ClassVar[list[Location]] = [Location.FIELD_DEFINITION] SCHEMA_DIRECTIVE_DESCRIPTION: ClassVar[Optional[str]] = None def __init__( self, *, message: Optional[str] = None, use_directives: bool = True, fail_silently: bool = True, ): super().__init__() self.message = message if message is not None else self.DEFAULT_ERROR_MESSAGE self.fail_silently = fail_silently self.use_directives = use_directives def apply(self, field: StrawberryField) -> None: # pragma: no cover if self.use_directives: directive = self.schema_directive # Avoid interfaces duplicating the directives if directive not in field.directives: field.directives.append(self.schema_directive) @functools.cached_property def schema_directive(self) -> object: key = "__strawberry_directive_type__" directive_class = getattr(self.__class__, key, None) if directive_class is None: @schema_directive( name=self.__class__.__name__, locations=self.SCHEMA_DIRECTIVE_LOCATIONS, description=self.SCHEMA_DIRECTIVE_DESCRIPTION, repeatable=True, ) class AutoDirective: ... directive_class = AutoDirective return directive_class() @django_resolver(qs_hook=None) def resolve( self, next_: SyncExtensionResolver, source: Any, info: Info, **kwargs: dict[str, Any], ) -> Any: user = get_current_user(info) if ( user and (get_user_or_anonymous := _get_user_or_anonymous_getter()) is not None ): user = get_user_or_anonymous(user) # make sure the user is loaded user.is_authenticated # noqa: B018 try: retval = self.resolve_for_user( functools.partial(next_, source, info, **kwargs), user, info=info, source=source, ) except DjangoNoPermission as e: retval = self.handle_no_permission(e, info=info) return retval async def resolve_async( self, next_: AsyncExtensionResolver, source: Any, info: Info, **kwargs: dict[str, Any], ) -> Any: user = await aget_current_user(info) try: from .integrations.guardian import get_user_or_anonymous except (ImportError, RuntimeError): # pragma: no cover pass else: user = user and await sync_to_async(get_user_or_anonymous)(user) # make sure the user is loaded await sync_to_async(getattr)(user, "is_anonymous") try: retval = self.resolve_for_user( functools.partial(next_, source, info, **kwargs), user, info=info, source=source, ) while inspect.isawaitable(retval): retval = await retval except DjangoNoPermission as e: retval = self.handle_no_permission(e, info=info) return retval def handle_no_permission(self, exception: BaseException, *, info: Info): if not self.fail_silently: raise PermissionDenied(self.message) from exception ret_type = info.return_type if isinstance(ret_type, StrawberryOptional): ret_type = ret_type.of_type is_optional = True else: is_optional = False if isinstance(ret_type, StrawberryUnion): ret_types = [] for type_ in ret_type.types: ret_types.append(ret_type) if not isinstance(type_, type): continue if issubclass(type_, OperationInfo): return type_( messages=[ OperationMessage( kind=OperationMessage.Kind.PERMISSION, message=self.message, field=info.field_name, ), ], ) if issubclass(type_, OperationMessage): return type_( kind=OperationMessage.Kind.PERMISSION, message=self.message, field=info.field_name, ) else: ret_types = [ret_type] if is_optional: return None if isinstance(ret_type, StrawberryList): return [] if isinstance(ret_type, type) and issubclass(ret_type, OffsetPaginated): django_model = cast("StrawberryDjangoField", info._field).django_model assert django_model return django_model._default_manager.none() # If it is a Connection, try to return an empty connection, but only if # it is the only possibility available... for ret_possibility in ret_types: if isinstance(ret_possibility, type) and issubclass( ret_possibility, relay.Connection, ): return [] # In last case, raise an error raise PermissionDenied(self.message) from exception @abc.abstractmethod def resolve_for_user( # pragma: no cover self, resolver: Callable, user: Optional[UserType], *, info: Info, source: Any, ) -> AwaitableOrValue[Any]: ... class IsAuthenticated(DjangoPermissionExtension): """Mark a field as only resolvable by authenticated users.""" DEFAULT_ERROR_MESSAGE: ClassVar[str] = "User is not authenticated." SCHEMA_DIRECTIVE_DESCRIPTION: ClassVar[Optional[str]] = _desc( "Can only be resolved by authenticated users.", ) @django_resolver(qs_hook=None) def resolve_for_user( self, resolver: Callable, user: Optional[UserType], *, info: Info, source: Any, ): if user is None or not user.is_authenticated or not user.is_active: raise DjangoNoPermission return resolver() class IsStaff(DjangoPermissionExtension): """Mark a field as only resolvable by staff users.""" DEFAULT_ERROR_MESSAGE: ClassVar[str] = "User is not a staff member." SCHEMA_DIRECTIVE_DESCRIPTION: ClassVar[Optional[str]] = _desc( "Can only be resolved by staff users.", ) @django_resolver(qs_hook=None) def resolve_for_user( self, resolver: Callable, user: Optional[UserType], *, info: Info, source: Any, ): if ( user is None or not user.is_authenticated or not getattr(user, "is_staff", False) ): raise DjangoNoPermission return resolver() class IsSuperuser(DjangoPermissionExtension): """Mark a field as only resolvable by superuser users.""" DEFAULT_ERROR_MESSAGE: ClassVar[str] = "User is not a superuser." SCHEMA_DIRECTIVE_DESCRIPTION: ClassVar[Optional[str]] = _desc( "Can only be resolved by superuser users.", ) @django_resolver(qs_hook=None) def resolve_for_user( self, resolver: Callable, user: Optional[UserType], *, info: Info, source: Any, ): if ( user is None or not user.is_authenticated or not getattr(user, "is_superuser", False) ): raise DjangoNoPermission return resolver() @strawberry.input(description="Permission definition for schema directives.") class PermDefinition: """Permission definition. Attributes ---------- app: The app to which we are requiring permission. permission: The permission itself """ app: Optional[str] = strawberry.field( description=( "The app to which we are requiring permission. If this is " "empty that means that we are checking the permission directly." ), ) permission: Optional[str] = strawberry.field( description=( "The permission itself. If this is empty that means that we " "are checking for any permission for the given app." ), ) @classmethod def from_perm(cls, perm: str): parts = perm.split(".") if len(parts) != 2: # noqa: PLR2004 raise TypeError( "Permissions need to be defined as `app_label.perm`, `app_label.`" " or `.perm`", ) return cls( app=parts[0].strip() or None, permission=parts[1].strip() or None, ) @property def perm(self): return f"{self.app or ''}.{self.permission or ''}".strip(".") def __eq__(self, other: object): if not isinstance(other, PermDefinition): return NotImplemented return self.perm == other.perm def __hash__(self): return hash((self.__class__, self.perm)) class PermTarget(enum.IntEnum): """Permission location.""" GLOBAL = enum.auto() SOURCE = enum.auto() RETVAL = enum.auto() def _default_perm_checker(info: Info, user: UserType): def perm_checker(perm: PermDefinition) -> bool: return ( user.has_perm(perm.perm) # type: ignore if perm.permission else user.has_module_perms(cast("str", perm.app)) # type: ignore ) return perm_checker def _default_obj_perm_checker(info: Info, user: UserType): def perm_checker(perm: PermDefinition, obj: Any) -> bool: # Check global perms first, then object specific return user.has_perm(perm.perm) or user.has_perm( # type: ignore perm.perm, obj=obj, ) return perm_checker class HasPerm(DjangoPermissionExtension): """Defines permissions required to access the given object/field. Given a `app` name, the user can access the decorated object/field if he has any of the permissions defined in this directive. Examples -------- To indicate that a mutation can only be done by someone who has "product.add_product" perm in the django system: >>> @strawberry.type ... class Query: ... @strawberry.mutation(directives=[HasPerm("product.add_product")]) ... def create_product(self, name: str) -> ProductType: ... ... Attributes ---------- perms: Perms required to access this app. any_perm: If any perm or all perms are required to resolve the object/field. target: The target to check for permissions. Use `HasSourcePerm` or `HasRetvalPerm` as a shortcut for this. with_anonymous: If we should optimize the permissions check and consider an anonymous user as not having any permissions. This is true by default, which means that anonymous users will not trigger has_perm checks. with_superuser: If we should optimize the permissions check and consider a superuser as having permissions foe everything. This is false by default to avoid returning unexpected results. Setting this to true will avoid triggering has_perm checks. """ DEFAULT_TARGET: ClassVar[PermTarget] = PermTarget.GLOBAL DEFAULT_ERROR_MESSAGE: ClassVar[str] = ( "You don't have permission to access this app." ) SCHEMA_DIRECTIVE_DESCRIPTION: ClassVar[Optional[str]] = _desc( "Will check if the user has any/all permissions to resolve this.", ) def __init__( self, perms: Union[list[str], str], *, message: Optional[str] = None, use_directives: bool = True, fail_silently: bool = True, target: Optional[PermTarget] = None, any_perm: bool = True, perm_checker: Optional[ Callable[[Info, UserType], Callable[[PermDefinition], bool]] ] = None, obj_perm_checker: Optional[ Callable[[Info, UserType], Callable[[PermDefinition, Any], bool]] ] = None, with_anonymous: bool = True, with_superuser: bool = False, ): super().__init__( message=message, use_directives=use_directives, fail_silently=fail_silently, ) if isinstance(perms, str): perms = [perms] if not perms: raise TypeError(f"At least one perm is required for {self!r}") self.perms: tuple[PermDefinition, ...] = tuple( PermDefinition.from_perm(p) if isinstance(p, str) else p for p in perms ) assert all(isinstance(p, PermDefinition) for p in self.perms) self.target = target if target is not None else self.DEFAULT_TARGET self.permissions = perms self.any_perm = any_perm self.perm_checker = ( perm_checker if perm_checker is not None else _default_perm_checker ) self.obj_perm_checker = ( obj_perm_checker if obj_perm_checker is not None else _default_obj_perm_checker ) self.with_anonymous = with_anonymous self.with_superuser = with_superuser @functools.cached_property def schema_directive(self) -> object: key = "__strawberry_directive_class__" directive_class = getattr(self.__class__, key, None) if directive_class is None: @schema_directive( name=self.__class__.__name__, locations=self.SCHEMA_DIRECTIVE_LOCATIONS, description=self.SCHEMA_DIRECTIVE_DESCRIPTION, repeatable=True, ) class AutoDirective: permissions: list[PermDefinition] = strawberry.field( description="Required perms to access this resource.", default_factory=list, ) any: bool = strawberry.field( description="If any or all perms listed are required.", default=True, ) directive_class = AutoDirective return directive_class( permissions=list(self.perms), any=self.any_perm, ) @django_resolver(qs_hook=None) def resolve_for_user( self, resolver: Callable, user: Optional[UserType], *, info: Info, source: Any, ): if user is None or (self.with_anonymous and user.is_anonymous): raise DjangoNoPermission if ( self.with_superuser and user.is_active and getattr(user, "is_superuser", False) ): return resolver() return self.resolve_for_user_with_perms( resolver, user, info=info, source=source, ) def resolve_for_user_with_perms( self, resolver: Callable, user: Optional[UserType], *, info: Info, source: Any, ): if user is None: raise DjangoNoPermission if self.target == PermTarget.GLOBAL: if not self._has_perm(source, user, info=info): raise DjangoNoPermission retval = resolver() elif self.target == PermTarget.SOURCE: # Just call _resolve_obj, it will raise DjangoNoPermission # if the user doesn't have permission for it self._resolve_obj(source, user, source, info=info) retval = resolver() elif self.target == PermTarget.RETVAL: with with_perm_checker(self): obj = resolver() retval = self._resolve_obj(source, user, obj, info=info) else: assert_never(self.target) return retval def _get_cache( self, info: Info, user: UserType, ) -> dict[tuple[Hashable, ...], bool]: cache_key = "_strawberry_django_permissions_cache" cache = getattr(user, cache_key, None) if cache is None: cache = {} setattr(user, cache_key, cache) return cache def _has_perm( self, source: Any, user: UserType, *, info: Info, ) -> bool: cache = self._get_cache(info, user) # Maybe the result ended up in the cache in the meantime cache_key = (self.perms, self.any_perm) if cache_key in cache: return cache[cache_key] f = any if self.any_perm else all checker = self.perm_checker(info, user) has_perm = f(checker(p) for p in self.perms) cache[cache_key] = has_perm return has_perm def _resolve_obj( self, source: Any, user: UserType, obj: Any, *, info: Info, ) -> Any: context = perm_context.get() if context.is_safe: return obj if isinstance(obj, Iterable): return list(self._resolve_iterable_obj(source, user, obj, info=info)) cache = self._get_cache(info, user) cache_key = (self.perms, self.any_perm, obj) has_perm = cache.get(cache_key) if has_perm is None: if isinstance(obj, OperationInfo): has_perm = True else: f = any if self.any_perm else all checker = self.obj_perm_checker(info, user) has_perm = f(checker(p, obj) for p in self.perms) cache[cache_key] = has_perm if not has_perm: raise DjangoNoPermission return obj def _resolve_iterable_obj( self, source: Any, user: UserType, objs: Iterable[Any], *, info: Info, ) -> Any: cache = self._get_cache(info, user) f = any if self.any_perm else all checker = self.obj_perm_checker(info, user) for obj in objs: cache_key = (self.perms, self.any_perm, obj) has_perm = cache.get(cache_key) if has_perm is None: if isinstance(obj, OperationInfo): has_perm = True else: has_perm = f(checker(p, obj) for p in self.perms) cache[cache_key] = has_perm if has_perm: yield obj class HasSourcePerm(HasPerm): """Defines permissions required to access the given field at object level. This will check the permissions for the source object to access the given field. Unlike `HasRetvalPerm`, this uses the source value (the object where the field is defined) to resolve the field, which means that this cannot be used for source queries and types. Examples -------- To indicate that a field inside a `ProductType` can only be accessed if the user has "product.view_field" in it in the django system: >>> @gql.django.type(Product) ... class ProductType: ... some_field: str = strawberry.field( ... directives=[HasSourcePerm(".add_product")], ... ) """ DEFAULT_TARGET: ClassVar[PermTarget] = PermTarget.SOURCE SCHEMA_DIRECTIVE_DESCRIPTION: ClassVar[Optional[str]] = _desc( "Will check if the user has any/all permissions for the parent " "of this field to resolve this.", ) class HasRetvalPerm(HasPerm): """Defines permissions required to access the given object/field at object level. Given a `app` name, the user can access the decorated object/field if he has any of the permissions defined in this directive. Note that this depends on resolving the object to check the permissions specifically for that object, unlike `HasPerm` which checks it before resolving. Examples -------- To indicate that a field that returns a `ProductType` can only be accessed by someone who has "product.view_product" has "product.view_product" perm in the django system: >>> @strawberry.type ... class SomeType: ... product: ProductType = strawberry.field( ... directives=[HasRetvalPerm(".add_product")], ... ) """ DEFAULT_TARGET: ClassVar[PermTarget] = PermTarget.RETVAL SCHEMA_DIRECTIVE_DESCRIPTION: ClassVar[Optional[str]] = _desc( "Will check if the user has any/all permissions for the resolved " "value of this field before returning it.", ) strawberry-graphql-django-0.62.0/strawberry_django/py.typed000066400000000000000000000000001502405145400241120ustar00rootroot00000000000000strawberry-graphql-django-0.62.0/strawberry_django/queryset.py000066400000000000000000000032521502405145400246620ustar00rootroot00000000000000from __future__ import annotations import dataclasses from typing import TYPE_CHECKING, Any, TypeVar from django.db.models import Model from django.db.models.query import QuerySet if TYPE_CHECKING: from strawberry import Info from strawberry_django.relay.cursor_connection import OrderingDescriptor _M = TypeVar("_M", bound=Model) CONFIG_KEY = "_strawberry_django_config" @dataclasses.dataclass class StrawberryDjangoQuerySetConfig: optimized: bool = False optimized_by_prefetching: bool = False type_get_queryset_did_run: bool = False ordering_descriptors: list[OrderingDescriptor] | None = None def get_queryset_config(queryset: QuerySet) -> StrawberryDjangoQuerySetConfig: config = getattr(queryset, CONFIG_KEY, None) if config is None: setattr(queryset, CONFIG_KEY, (config := StrawberryDjangoQuerySetConfig())) return config def run_type_get_queryset( qs: QuerySet[_M], origin: Any, info: Info | None = None, ) -> QuerySet[_M]: config = get_queryset_config(qs) get_queryset = getattr(origin, "get_queryset", None) if get_queryset and not config.type_get_queryset_did_run: qs = get_queryset(qs, info) new_config = get_queryset_config(qs) new_config.type_get_queryset_did_run = True return qs _original_clone = QuerySet._clone # type: ignore def _qs_clone(self): config = get_queryset_config(self) cloned = _original_clone(self) setattr(cloned, CONFIG_KEY, dataclasses.replace(config)) return cloned # Monkey patch the QuerySet._clone method to make sure our config is copied # to the new QuerySet instance once it is cloned. QuerySet._clone = _qs_clone # type: ignore strawberry-graphql-django-0.62.0/strawberry_django/relay/000077500000000000000000000000001502405145400235415ustar00rootroot00000000000000strawberry-graphql-django-0.62.0/strawberry_django/relay/__init__.py000066400000000000000000000022061502405145400256520ustar00rootroot00000000000000import warnings from typing import TYPE_CHECKING, Any from .cursor_connection import ( DjangoCursorConnection, DjangoCursorEdge, OrderedCollectionCursor, OrderingDescriptor, apply_cursor_pagination, ) from .list_connection import DjangoListConnection from .utils import ( resolve_model_id, resolve_model_id_attr, resolve_model_node, resolve_model_nodes, ) if TYPE_CHECKING: from .list_connection import ListConnectionWithTotalCount # noqa: F401 __all__ = [ "DjangoCursorConnection", "DjangoCursorEdge", "DjangoListConnection", "OrderedCollectionCursor", "OrderingDescriptor", "apply_cursor_pagination", "resolve_model_id", "resolve_model_id_attr", "resolve_model_node", "resolve_model_nodes", ] def __getattr__(name: str) -> Any: if name == "ListConnectionWithTotalCount": warnings.warn( "`ListConnectionWithTotalCount` is deprecated, use `DjangoListConnection` instead.", DeprecationWarning, stacklevel=2, ) return DjangoListConnection raise AttributeError(f"module {__name__} has no attribute {name}") strawberry-graphql-django-0.62.0/strawberry_django/relay/cursor_connection.py000066400000000000000000000422251502405145400276540ustar00rootroot00000000000000import json from dataclasses import dataclass from json import JSONDecodeError from typing import Any, ClassVar, Optional, cast import strawberry from asgiref.sync import sync_to_async from django.core.exceptions import ValidationError from django.db import DEFAULT_DB_ALIAS, models from django.db.models import Expression, F, OrderBy, Q, QuerySet, Value, Window from django.db.models.constants import LOOKUP_SEP from django.db.models.expressions import Col from django.db.models.functions import RowNumber from django.db.models.sql.datastructures import BaseTable from strawberry import Info, relay from strawberry.relay import NodeType, PageInfo, from_base64 from strawberry.relay.types import NodeIterableType from strawberry.relay.utils import should_resolve_list_connection_edges from strawberry.types import get_object_definition from strawberry.types.base import StrawberryContainer from strawberry.utils.await_maybe import AwaitableOrValue from strawberry.utils.inspect import in_async_context from typing_extensions import Self from strawberry_django.pagination import apply_window_pagination, get_total_count from strawberry_django.queryset import get_queryset_config from strawberry_django.resolvers import django_resolver def _get_order_by(qs: QuerySet) -> list[OrderBy]: return [ expr for expr, _ in qs.query.get_compiler( using=qs._db or DEFAULT_DB_ALIAS # type: ignore ).get_order_by() ] @dataclass class OrderingDescriptor: attname: str order_by: OrderBy # we have to assume everything is nullable by default maybe_null: bool = True def get_comparator(self, value: Any, before: bool) -> Optional[Q]: if value is None: # 1. When nulls are first: # 1.1 there is nothing before "null" # 1.2 after "null" comes everything non-null # 2. When nulls are last: # 2.1 there is nothing after "null" # 2.2 before "null" comes everything non-null # => 1.1 and 2.1 require no checks # => 1.2 and 2.2 require an "is not null" check if bool(self.order_by.nulls_first) ^ before: return Q((f"{self.attname}{LOOKUP_SEP}isnull", False)) return None lookup = "lt" if before ^ self.order_by.descending else "gt" cmp = Q((f"{self.attname}{LOOKUP_SEP}{lookup}", value)) if self.maybe_null and bool(self.order_by.nulls_first) == before: # if nulls are first, "before any value" can also mean "is null" # if nulls are last, "after any value" can also mean "is null" cmp |= Q((f"{self.attname}{LOOKUP_SEP}isnull", True)) return cmp def get_eq(self, value) -> Q: if value is None: return Q((f"{self.attname}{LOOKUP_SEP}isnull", True)) return Q((f"{self.attname}{LOOKUP_SEP}exact", value)) def annotate_ordering_fields( qs: QuerySet, ) -> tuple[QuerySet, list[OrderingDescriptor], list[OrderBy]]: annotations = {} descriptors = [] new_defer = None new_only = None order_bys = _get_order_by(qs) pk_in_order = False for index, order_by in enumerate(order_bys): if isinstance(order_by.expression, Col) and isinstance( # Col.alias is missing from django-types qs.query.alias_map[order_by.expression.alias], # type: ignore BaseTable, ): field_name = order_by.expression.field.name # if it's a field in the base table, just make sure it is not deferred (e.g. by the optimizer) existing, defer = qs.query.deferred_loading if defer and field_name in existing: # Query is in "defer fields" mode and our field is being deferred if new_defer is None: new_defer = set(existing) new_defer.discard(field_name) elif not defer and field_name not in existing: # Query is in "only these fields" mode and our field is not in the set of fields if new_only is None: new_only = set(existing) new_only.add(field_name) descriptors.append( OrderingDescriptor( order_by.expression.field.attname, order_by, maybe_null=order_by.expression.field.null, ) ) if order_by.expression.field.primary_key: pk_in_order = True else: dynamic_field = f"_strawberry_order_field_{index}" annotations[dynamic_field] = order_by.expression descriptors.append(OrderingDescriptor(dynamic_field, order_by)) if new_defer is not None: # defer is additive, so clear it first qs = qs.defer(None).defer(*new_defer) elif new_only is not None: # only overwrites qs = qs.only(*new_only) if not pk_in_order: # Ensure we always have a clearly defined order by ordering by pk if it isn't in the order already # We cannot use QuerySet.order_by, because it validates the order expressions again, # but we're operating on the OrderBy expressions which have already been resolved by the compiler # In case the user has previously ordered by an aggregate like so: # qs.annotate(_c=Count("foo")).order_by("_c") # noqa: ERA001 # then the OrderBy we get here would trigger a ValidationError by QuerySet.order_by. # But we only want to append to the existing order (and the existing order must be valid already) # So this is safe. pk_order = F("pk").resolve_expression(qs.query).asc() order_bys.append(pk_order) descriptors.append(OrderingDescriptor("pk", pk_order, maybe_null=False)) qs = qs._chain() # type: ignore qs.query.order_by += (pk_order,) return qs.annotate(**annotations), descriptors, order_bys def build_tuple_compare( descriptors: list[OrderingDescriptor], cursor_values: list[Optional[str]], before: bool, ) -> Q: current = None for descriptor, field_value in zip(reversed(descriptors), reversed(cursor_values)): if field_value is None: value_expr = None else: output_field = descriptor.order_by.expression.output_field value_expr = Value(field_value, output_field=output_field) cmp = descriptor.get_comparator(value_expr, before) if current is None: current = cmp else: eq = descriptor.get_eq(value_expr) current = cmp | (eq & current) if cmp is not None else eq & current return current if current is not None else Q() class AttrHelper: pass def _extract_expression_value( model: models.Model, expr: Expression, attname: str ) -> Optional[str]: output_field = expr.output_field # Unfortunately Field.value_to_string operates on the object, not a direct value # So we have to potentially construct a fake object # If the output field's attname doesn't match, we have to construct a fake object # Additionally, the output field may not have an attname at all # if expressions are used field_attname = getattr(output_field, "attname", None) if not field_attname: # If the field doesn't have an attname, it's a dynamically constructed field, # for the purposes of output_field in an expression. Just set its attname, it doesn't hurt anything output_field.attname = field_attname = attname obj: Any if field_attname != attname: obj = AttrHelper() setattr(obj, output_field.attname, getattr(model, attname)) else: obj = model value = output_field.value_from_object(obj) if value is None: return None # value_to_string is missing from django-types return output_field.value_to_string(obj) # type: ignore def apply_cursor_pagination( qs: QuerySet, *, related_field_id: Optional[str] = None, info: Info, before: Optional[str], after: Optional[str], first: Optional[int], last: Optional[int], max_results: Optional[int], ) -> tuple[QuerySet, list[OrderingDescriptor]]: max_results = ( max_results if max_results is not None else info.schema.config.relay_max_results ) qs, ordering_descriptors, original_order_by = annotate_ordering_fields(qs) if after: after_cursor = OrderedCollectionCursor.from_cursor(after, ordering_descriptors) qs = qs.filter( build_tuple_compare(ordering_descriptors, after_cursor.field_values, False) ) if before: before_cursor = OrderedCollectionCursor.from_cursor( before, ordering_descriptors ) qs = qs.filter( build_tuple_compare(ordering_descriptors, before_cursor.field_values, True) ) slice_: Optional[slice] = None if first is not None and last is not None: if last > max_results: raise ValueError(f"Argument 'last' cannot be higher than {max_results}.") # if first and last are given, we have to # - reverse the order in the DB so we can use slicing to apply [:last], # otherwise we would have to know the total count to apply slicing from the end # - We still need to apply forward-direction [:first] slicing, and according to the Relay spec, # it shall happen before [:last] slicing. To do this, we use a window function with a RowNumber ordered # in the original direction, which is opposite the actual query order. # This query is likely not very efficient, but using last _and_ first together is discouraged by the # spec anyway qs = ( qs.reverse() .annotate( _strawberry_row_number_fwd=Window( RowNumber(), order_by=original_order_by, ), ) .filter( _strawberry_row_number_fwd__lte=first + 1, ) ) # we're overfetching by two, in both directions slice_ = slice(last + 2) elif first is not None: if first < 0: raise ValueError("Argument 'first' must be a non-negative integer.") if first > max_results: raise ValueError(f"Argument 'first' cannot be higher than {max_results}.") slice_ = slice(first + 1) elif last is not None: # when using last, optimize by reversing the QuerySet ordering in the DB, # then slicing from the end (which is now the start in QuerySet ordering) # and then iterating the results in reverse to restore the original order if last < 0: raise ValueError("Argument 'last' must be a non-negative integer.") if last > max_results: raise ValueError(f"Argument 'last' cannot be higher than {max_results}.") slice_ = slice(last + 1) qs = qs.reverse() if related_field_id is not None: # we always apply window pagination for nested connections, # because we want its total count annotation offset = slice_.start or 0 if slice_ is not None else 0 qs = apply_window_pagination( qs, related_field_id=related_field_id, offset=offset, limit=slice_.stop - offset if slice_ is not None else None, ) elif slice_ is not None: qs = qs[slice_] get_queryset_config(qs).ordering_descriptors = ordering_descriptors return qs, ordering_descriptors @dataclass class OrderedCollectionCursor: field_values: list[Any] @classmethod def from_model( cls, model: models.Model, descriptors: list[OrderingDescriptor] ) -> Self: values = [ _extract_expression_value( model, descriptor.order_by.expression, descriptor.attname ) for descriptor in descriptors ] return cls(field_values=values) @classmethod def from_cursor(cls, cursor: str, descriptors: list[OrderingDescriptor]) -> Self: type_, values_json = from_base64(cursor) if type_ != DjangoCursorEdge.CURSOR_PREFIX: raise ValueError("Invalid cursor") try: string_values = json.loads(values_json) except JSONDecodeError as e: raise ValueError("Invalid cursor") from e if ( not isinstance(string_values, list) or len(string_values) != len(descriptors) or any(not (v is None or isinstance(v, str)) for v in string_values) ): raise ValueError("Invalid cursor") try: decoded_values = [ d.order_by.expression.output_field.to_python(v) for d, v in zip(descriptors, string_values) ] except ValidationError as e: raise ValueError("Invalid cursor") from e return cls(decoded_values) def __str__(self): return json.dumps(self.field_values, separators=(",", ":")) @strawberry.type(name="CursorEdge", description="An edge in a connection.") class DjangoCursorEdge(relay.Edge[relay.NodeType]): CURSOR_PREFIX: ClassVar[str] = "orderedcursor" @strawberry.type( name="CursorConnection", description="A connection to a list of items." ) class DjangoCursorConnection(relay.Connection[relay.NodeType]): total_count_qs: strawberry.Private[Optional[QuerySet]] = None edges: list[DjangoCursorEdge[NodeType]] = strawberry.field( # type: ignore description="Contains the nodes in this connection" ) @strawberry.field(description="Total quantity of existing nodes.") @django_resolver def total_count(self) -> int: assert self.total_count_qs is not None return get_total_count(self.total_count_qs) @classmethod def resolve_connection( cls, nodes: NodeIterableType[NodeType], *, info: Info, before: Optional[str] = None, after: Optional[str] = None, first: Optional[int] = None, last: Optional[int] = None, max_results: Optional[int] = None, **kwargs: Any, ) -> AwaitableOrValue[Self]: from strawberry_django.optimizer import is_optimized_by_prefetching if not isinstance(nodes, QuerySet): raise TypeError("DjangoCursorConnection requires a QuerySet") total_count_qs: QuerySet = nodes qs: QuerySet if not is_optimized_by_prefetching(nodes): qs, ordering_descriptors = apply_cursor_pagination( nodes, info=info, before=before, after=after, first=first, last=last, max_results=max_results, ) else: qs = nodes ordering_descriptors = get_queryset_config(qs).ordering_descriptors assert ordering_descriptors is not None type_def = get_object_definition(cls) assert type_def field_def = type_def.get_field("edges") assert field_def field = field_def.resolve_type(type_definition=type_def) while isinstance(field, StrawberryContainer): field = field.of_type edge_class = cast("DjangoCursorEdge[NodeType]", field) if not should_resolve_list_connection_edges(info): return cls( edges=[], total_count_qs=total_count_qs, page_info=PageInfo( start_cursor=None, end_cursor=None, has_previous_page=False, has_next_page=False, ), ) def finish_resolving(): nonlocal qs has_previous_page = has_next_page = False results = list(qs) if first is not None: if last is None: has_next_page = len(results) > first results = results[:first] # we're paginating forwards _and_ backwards # remove the (potentially) overfetched row in the forwards direction first elif ( results and getattr(results[0], "_strawberry_row_number_fwd", 0) > first ): has_next_page = True results = results[1:] if last is not None: has_previous_page = len(results) > last results = results[:last] it = reversed(results) if last is not None else results edges = [ edge_class.resolve_edge( cls.resolve_node(v, info=info, **kwargs), cursor=OrderedCollectionCursor.from_model(v, ordering_descriptors), ) for v in it ] return cls( edges=edges, total_count_qs=total_count_qs, page_info=PageInfo( start_cursor=edges[0].cursor if edges else None, end_cursor=edges[-1].cursor if edges else None, has_previous_page=has_previous_page, has_next_page=has_next_page, ), ) if in_async_context() and qs._result_cache is None: # type: ignore return sync_to_async(finish_resolving)() return finish_resolving() strawberry-graphql-django-0.62.0/strawberry_django/relay/list_connection.py000066400000000000000000000125511502405145400273110ustar00rootroot00000000000000import inspect import warnings from collections.abc import Sized from typing import TYPE_CHECKING, Any, Optional, cast import strawberry from django.db import models from strawberry import Info, relay from strawberry.relay.types import NodeIterableType from strawberry.types import get_object_definition from strawberry.types.base import StrawberryContainer from strawberry.utils.await_maybe import AwaitableOrValue from typing_extensions import Self, deprecated from strawberry_django.pagination import get_total_count from strawberry_django.resolvers import django_resolver @strawberry.type(name="Connection", description="A connection to a list of items.") class DjangoListConnection(relay.ListConnection[relay.NodeType]): nodes: strawberry.Private[Optional[NodeIterableType[relay.NodeType]]] = None @strawberry.field(description="Total quantity of existing nodes.") @django_resolver def total_count(self) -> Optional[int]: assert self.nodes is not None if isinstance(self.nodes, models.QuerySet): return get_total_count(self.nodes) return len(self.nodes) if isinstance(self.nodes, Sized) else None @classmethod def resolve_connection( cls, nodes: NodeIterableType[relay.NodeType], *, info: Info, before: Optional[str] = None, after: Optional[str] = None, first: Optional[int] = None, last: Optional[int] = None, **kwargs: Any, ) -> AwaitableOrValue[Self]: from strawberry_django.optimizer import is_optimized_by_prefetching if isinstance(nodes, models.QuerySet) and is_optimized_by_prefetching(nodes): try: conn = cls.resolve_connection_from_cache( nodes, info=info, before=before, after=after, first=first, last=last, **kwargs, ) except AttributeError: warnings.warn( ( "Pagination annotations not found, falling back to QuerySet resolution. " "This might cause N+1 issues..." ), RuntimeWarning, stacklevel=2, ) else: conn = cast("Self", conn) conn.nodes = nodes return conn conn = super().resolve_connection( nodes, info=info, before=before, after=after, first=first, last=last, **kwargs, ) if inspect.isawaitable(conn): async def wrapper(): resolved = await conn resolved.nodes = nodes return resolved return wrapper() conn = cast("Self", conn) conn.nodes = nodes return conn @classmethod def resolve_connection_from_cache( cls, nodes: NodeIterableType[relay.NodeType], *, info: Info, before: Optional[str] = None, after: Optional[str] = None, first: Optional[int] = None, last: Optional[int] = None, **kwargs: Any, ) -> AwaitableOrValue[Self]: """Resolve the connection from the prefetched cache. NOTE: This will try to access `node._strawberry_total_count` and `node._strawberry_row_number` attributes from the nodes. If they don't exist, `AttriuteError` will be raised, meaning that we should fallback to the queryset resolution. """ result = nodes._result_cache # type: ignore type_def = get_object_definition(cls, strict=True) field_def = type_def.get_field("edges") assert field_def field = field_def.resolve_type(type_definition=type_def) while isinstance(field, StrawberryContainer): field = field.of_type edge_class = cast("relay.Edge[relay.NodeType]", field) edges: list[relay.Edge] = [ edge_class.resolve_edge( cls.resolve_node(node, info=info, **kwargs), cursor=node._strawberry_row_number - 1, ) for node in result ] has_previous_page = result[0]._strawberry_row_number > 1 if result else False has_next_page = ( result[-1]._strawberry_row_number < result[-1]._strawberry_total_count if result else False ) return cls( edges=edges, page_info=relay.PageInfo( start_cursor=edges[0].cursor if edges else None, end_cursor=edges[-1].cursor if edges else None, has_previous_page=has_previous_page, has_next_page=has_next_page, ), ) if TYPE_CHECKING: @deprecated( "`ListConnectionWithTotalCount` is deprecated, use `DjangoListConnection` instead." ) class ListConnectionWithTotalCount(DjangoListConnection): ... def __getattr__(name: str) -> Any: if name == "ListConnectionWithTotalCount": warnings.warn( "`ListConnectionWithTotalCount` is deprecated, use `DjangoListConnection` instead.", DeprecationWarning, stacklevel=2, ) return DjangoListConnection raise AttributeError(f"module {__name__} has no attribute {name}") strawberry-graphql-django-0.62.0/strawberry_django/relay/utils.py000066400000000000000000000245461502405145400252660ustar00rootroot00000000000000import functools import inspect from collections.abc import Iterable from typing import ( Callable, Optional, TypeVar, Union, cast, overload, ) import strawberry from asgiref.sync import sync_to_async from django.db import models from strawberry import relay from strawberry.relay.exceptions import NodeIDAnnotationError from strawberry.types.info import Info from strawberry.utils.await_maybe import AwaitableOrValue from typing_extensions import Literal from strawberry_django.queryset import run_type_get_queryset from strawberry_django.resolvers import django_getattr, django_resolver from strawberry_django.utils.typing import ( WithStrawberryDjangoObjectDefinition, get_django_definition, ) _T = TypeVar("_T") _M = TypeVar("_M", bound=models.Model) __all__ = [ "resolve_model_id", "resolve_model_id_attr", "resolve_model_node", "resolve_model_nodes", ] def get_node_caster(origin: Optional[type]) -> Callable[[_T], _T]: if origin is None: return lambda node: node return functools.partial(strawberry.cast, origin) @overload def resolve_model_nodes( source: Union[ type[WithStrawberryDjangoObjectDefinition], type[relay.Node], type[_M], ], *, info: Optional[Info] = None, node_ids: Iterable[Union[str, relay.GlobalID]], required: Literal[True], filter_perms: bool = False, ) -> AwaitableOrValue[Iterable[_M]]: ... @overload def resolve_model_nodes( source: Union[ type[WithStrawberryDjangoObjectDefinition], type[relay.Node], type[_M], ], *, info: Optional[Info] = None, node_ids: None = None, required: Literal[True], filter_perms: bool = False, ) -> AwaitableOrValue[models.QuerySet[_M]]: ... @overload def resolve_model_nodes( source: Union[ type[WithStrawberryDjangoObjectDefinition], type[relay.Node], type[_M], ], *, info: Optional[Info] = None, node_ids: Iterable[Union[str, relay.GlobalID]], required: Literal[False], filter_perms: bool = False, ) -> AwaitableOrValue[Iterable[Optional[_M]]]: ... @overload def resolve_model_nodes( source: Union[ type[WithStrawberryDjangoObjectDefinition], type[relay.Node], type[_M], ], *, info: Optional[Info] = None, node_ids: None = None, required: Literal[False], filter_perms: bool = False, ) -> AwaitableOrValue[Optional[models.QuerySet[_M]]]: ... @overload def resolve_model_nodes( source: Union[ type[WithStrawberryDjangoObjectDefinition], type[relay.Node], type[_M], ], *, info: Optional[Info] = None, node_ids: Optional[Iterable[Union[str, relay.GlobalID]]] = None, required: bool = False, filter_perms: bool = False, ) -> AwaitableOrValue[ Union[ Iterable[_M], models.QuerySet[_M], Iterable[Optional[_M]], Optional[models.QuerySet[_M]], ] ]: ... def resolve_model_nodes( source, *, info=None, node_ids=None, required=False, filter_perms=False, ) -> AwaitableOrValue[ Union[ Iterable[_M], models.QuerySet[_M], Iterable[Optional[_M]], Optional[models.QuerySet[_M]], ] ]: """Resolve model nodes, ensuring those are prefetched in a sync context. Args: ---- source: The source model or the model type that implements the `Node` interface info: Optional gql execution info. Make sure to always provide this or otherwise, the queryset cannot be optimized in case DjangoOptimizerExtension is enabled. This will also be used for `is_awaitable` check. node_ids: Optional filter by those node_ids instead of retrieving everything required: If `True`, all `node_ids` requested must exist. If they don't, an error must be raised. If `False`, missing nodes should be returned as `None`. It only makes sense when passing a list of `node_ids`, otherwise it will should ignored. Returns: ------- The resolved queryset, already prefetched from the database """ from strawberry_django import optimizer # avoid circular import from strawberry_django.permissions import filter_with_perms if issubclass(source, models.Model): origin = None else: origin = source django_type = get_django_definition(source, strict=True) source = cast("type[_M]", django_type.model) qs = cast("models.QuerySet[_M]", source._default_manager.all()) qs = run_type_get_queryset(qs, origin, info) id_attr = cast("relay.Node", origin).resolve_id_attr() if node_ids is not None: qs = qs.filter( **{ f"{id_attr}__in": [ i.node_id if isinstance(i, relay.GlobalID) else i for i in node_ids ], }, ) extra_args = {} if info is not None: if filter_perms: qs = filter_with_perms(qs, info) # Connection will filter the results when its is being resolved. # We don't want to fetch everything before it does that return_type = info.return_type if isinstance(return_type, type) and issubclass(return_type, relay.Connection): extra_args["qs_hook"] = lambda qs: qs ext = optimizer.optimizer.get() if ext is not None: # If optimizer extension is enabled, optimize this queryset qs = ext.optimize(qs, info=info) retval = cast( "AwaitableOrValue[models.QuerySet[_M]]", django_resolver(lambda _qs: _qs, **extra_args)(qs), ) if not node_ids: return retval def map_results(results: models.QuerySet[_M]) -> list[_M]: node_caster = get_node_caster(origin) results_map = {str(getattr(obj, id_attr)): node_caster(obj) for obj in results} retval: list[Optional[_M]] = [] for node_id in node_ids: if required: retval.append(results_map[str(node_id)]) else: retval.append(results_map.get(str(node_id), None)) return retval # type: ignore if inspect.isawaitable(retval): async def async_resolver(): return await sync_to_async(map_results)(await retval) return async_resolver() return map_results(retval) @overload def resolve_model_node( source: Union[ type[WithStrawberryDjangoObjectDefinition], type[relay.Node], type[_M], ], node_id: Union[str, relay.GlobalID], *, info: Optional[Info] = ..., required: Literal[False] = ..., filter_perms: bool = False, ) -> AwaitableOrValue[Optional[_M]]: ... @overload def resolve_model_node( source: Union[ type[WithStrawberryDjangoObjectDefinition], type[relay.Node], type[_M], ], node_id: Union[str, relay.GlobalID], *, info: Optional[Info] = ..., required: Literal[True], filter_perms: bool = False, ) -> AwaitableOrValue[_M]: ... def resolve_model_node( source, node_id, *, info: Optional[Info] = None, required=False, filter_perms=False, ): """Resolve model nodes, ensuring it is retrieved in a sync context. Args: ---- source: The source model or the model type that implements the `Node` interface node_id: The node it to retrieve the model from info: Optional gql execution info. Make sure to always provide this or otherwise, the queryset cannot be optimized in case DjangoOptimizerExtension is enabled. This will also be used for `is_awaitable` check. required: If the return value is required to exist. If true, `qs.get()` will be used, which might raise `model.DoesNotExist` error if the node doesn't exist. Otherwise, `qs.first()` will be used, which might return None. Returns: ------- The resolved node, already prefetched from the database """ from strawberry_django import optimizer # avoid circular import from strawberry_django.permissions import filter_with_perms if issubclass(source, models.Model): origin = None else: origin = source django_type = get_django_definition(source, strict=True) source = cast("type[models.Model]", django_type.model) if isinstance(node_id, relay.GlobalID): node_id = node_id.node_id id_attr = cast("relay.Node", origin).resolve_id_attr() qs = source._default_manager.all() qs = run_type_get_queryset(qs, origin, info) qs = qs.filter(**{id_attr: node_id}) if info is not None: if filter_perms: qs = filter_with_perms(qs, info) ext = optimizer.optimizer.get() if ext is not None: # If optimizer extension is enabled, optimize this queryset qs = ext.optimize(qs, info=info) node_caster = get_node_caster(origin) return django_resolver(lambda: node_caster(qs.get() if required else qs.first()))() def resolve_model_id_attr(source: type) -> str: """Resolve the model id, ensuring it is retrieved in a sync context. Args: ---- source: The source model type that implements the `Node` interface Returns: ------- The resolved id attr """ try: id_attr = super(source, source).resolve_id_attr() # type: ignore except NodeIDAnnotationError: id_attr = "pk" return id_attr def resolve_model_id( source: Union[ type[WithStrawberryDjangoObjectDefinition], type[relay.Node], type[_M], ], root: models.Model, *, info: Optional[Info] = None, ) -> AwaitableOrValue[str]: """Resolve the model id, ensuring it is retrieved in a sync context. Args: ---- source: The source model or the model type that implements the `Node` interface root: The source model object. Returns: ------- The resolved object id """ id_attr = cast("relay.Node", source).resolve_id_attr() assert isinstance(root, models.Model) if id_attr == "pk": pk = root.__class__._meta.pk assert pk id_attr = pk.attname assert id_attr try: # Prefer to retrieve this from the cache return str(root.__dict__[id_attr]) except KeyError: return django_getattr(root, id_attr) strawberry-graphql-django-0.62.0/strawberry_django/resolvers.py000066400000000000000000000144311502405145400250260ustar00rootroot00000000000000from __future__ import annotations import contextvars import functools import inspect from typing import TYPE_CHECKING, Any, TypeVar, overload from asgiref.sync import sync_to_async from django.db import models from django.db.models.fields.files import FileDescriptor from django.db.models.manager import BaseManager from strawberry.utils.inspect import in_async_context from typing_extensions import ParamSpec if TYPE_CHECKING: from collections.abc import Callable from graphql.pyutils import AwaitableOrValue _SENTINEL = object() _R = TypeVar("_R") _P = ParamSpec("_P") _M = TypeVar("_M", bound=models.Model) resolving_async: contextvars.ContextVar[bool] = contextvars.ContextVar( "resolving-async", default=False, ) def default_qs_hook(qs: models.QuerySet[_M]) -> models.QuerySet[_M]: if isinstance(qs, list): # return sliced queryset as-is return qs # FIXME: We probably won't need this anymore when we can use graphql-core 3.3.0+ # as its `complete_list_value` gives a preference to async iteration it if is # provided by the object. # This is what QuerySet does internally to fetch results. # After this, iterating over the queryset should be async safe if qs._result_cache is None: # type: ignore qs._fetch_all() # type: ignore return qs @overload def django_resolver( f: Callable[_P, _R], *, qs_hook: Callable[[models.QuerySet[_M]], Any] | None = default_qs_hook, except_as_none: tuple[type[Exception], ...] | None = None, ) -> Callable[_P, AwaitableOrValue[_R]]: ... @overload def django_resolver( *, qs_hook: Callable[[models.QuerySet[_M]], Any] | None = default_qs_hook, except_as_none: tuple[type[Exception], ...] | None = None, ) -> Callable[[Callable[_P, _R]], Callable[_P, AwaitableOrValue[_R]]]: ... def django_resolver( f=None, *, qs_hook: Callable[[models.QuerySet[_M]], Any] | None = default_qs_hook, except_as_none: tuple[type[Exception], ...] | None = None, ): """Django resolver for handling both sync and async. This decorator is used to make sure that resolver is always called from sync context. sync_to_async helper in used if function is called from async context. This is useful especially with Django ORM, which does not support async. Coroutines are not wrapped. """ def wrapper(resolver): if inspect.iscoroutinefunction(resolver) or inspect.isasyncgenfunction( resolver, ): return resolver def sync_resolver(*args, **kwargs): try: retval = resolver(*args, **kwargs) if callable(retval): retval = retval() if isinstance(retval, BaseManager): retval = retval.all() if qs_hook is not None and isinstance(retval, models.QuerySet): retval = qs_hook(retval) except Exception as e: if except_as_none is not None and isinstance(e, except_as_none): return None raise return retval @sync_to_async def async_resolver(*args, **kwargs): token = resolving_async.set(True) try: return sync_resolver(*args, **kwargs) finally: resolving_async.reset(token) @functools.wraps(resolver) def inner_wrapper(*args, **kwargs): f = ( async_resolver if in_async_context() and not resolving_async.get() else sync_resolver ) return f(*args, **kwargs) return inner_wrapper if f is not None: return wrapper(f) return wrapper @django_resolver(qs_hook=None) def django_fetch(qs: models.QuerySet[_M]) -> models.QuerySet[_M]: return default_qs_hook(qs) @overload def django_getattr( obj: Any, name: str, *, qs_hook: Callable[[models.QuerySet[_M]], Any] = default_qs_hook, except_as_none: tuple[type[Exception], ...] | None = None, empty_file_descriptor_as_null: bool = False, ) -> AwaitableOrValue[Any]: ... @overload def django_getattr( obj: Any, name: str, default: Any, *, qs_hook: Callable[[models.QuerySet[_M]], Any] = default_qs_hook, except_as_none: tuple[type[Exception], ...] | None = None, empty_file_descriptor_as_null: bool = False, ) -> AwaitableOrValue[Any]: ... def django_getattr( obj: Any, name: str, default: Any = _SENTINEL, *, qs_hook: Callable[[models.QuerySet[_M]], Any] = default_qs_hook, except_as_none: tuple[type[Exception], ...] | None = None, empty_file_descriptor_as_null: bool = False, ): return django_resolver( _django_getattr, qs_hook=qs_hook, except_as_none=except_as_none, )( obj, name, default, empty_file_descriptor_as_null=empty_file_descriptor_as_null, ) def _django_getattr( obj: Any, name: str, default: Any = _SENTINEL, *, empty_file_descriptor_as_null: bool = False, ): args = (default,) if default is not _SENTINEL else () result = getattr(obj, name, *args) if empty_file_descriptor_as_null and isinstance(result, FileDescriptor): result = None return result def resolve_base_manager(manager: BaseManager) -> Any: if (result_instance := getattr(manager, "instance", None)) is not None: prefetched_cache = getattr(result_instance, "_prefetched_objects_cache", {}) # Both ManyRelatedManager and RelatedManager are defined inside functions, which # prevents us from importing and checking isinstance on them directly. try: # ManyRelatedManager return prefetched_cache[manager.prefetch_cache_name] # type: ignore except (AttributeError, KeyError): try: # RelatedManager result_field = manager.field # type: ignore cache_name = ( # 5.1+ uses "cache_name" instead of "get_cache_name() getattr(result_field.remote_field, "cache_name", None) or result_field.remote_field.get_cache_name() ) return prefetched_cache[cache_name] except (AttributeError, KeyError): pass return manager.all() strawberry-graphql-django-0.62.0/strawberry_django/routers.py000066400000000000000000000045431502405145400245100ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING from channels.auth import AuthMiddlewareStack from channels.routing import ProtocolTypeRouter, URLRouter from channels.security.websocket import AllowedHostsOriginValidator from django.urls import URLPattern, URLResolver, re_path from strawberry.channels.handlers.http_handler import GraphQLHTTPConsumer from strawberry.channels.handlers.ws_handler import GraphQLWSConsumer if TYPE_CHECKING: from django.core.handlers.asgi import ASGIHandler from strawberry.schema import BaseSchema class AuthGraphQLProtocolTypeRouter(ProtocolTypeRouter): """Convenience class to set up GraphQL on both HTTP and Websocket. This convenience class will include AuthMiddlewareStack and the AllowedHostsOriginValidator to ensure you have user object available. ``` from strawberry_django.routers import AuthGraphQLProtocolTypeRouter from django.core.asgi import get_asgi_application. django_asgi = get_asgi_application() from myapi import schema application = AuthGraphQLProtocolTypeRouter( schema, django_application=django_asgi, ) ``` This will route all requests to /graphql on either HTTP or websockets to us, and everything else to the Django application. """ def __init__( self, schema: BaseSchema, django_application: ASGIHandler | None = None, url_pattern: str = "^graphql", ): http_urls: list[URLPattern | URLResolver] = [ re_path(url_pattern, GraphQLHTTPConsumer.as_asgi(schema=schema)), ] if django_application is not None: http_urls.append(re_path(r"^", django_application)) super().__init__( { "http": AuthMiddlewareStack( URLRouter( http_urls, ), ), "websocket": AllowedHostsOriginValidator( AuthMiddlewareStack( URLRouter( [ re_path( url_pattern, GraphQLWSConsumer.as_asgi(schema=schema), ), ], ), ), ), }, ) strawberry-graphql-django-0.62.0/strawberry_django/settings.py000066400000000000000000000052431502405145400246430ustar00rootroot00000000000000"""Code for interacting with Django settings.""" from typing import Optional, cast from django.conf import settings from typing_extensions import TypedDict class StrawberryDjangoSettings(TypedDict): """Dictionary defining the shape `settings.STRAWBERRY_DJANGO` should have. All settings are optional and have defaults as described in their docstrings and defined in `DEFAULT_DJANGO_SETTINGS`. """ #: If True, field descriptions will be fetched from the #: corresponding model field's `help_text` attribute. FIELD_DESCRIPTION_FROM_HELP_TEXT: bool #: If True, type descriptions will be fetched from the #: corresponding model model's docstring. TYPE_DESCRIPTION_FROM_MODEL_DOCSTRING: bool #: If True, fields with `choices` will have automatically generate #: an enum of possibilities instead of being exposed as `String` GENERATE_ENUMS_FROM_CHOICES: bool #: Set a custom default name for CUD mutations input type. MUTATIONS_DEFAULT_ARGUMENT_NAME: str #: If True, mutations will default to handling django errors by default #: when no option is passed to the field itself. MUTATIONS_DEFAULT_HANDLE_ERRORS: bool #: If True, `auto` fields that refer to model ids will be mapped to #: `relay.GlobalID` instead of `strawberry.ID` for types and filters. MAP_AUTO_ID_AS_GLOBAL_ID: bool #: Set a primary key default field name for Django CRUD resolvers. DEFAULT_PK_FIELD_NAME: str #: If True, deprecated way of using filters will be working USE_DEPRECATED_FILTERS: bool #: The default limit for pagination when not provided. Can be set to `None` #: to set it to unlimited. PAGINATION_DEFAULT_LIMIT: Optional[int] #: If True, filters used in mutations can be omitted ALLOW_MUTATIONS_WITHOUT_FILTERS: bool DEFAULT_DJANGO_SETTINGS = StrawberryDjangoSettings( FIELD_DESCRIPTION_FROM_HELP_TEXT=False, TYPE_DESCRIPTION_FROM_MODEL_DOCSTRING=False, GENERATE_ENUMS_FROM_CHOICES=False, MUTATIONS_DEFAULT_ARGUMENT_NAME="data", MUTATIONS_DEFAULT_HANDLE_ERRORS=False, MAP_AUTO_ID_AS_GLOBAL_ID=False, DEFAULT_PK_FIELD_NAME="pk", USE_DEPRECATED_FILTERS=False, PAGINATION_DEFAULT_LIMIT=100, ALLOW_MUTATIONS_WITHOUT_FILTERS=False, ) def strawberry_django_settings() -> StrawberryDjangoSettings: """Get strawberry django settings. Return the dictionary from `settings.STRAWBERRY_DJANGO`, with defaults for missing keys. Preferred to direct access for the type hints and defaults. """ defaults = DEFAULT_DJANGO_SETTINGS return cast( "StrawberryDjangoSettings", {**defaults, **getattr(settings, "STRAWBERRY_DJANGO", {})}, ) strawberry-graphql-django-0.62.0/strawberry_django/templates/000077500000000000000000000000001502405145400244235ustar00rootroot00000000000000strawberry-graphql-django-0.62.0/strawberry_django/templates/strawberry_django/000077500000000000000000000000001502405145400301515ustar00rootroot00000000000000strawberry-graphql-django-0.62.0/strawberry_django/templates/strawberry_django/debug_toolbar.html000066400000000000000000000026451502405145400336560ustar00rootroot00000000000000 strawberry-graphql-django-0.62.0/strawberry_django/test/000077500000000000000000000000001502405145400234045ustar00rootroot00000000000000strawberry-graphql-django-0.62.0/strawberry_django/test/__init__.py000066400000000000000000000000001502405145400255030ustar00rootroot00000000000000strawberry-graphql-django-0.62.0/strawberry_django/test/client.py000066400000000000000000000060101502405145400252310ustar00rootroot00000000000000import contextlib import warnings from typing import TYPE_CHECKING, Any, Optional, cast from asgiref.sync import sync_to_async from django.contrib.auth.base_user import AbstractBaseUser from django.test.client import AsyncClient, Client from strawberry.test import BaseGraphQLTestClient from strawberry.test.client import Response from typing_extensions import override if TYPE_CHECKING: from collections.abc import Awaitable class TestClient(BaseGraphQLTestClient): __test__ = False def __init__(self, path: str, client: Optional[Client] = None): self.path = path super().__init__(client or Client()) @property def client(self) -> Client: return self._client def request( self, body: dict[str, object], headers: Optional[dict[str, object]] = None, files: Optional[dict[str, object]] = None, ): kwargs: dict[str, object] = {"data": body, "headers": headers} if files: kwargs["format"] = "multipart" else: kwargs["content_type"] = "application/json" return self.client.post( self.path, **kwargs, # type: ignore ) @contextlib.contextmanager def login(self, user: AbstractBaseUser): self.client.force_login(user) yield self.client.logout() class AsyncTestClient(TestClient): def __init__(self, path: str, client: Optional[AsyncClient] = None): super().__init__( path, client or AsyncClient(), # type: ignore ) @property def client(self) -> AsyncClient: # type: ignore[reportIncompatibleMethodOverride] return self._client @override async def query( self, query: str, variables: Optional[dict[str, Any]] = None, headers: Optional[dict[str, object]] = None, asserts_errors: Optional[bool] = None, files: Optional[dict[str, object]] = None, assert_no_errors: Optional[bool] = True, ) -> Response: body = self._build_body(query, variables, files) resp = await cast("Awaitable", self.request(body, headers, files)) data = self._decode(resp, type="multipart" if files else "json") response = Response( errors=data.get("errors"), data=data.get("data"), extensions=data.get("extensions"), ) if asserts_errors is not None: warnings.warn( "The `asserts_errors` argument has been renamed to `assert_no_errors`", DeprecationWarning, stacklevel=2, ) assert_no_errors = ( assert_no_errors if asserts_errors is None else asserts_errors ) if assert_no_errors: assert response.errors is None return response @contextlib.asynccontextmanager async def login(self, user: AbstractBaseUser): # type: ignore await sync_to_async(self.client.force_login)(user) yield await sync_to_async(self.client.logout)() strawberry-graphql-django-0.62.0/strawberry_django/type.py000066400000000000000000000530631502405145400237670ustar00rootroot00000000000000import builtins import copy import dataclasses import functools import inspect import sys import types from collections.abc import Callable, Collection, Sequence from typing import ( Generic, Optional, TypeVar, Union, cast, ) import strawberry from django.core.exceptions import FieldDoesNotExist from django.db.models import ForeignKey from django.db.models.base import Model from django.db.models.fields.reverse_related import ManyToManyRel, ManyToOneRel from strawberry import UNSET, relay from strawberry.annotation import StrawberryAnnotation from strawberry.exceptions import ( MissingFieldAnnotationError, ) from strawberry.types import get_object_definition from strawberry.types.base import WithStrawberryObjectDefinition from strawberry.types.cast import get_strawberry_type_cast from strawberry.types.field import StrawberryField from strawberry.types.private import is_private from strawberry.utils.deprecations import DeprecatedDescriptor from typing_extensions import Literal, Self, dataclass_transform from strawberry_django.optimizer import OptimizerStore from strawberry_django.relay import ( resolve_model_id, resolve_model_id_attr, resolve_model_node, resolve_model_nodes, ) from strawberry_django.resolvers import django_resolver from strawberry_django.utils.typing import ( AnnotateType, PrefetchType, TypeOrMapping, TypeOrSequence, WithStrawberryDjangoObjectDefinition, get_annotations, is_auto, ) from .descriptors import ModelProperty from .fields.field import StrawberryDjangoField from .fields.field import field as _field from .fields.types import get_model_field, resolve_model_field_name from .settings import strawberry_django_settings as django_settings __all__ = [ "StrawberryDjangoDefinition", "input", "interface", "partial", "type", ] _T = TypeVar("_T", bound=type) _O = TypeVar("_O", bound=type[WithStrawberryObjectDefinition]) _M = TypeVar("_M", bound=Model) def _process_type( cls: _T, model: type[Model], *, field_cls: type[StrawberryDjangoField] = StrawberryDjangoField, filters: Optional[type] = None, order: Optional[type] = None, ordering: Optional[type] = None, pagination: bool = False, partial: bool = False, is_filter: Union[Literal["lookups"], bool] = False, only: Optional[TypeOrSequence[str]] = None, select_related: Optional[TypeOrSequence[str]] = None, prefetch_related: Optional[TypeOrSequence[PrefetchType]] = None, annotate: Optional[TypeOrMapping[AnnotateType]] = None, disable_optimization: bool = False, fields: Optional[Union[list[str], Literal["__all__"]]] = None, exclude: Optional[list[str]] = None, **kwargs, ) -> _T: is_input = kwargs.get("is_input", False) if fields == "__all__": model_fields = list(model._meta.fields) elif isinstance(fields, Collection): model_fields = [f for f in model._meta.fields if f.name in fields] elif isinstance(exclude, Collection) and len(exclude) > 0: model_fields = [f for f in model._meta.fields if f.name not in exclude] else: model_fields = [] # If MAP_AUTO_ID_AS_GLOBAL_ID is True, we can no longer set the id # from fields or it will override the GlobalID and return the default # django id instead in the query-result. This adjustment however still # does not fix if the id was set to auto manually on the ModelType. if django_settings().get("MAP_AUTO_ID_AS_GLOBAL_ID", False): model_fields = [f for f in model_fields if f.name != "id"] existing_annotations = get_annotations(cls) cls_annotations = cls.__dict__.get("__annotations__", {}) cls.__annotations__ = cls_annotations for f in model_fields: if existing_annotations.get(f.name): continue cls_annotations[f.name] = strawberry.auto if is_filter: cls_annotations.update( { "AND": existing_annotations.get("AND").annotation # type: ignore if existing_annotations.get("AND") else Optional[Self], # type: ignore "OR": existing_annotations.get("OR").annotation # type: ignore if existing_annotations.get("OR") else Optional[Self], # type: ignore "NOT": existing_annotations.get("NOT").annotation # type: ignore if existing_annotations.get("NOT") else Optional[Self], # type: ignore "DISTINCT": existing_annotations.get("DISTINCT").annotation # type: ignore if existing_annotations.get("DISTINCT") else Optional[bool], }, ) django_type = StrawberryDjangoDefinition( origin=cast("builtins.type[WithStrawberryObjectDefinition]", cls), model=model, field_cls=field_cls, is_partial=partial, is_input=is_input, is_filter=is_filter, filters=filters, order=order, ordering=ordering, pagination=pagination, disable_optimization=disable_optimization, store=OptimizerStore.with_hints( only=only, select_related=select_related, prefetch_related=prefetch_related, annotate=annotate, ), ) auto_fields: set[str] = set() for field_name, field_annotation in get_annotations(cls).items(): annotation = field_annotation.annotation if is_private(annotation): continue if is_auto(annotation): auto_fields.add(field_name) # FIXME: For input types it is important to set the default value to UNSET # Is there a better way of doing this? if is_input: # First check if the field is defined in the class. If it is, # then we just need to set its default value to UNSET in case # it is MISSING if field_name in cls.__dict__: field = cls.__dict__[field_name] if ( isinstance(field, dataclasses.Field) and field.default is dataclasses.MISSING ): field.default = UNSET if isinstance(field, StrawberryField): field.default_value = UNSET continue if not hasattr(cls, field_name): base_field = getattr(cls, "__dataclass_fields__", {}).get(field_name) if base_field is not None and isinstance(base_field, StrawberryField): new_field = copy.copy(base_field) else: new_field = _field(default=UNSET) cls_annotations[field_name] = field_annotation.raw_annotation new_field.default = UNSET if isinstance(base_field, StrawberryField): new_field.default_value = UNSET setattr(cls, field_name, new_field) # Make sure model is also considered a "virtual subclass" of cls if "is_type_of" not in cls.__dict__: def is_type_of(obj, info): if (type_cast := get_strawberry_type_cast(obj)) is not None: return type_cast is cls return isinstance(obj, (cls, model)) cls.is_type_of = is_type_of # Default querying methods for relay if issubclass(cls, relay.Node): for attr, func in [ ("resolve_id", resolve_model_id), ("resolve_id_attr", resolve_model_id_attr), ("resolve_node", resolve_model_node), ("resolve_nodes", resolve_model_nodes), ]: existing_resolver = getattr(cls, attr, None) if ( existing_resolver is None or existing_resolver.__func__ is getattr(relay.Node, attr).__func__ ): setattr(cls, attr, types.MethodType(django_resolver(func), cls)) # type: ignore # Adjust types that inherit from other types/interfaces that implement Node # to make sure they pass themselves as the node type meth = getattr(cls, attr) if isinstance(meth, types.MethodType) and meth.__self__ is not cls: setattr( cls, attr, types.MethodType(cast("classmethod", meth).__func__, cls), ) settings = django_settings() if ( kwargs.get("description") is None and model.__doc__ and settings["TYPE_DESCRIPTION_FROM_MODEL_DOCSTRING"] ): kwargs["description"] = inspect.cleandoc(model.__doc__) strawberry.type(cls, **kwargs) # update annotations and fields type_def = get_object_definition(cls, strict=True) description_from_doc = settings["FIELD_DESCRIPTION_FROM_HELP_TEXT"] new_fields: list[StrawberryField] = [] for f in type_def.fields: django_name: Optional[str] = ( getattr(f, "django_name", None) or f.python_name or f.name ) assert django_name is not None description: Optional[str] = getattr(f, "description", None) type_annotation: Optional[StrawberryAnnotation] = getattr( f, "type_annotation", None, ) # We need to reset the `__eval_cache__` to make sure inherited types # will be forced to reevaluate the annotation on strawberry 0.192.2+ if type_annotation is not None and hasattr( type_annotation, "__resolve_cache__", ): type_annotation.__resolve_cache__ = None if f.name in auto_fields: f_is_auto = True # Force the field to be auto again for it to be re-evaluated if type_annotation: type_annotation.annotation = strawberry.auto else: f_is_auto = type_annotation is not None and is_auto( type_annotation.annotation, ) try: model_attr = get_model_field(django_type.model, django_name) except FieldDoesNotExist as e: model_attr = getattr(django_type.model, django_name, None) is_relation = False if model_attr is not None and isinstance(model_attr, ModelProperty): if type_annotation is None or f_is_auto: type_annotation = StrawberryAnnotation( model_attr.type_annotation, namespace=sys.modules[model_attr.func.__module__].__dict__, ) if description is None and description_from_doc: description = model_attr.description f_is_auto = False elif model_attr is not None and isinstance( model_attr, (property, functools.cached_property), ): func = ( model_attr.fget if isinstance(model_attr, property) else model_attr.func ) if type_annotation is None or f_is_auto: return_type = func.__annotations__.get("return") if return_type is None: raise MissingFieldAnnotationError( django_name, type_def.origin, ) from e type_annotation = StrawberryAnnotation( return_type, namespace=sys.modules[func.__module__].__dict__, ) if description is None and func.__doc__ and description_from_doc: description = inspect.cleandoc(func.__doc__) f_is_auto = False if type_annotation is None or f_is_auto: raise else: is_relation = model_attr.is_relation django_name = getattr(f, "django_name", None) or resolve_model_field_name( model_attr, is_input=django_type.is_input, is_filter=bool(django_type.is_filter), is_fk_id=( f.python_name.endswith("_id") and isinstance(model_attr, ForeignKey) ), ) if description is None and description_from_doc: try: from django.contrib.contenttypes.fields import ( GenericForeignKey, GenericRel, ) except (ImportError, RuntimeError): # pragma: no cover GenericForeignKey = None # noqa: N806 GenericRel = None # noqa: N806 if ( GenericForeignKey is not None and GenericRel is not None and isinstance(model_attr, (GenericRel, GenericForeignKey)) ): f_description = None elif isinstance(model_attr, (ManyToOneRel, ManyToManyRel)): f_description = model_attr.field.help_text else: f_description = getattr(model_attr, "help_text", None) if f_description: description = str(f_description) if isinstance(f, StrawberryDjangoField) and not f.origin_django_type: # If the field is a StrawberryDjangoField and it is the first time # seeing it, just update its annotations/description/etc f.type_annotation = type_annotation f.description = description elif isinstance(f, StrawberryDjangoField): f = copy.copy(f) # noqa: PLW2901 elif ( not isinstance(f, StrawberryDjangoField) and getattr(f, "base_resolver", None) is not None ): # If this is not a StrawberryDjangoField, but has a base_resolver, no need # avoid forcing it to be a StrawberryDjangoField new_fields.append(f) continue else: f = field_cls( # noqa: PLW2901 django_name=django_name, description=description, type_annotation=type_annotation, python_name=f.python_name, graphql_name=getattr(f, "graphql_name", None), origin=getattr(f, "origin", None), is_subscription=getattr(f, "is_subscription", False), base_resolver=getattr(f, "base_resolver", None), permission_classes=getattr(f, "permission_classes", ()), default=getattr(f, "default", dataclasses.MISSING), default_factory=getattr(f, "default_factory", dataclasses.MISSING), metadata=getattr(f, "metadata", None), deprecation_reason=getattr(f, "deprecation_reason", None), directives=getattr(f, "directives", ()), pagination=getattr(f, "pagination", UNSET), filters=getattr(f, "filters", UNSET), order=getattr(f, "order", UNSET), extensions=getattr(f, "extensions", ()), ) f.django_name = django_name f.is_relation = is_relation f.origin_django_type = django_type new_fields.append(f) if f.base_resolver and f.python_name: setattr(cls, f.python_name, f) type_def.fields = new_fields cls.__strawberry_django_definition__ = django_type # type: ignore # TODO: remove when deprecating _type_definition DeprecatedDescriptor( "_django_type is deprecated, use __strawberry_django_definition__ instead", cast( "WithStrawberryDjangoObjectDefinition", cls, ).__strawberry_django_definition__, "_django_type", ).inject(cls) return cast("_T", cls) @dataclasses.dataclass class StrawberryDjangoDefinition(Generic[_O, _M]): origin: _O model: type[_M] store: OptimizerStore is_input: bool = False is_partial: bool = False is_filter: Union[Literal["lookups"], bool] = False filters: Optional[type] = None order: Optional[type] = None ordering: Optional[type] = None pagination: bool = False field_cls: type[StrawberryDjangoField] = StrawberryDjangoField disable_optimization: bool = False @dataclass_transform( order_default=True, field_specifiers=( StrawberryField, _field, ), ) def type( # noqa: A001 model: type[Model], *, name: Optional[str] = None, field_cls: type[StrawberryDjangoField] = StrawberryDjangoField, is_input: bool = False, is_interface: bool = False, is_filter: Union[Literal["lookups"], bool] = False, description: Optional[str] = None, directives: Optional[Sequence[object]] = (), extend: bool = False, filters: Optional[type] = None, order: Optional[type] = None, ordering: Optional[type] = None, pagination: bool = False, only: Optional[TypeOrSequence[str]] = None, select_related: Optional[TypeOrSequence[str]] = None, prefetch_related: Optional[TypeOrSequence[PrefetchType]] = None, annotate: Optional[TypeOrMapping[AnnotateType]] = None, disable_optimization: bool = False, fields: Optional[Union[list[str], Literal["__all__"]]] = None, exclude: Optional[list[str]] = None, ) -> Callable[[_T], _T]: """Annotates a class as a Django GraphQL type. Examples -------- It can be used like this: >>> @strawberry_django.type(SomeModel) ... class X: ... some_field: strawberry.auto ... otherfield: str = strawberry_django.field() """ def wrapper(cls: _T) -> _T: return _process_type( cls, model, name=name, field_cls=field_cls, is_input=is_input, is_filter=is_filter, is_interface=is_interface, description=description, directives=directives, extend=extend, filters=filters, pagination=pagination, order=order, ordering=ordering, only=only, select_related=select_related, prefetch_related=prefetch_related, annotate=annotate, disable_optimization=disable_optimization, fields=fields, exclude=exclude, ) return wrapper @dataclass_transform( order_default=True, field_specifiers=( StrawberryField, _field, ), ) def interface( model: builtins.type[Model], *, name: Optional[str] = None, field_cls: builtins.type[StrawberryDjangoField] = StrawberryDjangoField, description: Optional[str] = None, directives: Optional[Sequence[object]] = (), disable_optimization: bool = False, ) -> Callable[[_T], _T]: """Annotates a class as a Django GraphQL interface. Examples -------- It can be used like this: >>> @strawberry_django.interface(SomeModel) ... class X: ... some_field: strawberry.auto ... otherfield: str = strawberry_django.field() """ def wrapper(cls: _T) -> _T: return _process_type( cls, model, name=name, field_cls=field_cls, is_interface=True, description=description, directives=directives, disable_optimization=disable_optimization, ) return wrapper @dataclass_transform( order_default=True, field_specifiers=( StrawberryField, _field, ), ) def input( # noqa: A001 model: builtins.type[Model], *, name: Optional[str] = None, field_cls: builtins.type[StrawberryDjangoField] = StrawberryDjangoField, description: Optional[str] = None, directives: Optional[Sequence[object]] = (), is_filter: Union[Literal["lookups"], bool] = False, partial: bool = False, fields: Optional[Union[list[str], Literal["__all__"]]] = None, exclude: Optional[list[str]] = None, ) -> Callable[[_T], _T]: """Annotates a class as a Django GraphQL input. Examples -------- It can be used like this: >>> @strawberry_django.input(SomeModel) ... class X: ... some_field: strawberry.auto ... otherfield: str = strawberry_django.field() """ def wrapper(cls: _T) -> _T: return _process_type( cls, model, name=name, field_cls=field_cls, is_input=True, is_filter=is_filter, description=description, directives=directives, partial=partial, fields=fields, exclude=exclude, ) return wrapper @dataclass_transform( order_default=True, field_specifiers=( StrawberryField, _field, ), ) def partial( model: builtins.type[Model], *, name: Optional[str] = None, field_cls: builtins.type[StrawberryDjangoField] = StrawberryDjangoField, description: Optional[str] = None, directives: Optional[Sequence[object]] = (), fields: Optional[Union[list[str], Literal["__all__"]]] = None, exclude: Optional[list[str]] = None, ) -> Callable[[_T], _T]: """Annotates a class as a Django GraphQL partial. Examples -------- It can be used like this: >>> @strawberry_django.partial(SomeModel) ... class X: ... some_field: strawberry.auto ... otherfield: str = strawberry_django.field() """ def wrapper(cls: _T) -> _T: return _process_type( cls, model, name=name, field_cls=field_cls, is_input=True, description=description, directives=directives, partial=True, fields=fields, exclude=exclude, ) return wrapper strawberry-graphql-django-0.62.0/strawberry_django/utils/000077500000000000000000000000001502405145400235655ustar00rootroot00000000000000strawberry-graphql-django-0.62.0/strawberry_django/utils/__init__.py000066400000000000000000000000001502405145400256640ustar00rootroot00000000000000strawberry-graphql-django-0.62.0/strawberry_django/utils/inspect.py000066400000000000000000000310351502405145400256060ustar00rootroot00000000000000from __future__ import annotations import dataclasses import functools import itertools import weakref from collections.abc import Iterable from typing import ( TYPE_CHECKING, Any, cast, ) from django.db.models.query import Prefetch, QuerySet from django.db.models.sql.where import WhereNode from strawberry import Schema from strawberry.types import has_object_definition from strawberry.types.base import ( StrawberryContainer, StrawberryObjectDefinition, StrawberryType, StrawberryTypeVar, ) from strawberry.types.lazy_type import LazyType from strawberry.types.union import StrawberryUnion from strawberry.utils.str_converters import to_camel_case from typing_extensions import TypeIs, assert_never from strawberry_django.fields.types import resolve_model_field_name from .pyutils import DictTree, dicttree_insersection_differs, dicttree_merge from .typing import get_django_definition if TYPE_CHECKING: from collections.abc import Generator, Iterable from django.db import models from django.db.models.expressions import Expression from django.db.models.fields import Field from django.db.models.fields.reverse_related import ForeignObjectRel from django.db.models.sql.query import Query from model_utils.managers import ( InheritanceManagerMixin, InheritanceQuerySetMixin, ) from polymorphic.models import PolymorphicModel @functools.lru_cache def get_model_fields( model: type[models.Model], *, camel_case: bool = False, is_input: bool = False, is_filter: bool = False, ) -> dict[str, Field | ForeignObjectRel]: """Get a list of model fields from the model.""" fields = {} for f in model._meta.get_fields(): name = cast( "str", resolve_model_field_name(f, is_input=is_input, is_filter=is_filter), ) if camel_case: name = to_camel_case(name) fields[name] = f return fields def get_model_field( model: type[models.Model], field_name: str, *, camel_case: bool = False, is_input: bool = False, is_filter: bool = False, ) -> Field | ForeignObjectRel | None: """Get a model fields from the model given its name.""" return get_model_fields( model, camel_case=camel_case, is_input=is_input, is_filter=is_filter, ).get(field_name) def get_possible_types( gql_type: StrawberryObjectDefinition | StrawberryType | type, *, object_definition: StrawberryObjectDefinition | None = None, ) -> Generator[type]: """Resolve all possible types for gql_type. Args: ---- gql_type: The type to retrieve possibilities from. type_def: Optional type definition to use to resolve type vars. This is used internally. Yields: ------ All possibilities for the type """ if isinstance(gql_type, StrawberryObjectDefinition): yield from get_possible_types(gql_type.origin, object_definition=gql_type) elif isinstance(gql_type, LazyType): yield from get_possible_types(gql_type.resolve_type()) elif isinstance(gql_type, StrawberryTypeVar) and object_definition is not None: resolved = object_definition.type_var_map.get(gql_type.type_var.__name__, None) if resolved is not None: yield from get_possible_types(resolved) elif isinstance(gql_type, StrawberryContainer): yield from get_possible_types(gql_type.of_type) elif isinstance(gql_type, StrawberryUnion): yield from itertools.chain.from_iterable( (get_possible_types(t) for t in gql_type.types), ) elif isinstance(gql_type, StrawberryType): # Nothing to return here pass elif isinstance(gql_type, type): yield gql_type else: assert_never(gql_type) def get_possible_type_definitions( gql_type: StrawberryObjectDefinition | StrawberryType | type, ) -> Generator[StrawberryObjectDefinition]: """Resolve all possible type definitions for gql_type. Args: ---- gql_type: The type to retrieve possibilities from. Yields: ------ All possibilities for the type """ if isinstance(gql_type, StrawberryObjectDefinition): yield gql_type return for t in get_possible_types(gql_type): if isinstance(t, StrawberryObjectDefinition): yield t elif has_object_definition(t): yield t.__strawberry_definition__ try: # Can't import PolymorphicModel, because it requires Django Apps to be ready # Import polymorphic instead to check for its existence import polymorphic # noqa: F401 def is_polymorphic_model(v: type) -> TypeIs[type[PolymorphicModel]]: return getattr(v, "polymorphic_model_marker", False) is True except ImportError: def is_polymorphic_model(v: type) -> TypeIs[type[PolymorphicModel]]: return False try: from model_utils.managers import InheritanceManagerMixin, InheritanceQuerySetMixin def is_inheritance_manager( v: Any, ) -> TypeIs[InheritanceManagerMixin]: return isinstance(v, InheritanceManagerMixin) def is_inheritance_qs( v: Any, ) -> TypeIs[InheritanceQuerySetMixin]: return isinstance(v, InheritanceQuerySetMixin) except ImportError: def is_inheritance_manager( v: Any, ) -> TypeIs[InheritanceManagerMixin]: return False def is_inheritance_qs( v: Any, ) -> TypeIs[InheritanceQuerySetMixin]: return False def _can_optimize_subtypes(model: type[models.Model]) -> bool: return is_polymorphic_model(model) or is_inheritance_manager(model._default_manager) _interfaces: weakref.WeakKeyDictionary[ Schema, dict[StrawberryObjectDefinition, list[StrawberryObjectDefinition]], ] = weakref.WeakKeyDictionary() def get_possible_concrete_types( model: type[models.Model], schema: Schema, strawberry_type: StrawberryObjectDefinition | StrawberryType, ) -> Iterable[StrawberryObjectDefinition]: """Return the object definitions the optimizer should look at when optimizing a model. Returns any object definitions attached to either the model or one of its supertypes. If the model is one that supports polymorphism, by returning subtypes from its queryset, subtypes are also looked at. Currently, this is supported for django-polymorphic and django-model-utils InheritanceManager. """ for object_definition in get_possible_type_definitions(strawberry_type): if not object_definition.is_interface: yield object_definition continue schema_interfaces = _interfaces.setdefault(schema, {}) interface_definitions = schema_interfaces.get(object_definition) if interface_definitions is None: interface_definitions = [] for t in schema.schema_converter.type_map.values(): t_definition = t.definition if isinstance(t_definition, StrawberryObjectDefinition) and issubclass( t_definition.origin, object_definition.origin ): interface_definitions.append(t_definition) schema_interfaces[object_definition] = interface_definitions for interface_definition in interface_definitions: dj_definition = get_django_definition(interface_definition.origin) if dj_definition and ( issubclass(model, dj_definition.model) or ( _can_optimize_subtypes(model) and issubclass(dj_definition.model, model) ) ): yield interface_definition @dataclasses.dataclass(eq=True) class PrefetchInspector: """Prefetch hints.""" prefetch: Prefetch qs: QuerySet = dataclasses.field(init=False, compare=False) query: Query = dataclasses.field(init=False, compare=False) def __post_init__(self): self.qs = cast("QuerySet", self.prefetch.queryset) # type: ignore self.query = self.qs.query @property def only(self) -> frozenset[str] | None: if self.query.deferred_loading[1]: return None return frozenset(self.query.deferred_loading[0]) @only.setter def only(self, value: Iterable[str | None] | None): value = frozenset(v for v in (value or []) if v is not None) self.query.deferred_loading = (value, len(value) == 0) @property def defer(self) -> frozenset[str] | None: if not self.query.deferred_loading[1]: return None return frozenset(self.query.deferred_loading[0]) @defer.setter def defer(self, value: Iterable[str | None] | None): value = frozenset(v for v in (value or []) if v is not None) self.query.deferred_loading = (value, True) @property def select_related(self) -> DictTree | None: if not isinstance(self.query.select_related, dict): return None return self.query.select_related @select_related.setter def select_related(self, value: DictTree | None): self.query.select_related = value or {} @property def prefetch_related(self) -> list[Prefetch | str]: return list(self.qs._prefetch_related_lookups) # type: ignore @prefetch_related.setter def prefetch_related(self, value: Iterable[Prefetch | str] | None): self.qs._prefetch_related_lookups = tuple(value or []) # type: ignore @property def annotations(self) -> dict[str, Expression]: return self.query.annotations @annotations.setter def annotations(self, value: dict[str, Expression] | None): self.query.annotations = value or {} # type: ignore @property def extra(self) -> DictTree: return self.query.extra @extra.setter def extra(self, value: DictTree | None): self.query.extra = value or {} # type: ignore @property def where(self) -> WhereNode: return self.query.where @where.setter def where(self, value: WhereNode | None): self.query.where = value or WhereNode() def merge(self, other: PrefetchInspector, *, allow_unsafe_ops: bool = False): if not allow_unsafe_ops and self.where != other.where: raise ValueError( "Tried to prefetch 2 queries with different filters to the " "same attribute. Use `to_attr` in this case...", ) # Merge select_related self.select_related = dicttree_merge( self.select_related or {}, other.select_related or {}, ) # Merge only/deferred if not allow_unsafe_ops and (self.defer is None) != (other.defer is None): raise ValueError( "Tried to prefetch 2 queries with different deferred " "operations. Use only `only` or `deferred`, not both...", ) if self.only is not None and other.only is not None: self.only |= other.only elif self.defer is not None and other.defer is not None: self.defer |= other.defer else: # One has defer, the other only. In this case, defer nothing self.defer = frozenset() # Merge annotations s_annotations = self.annotations o_annotations = other.annotations if not allow_unsafe_ops: for k in set(s_annotations) & set(o_annotations): if s_annotations[k] != o_annotations[k]: raise ValueError( "Tried to prefetch 2 queries with overlapping annotations.", ) self.annotations = {**s_annotations, **o_annotations} # Merge extra s_extra = self.extra o_extra = other.extra if not allow_unsafe_ops and dicttree_insersection_differs(s_extra, o_extra): raise ValueError("Tried to prefetch 2 queries with overlapping extras.") self.extra = {**s_extra, **o_extra} prefetch_related: dict[str, str | Prefetch] = {} for p in itertools.chain(self.prefetch_related, other.prefetch_related): if isinstance(p, str): if p not in prefetch_related: prefetch_related[p] = p continue path = p.prefetch_to existing = prefetch_related.get(path) if not existing or isinstance(existing, str): prefetch_related[path] = p continue inspector = self.__class__(existing).merge(PrefetchInspector(p)) prefetch_related[path] = inspector.prefetch self.prefetch_related = prefetch_related return self strawberry-graphql-django-0.62.0/strawberry_django/utils/patches.py000066400000000000000000000060751502405145400255760ustar00rootroot00000000000000import django from django.db import ( DEFAULT_DB_ALIAS, NotSupportedError, connections, ) from django.db.models import Q, Window from django.db.models.fields import related_descriptors from django.db.models.functions import RowNumber from django.db.models.lookups import GreaterThan, LessThanOrEqual from django.db.models.sql import Query from django.db.models.sql.constants import INNER from django.db.models.sql.where import AND def apply_pagination_fix(): """Apply pagination fix for Django 5.1 or older. This is based on the fix in this patch, which is going to be included in Django 5.2: https://code.djangoproject.com/ticket/35677#comment:9 If can safely be removed when Django 5.2 is the minimum version we support """ if django.VERSION >= (5, 2): return # This is a copy of the function, exactly as it exists on Django 4.2, 5.0 and 5.1 # (there are no differences in this function between these versions) def _filter_prefetch_queryset(queryset, field_name, instances): predicate = Q(**{f"{field_name}__in": instances}) db = queryset._db or DEFAULT_DB_ALIAS if queryset.query.is_sliced: if not connections[db].features.supports_over_clause: raise NotSupportedError( "Prefetching from a limited queryset is only supported on backends " "that support window functions." ) low_mark, high_mark = queryset.query.low_mark, queryset.query.high_mark order_by = [ expr for expr, _ in queryset.query.get_compiler(using=db).get_order_by() ] window = Window(RowNumber(), partition_by=field_name, order_by=order_by) predicate &= GreaterThan(window, low_mark) if high_mark is not None: predicate &= LessThanOrEqual(window, high_mark) queryset.query.clear_limits() # >> ORIGINAL CODE # return queryset.filter(predicate) # noqa: ERA001 # << ORIGINAL CODE # >> PATCHED CODE queryset.query.add_q(predicate, reuse_all_aliases=True) return queryset # << PATCHED CODE related_descriptors._filter_prefetch_queryset = _filter_prefetch_queryset # type: ignore # This is a copy of the function, exactly as it exists on Django 4.2, 5.0 and 5.1 # (there are no differences in this function between these versions) def add_q(self, q_object, reuse_all_aliases=False): existing_inner = { a for a in self.alias_map if self.alias_map[a].join_type == INNER } # >> ORIGINAL CODE # clause, _ = self._add_q(q_object, self.used_aliases) # noqa: ERA001 # << ORIGINAL CODE # >> PATCHED CODE if reuse_all_aliases: # noqa: SIM108 can_reuse = set(self.alias_map) else: can_reuse = self.used_aliases clause, _ = self._add_q(q_object, can_reuse) # << PATCHED CODE if clause: self.where.add(clause, AND) self.demote_joins(existing_inner) Query.add_q = add_q strawberry-graphql-django-0.62.0/strawberry_django/utils/pyutils.py000066400000000000000000000022531502405145400256520ustar00rootroot00000000000000from collections.abc import Mapping from typing import Any, TypeVar from typing_extensions import TypeAlias _K = TypeVar("_K", bound=Any) _V = TypeVar("_V", bound=Any) DictTree: TypeAlias = dict[str, "DictTree"] def dicttree_merge(dict1: Mapping[_K, _V], dict2: Mapping[_K, _V]) -> dict[_K, _V]: new = { **dict1, **dict2, } for k, v1 in dict1.items(): if not isinstance(v1, dict): continue v2 = dict2.get(k) if isinstance(v2, Mapping): new[k] = dicttree_merge(v1, v2) # type: ignore for k, v2 in dict2.items(): if not isinstance(v2, dict): continue v1 = dict1.get(k) if isinstance(v1, Mapping): new[k] = dicttree_merge(v1, v2) # type: ignore return new def dicttree_insersection_differs( dict1: Mapping[_K, _V], dict2: Mapping[_K, _V], ) -> bool: for k in set(dict1) & set(dict2): v1 = dict1[k] v2 = dict2[k] if isinstance(v1, Mapping) and isinstance(v2, Mapping): if dicttree_insersection_differs(v1, v2): return True elif v1 != v2: return True return False strawberry-graphql-django-0.62.0/strawberry_django/utils/query.py000066400000000000000000000153041502405145400253070ustar00rootroot00000000000000import functools from typing import TYPE_CHECKING, Optional, TypeVar, cast from django.core.exceptions import FieldDoesNotExist from django.db.models import Exists, F, Model, Q, QuerySet from django.db.models.functions import Cast from strawberry.utils.inspect import in_async_context from .typing import TypeOrIterable, UserType if TYPE_CHECKING: from django.contrib.auth.models import AbstractUser from django.contrib.contenttypes.models import ContentType from guardian.managers import ( GroupObjectPermissionManager, UserObjectPermissionManager, ) _Q = TypeVar("_Q", bound=QuerySet) def _filter( qs: _Q, perms: list[str], *, lookup: str = "", model: type[Model], any_perm: bool = True, ctype: Optional["ContentType"] = None, ) -> _Q: lookup = lookup and f"{lookup}__" ctype_attr = f"{lookup}content_type" if ctype is not None: q = Q(**{ctype_attr: ctype}) else: meta = model._meta q = Q( **{ f"{ctype_attr}__app_label": meta.app_label, f"{ctype_attr}__model": meta.model_name, }, ) if len(perms) == 1: q &= Q(**{f"{lookup}codename": perms[0]}) elif any_perm: q &= Q(**{f"{lookup}codename__in": perms}) else: q = functools.reduce( lambda acu, p: acu & Q(**{f"{lookup}codename": p}), perms, q, ) return qs.filter(q) def filter_for_user_q( qs: QuerySet, user: UserType, perms: TypeOrIterable[str], *, any_perm: bool = True, with_groups: bool = True, with_superuser: bool = False, ): if with_superuser and user.is_active and getattr(user, "is_superuser", False): return qs if user.is_anonymous: return qs.none() groups_field = None try: groups_field = cast("AbstractUser", user)._meta.get_field("groups") except FieldDoesNotExist: with_groups = False if isinstance(perms, str): perms = [perms] model = cast("type[Model]", qs.model) if model._meta.concrete_model: model = model._meta.concrete_model try: from django.contrib.contenttypes.models import ContentType except (ImportError, RuntimeError): # pragma: no cover ctype = None else: try: # We don't want to query the database here because this might not be async # safe. Try to retrieve the ContentType from cache. If it is not there, we # will query it through the queryset meta = model._meta ctype = cast( "ContentType", ContentType.objects._get_from_cache(meta), # type: ignore ) except KeyError: # pragma:nocover # If we are not running async, retrieve it ctype = ( ContentType.objects.get_for_model(model, for_concrete_model=False) if not in_async_context() else None ) app_labels = set() perms_list = [] for p in perms: parts = p.split(".") if len(parts) > 1: app_labels.add(parts[0]) perms_list.append(parts[-1]) if len(app_labels) == 1 and ctype is not None: app_label = app_labels.pop() if app_label != ctype.app_label: # pragma:nocover raise ValueError( f"Given perms must have same app label ({app_label!r} !=" f" {ctype.app_label!r})", ) elif len(app_labels) > 1: # pragma:nocover raise ValueError(f"Cannot mix app_labels ({app_labels!r})") # Small optimization if the user's permissions are cached perm_cache: Optional[set[str]] = getattr(user, "_perm_cache", None) if perm_cache is not None: f = any if any_perm else all if f(p in perm_cache for p in perms_list): return qs q = Q() if hasattr(user, "user_permissions"): q |= Q( Exists( _filter( cast("AbstractUser", user).user_permissions, perms_list, model=model, ctype=ctype, ), ), ) if with_groups: q |= Q( Exists( _filter( cast("AbstractUser", user).groups, perms_list, lookup="permissions", model=model, ctype=ctype, ), ), ) try: from strawberry_django.integrations.guardian import ( get_object_permission_models, ) except (ImportError, RuntimeError): # pragma: no cover pass else: perm_models = get_object_permission_models(qs.model) user_model = perm_models.user user_qs = _filter( user_model.objects.filter(user=user), perms_list, lookup="permission", model=model, ctype=ctype, ) if cast("UserObjectPermissionManager", user_model.objects).is_generic(): user_qs = user_qs.filter(content_type=F("permission__content_type")) else: user_qs = user_qs.annotate(object_pk=F("content_object")) obj_qs = user_qs.values_list( Cast("object_pk", cast("str", model._meta.pk)), flat=True, ).distinct() if with_groups: assert groups_field is not None group_model = perm_models.group user_key = f"group__{groups_field.related_query_name()}" # type: ignore group_qs = _filter( group_model.objects.filter(**{user_key: user}), perms_list, lookup="permission", model=model, ctype=ctype, ) if cast("GroupObjectPermissionManager", group_model.objects).is_generic(): group_qs = group_qs.filter(content_type=F("permission__content_type")) else: group_qs = group_qs.annotate(object_pk=F("content_object")) obj_qs = obj_qs.union( group_qs.values_list( Cast("object_pk", cast("str", model._meta.pk)), flat=True, ).distinct(), ) q |= Q(pk__in=obj_qs) return q def filter_for_user( qs: QuerySet, user: UserType, perms: TypeOrIterable[str], *, any_perm: bool = True, with_groups: bool = True, with_superuser: bool = False, ): return qs & qs.filter( filter_for_user_q( qs, user, perms, any_perm=any_perm, with_groups=with_groups, with_superuser=with_superuser, ), ) strawberry-graphql-django-0.62.0/strawberry_django/utils/requests.py000066400000000000000000000006751502405145400260220ustar00rootroot00000000000000from django.http.request import HttpRequest from strawberry.types import Info def get_request(info: Info) -> HttpRequest: """Return the request from Info. description: Return the request object for both WSGI and ASGI implementations. It tends to move based on the environment. """ try: request = info.context.request except AttributeError: request = info.context.get("request") return request strawberry-graphql-django-0.62.0/strawberry_django/utils/typing.py000066400000000000000000000072761502405145400254650ustar00rootroot00000000000000from __future__ import annotations import dataclasses import sys from collections.abc import Callable, Iterable, Mapping, Sequence from typing import ( TYPE_CHECKING, Any, ClassVar, TypeVar, Union, cast, overload, ) from django.db.models.expressions import BaseExpression, Combinable from graphql.type.definition import GraphQLResolveInfo from strawberry.annotation import StrawberryAnnotation from strawberry.types.auto import StrawberryAuto from strawberry.types.base import ( StrawberryContainer, StrawberryType, WithStrawberryObjectDefinition, ) from strawberry.types.lazy_type import LazyType from strawberry.utils.typing import is_classvar from typing_extensions import Protocol if TYPE_CHECKING: from django.contrib.auth.base_user import AbstractBaseUser from django.contrib.auth.models import AnonymousUser from django.db.models import Prefetch from typing_extensions import Literal, TypeAlias, TypeGuard from strawberry_django.type import StrawberryDjangoDefinition _T = TypeVar("_T") _Type = TypeVar("_Type", bound="StrawberryType | type") TypeOrSequence: TypeAlias = Union[_T, Sequence[_T]] TypeOrMapping: TypeAlias = Union[_T, Mapping[str, _T]] TypeOrIterable: TypeAlias = Union[_T, Iterable[_T]] UserType: TypeAlias = Union["AbstractBaseUser", "AnonymousUser"] PrefetchCallable: TypeAlias = Callable[[GraphQLResolveInfo], "Prefetch[Any]"] PrefetchType: TypeAlias = Union[str, "Prefetch[Any]", PrefetchCallable] AnnotateCallable: TypeAlias = Callable[ [GraphQLResolveInfo], Union[BaseExpression, Combinable], ] AnnotateType: TypeAlias = Union[BaseExpression, Combinable, AnnotateCallable] class WithStrawberryDjangoObjectDefinition(WithStrawberryObjectDefinition, Protocol): __strawberry_django_definition__: ClassVar[StrawberryDjangoDefinition] def has_django_definition( obj: Any, ) -> TypeGuard[type[WithStrawberryDjangoObjectDefinition]]: return hasattr(obj, "__strawberry_django_definition__") @overload def get_django_definition( obj: Any, *, strict: Literal[True], ) -> StrawberryDjangoDefinition: ... @overload def get_django_definition( obj: Any, *, strict: bool = False, ) -> StrawberryDjangoDefinition | None: ... def get_django_definition( obj: Any, *, strict: bool = False, ) -> StrawberryDjangoDefinition | None: return ( obj.__strawberry_django_definition__ if strict else getattr(obj, "__strawberry_django_definition__", None) ) def is_auto(obj: Any) -> bool: if isinstance(obj, str): return obj in {"auto", "strawberry.auto"} return isinstance(obj, StrawberryAuto) def get_annotations(cls) -> dict[str, StrawberryAnnotation]: annotations: dict[str, StrawberryAnnotation] = {} for c in reversed(cls.__mro__): # Skip non dataclass bases other than cls itself if c is not cls and not dataclasses.is_dataclass(c): continue namespace = sys.modules[c.__module__].__dict__ for k, v in getattr(c, "__annotations__", {}).items(): if not is_classvar(cast("type", c), v): annotations[k] = StrawberryAnnotation(v, namespace=namespace) return annotations @overload def unwrap_type(type_: StrawberryContainer) -> StrawberryType | type: ... @overload def unwrap_type(type_: LazyType) -> StrawberryType | type: ... @overload def unwrap_type(type_: None) -> None: ... @overload def unwrap_type(type_: _Type) -> _Type: ... def unwrap_type(type_): while True: if isinstance(type_, LazyType): type_ = type_.resolve_type() elif isinstance(type_, StrawberryContainer): type_ = type_.of_type else: break return type_ strawberry-graphql-django-0.62.0/tests/000077500000000000000000000000001502405145400200415ustar00rootroot00000000000000strawberry-graphql-django-0.62.0/tests/__init__.py000066400000000000000000000000001502405145400221400ustar00rootroot00000000000000strawberry-graphql-django-0.62.0/tests/auth/000077500000000000000000000000001502405145400210025ustar00rootroot00000000000000strawberry-graphql-django-0.62.0/tests/auth/__init__.py000066400000000000000000000000001502405145400231010ustar00rootroot00000000000000strawberry-graphql-django-0.62.0/tests/auth/conftest.py000066400000000000000000000010701502405145400231770ustar00rootroot00000000000000from collections import UserDict import pytest from django.contrib import auth as django_auth UserModel = django_auth.get_user_model() @pytest.fixture def context(mocker): class Session(UserDict): def cycle_key(self): pass def flush(self): pass context = mocker.Mock() context.request.session = Session() django_auth.logout(context.request) return context @pytest.fixture def user(db, group, tag): return UserModel.objects.create_user( username="user", password="password", ) strawberry-graphql-django-0.62.0/tests/auth/test_mutations.py000066400000000000000000000053611502405145400244430ustar00rootroot00000000000000from typing import Optional import django.contrib.auth as django_auth import pytest import strawberry from django.conf import settings from django.core.exceptions import ObjectDoesNotExist import strawberry_django from strawberry_django import auth from tests import utils UserModel = django_auth.get_user_model() @strawberry_django.type(UserModel) class User: username: strawberry.auto email: strawberry.auto @strawberry_django.input(UserModel) class UserInput: username: strawberry.auto password: strawberry.auto email: strawberry.auto @strawberry.type class Mutation: login: Optional[User] = auth.login() # type: ignore logout = auth.logout() register: User = auth.register(UserInput) @pytest.fixture def mutation(db): return utils.generate_query(mutation=Mutation) def test_login(mutation, user, context): result = mutation( '{ login(username: "user", password: "password") { username } }', context_value=context, ) assert not result.errors assert result.data["login"] == {"username": "user"} assert context.request.user == user def test_login_with_wrong_password(mutation, user, context): result = mutation( '{ login(username: "user", password: "wrong") { username } }', context_value=context, ) assert result.errors assert result.data["login"] is None assert context.request.user.is_anonymous def test_logout(mutation, user, context): django_auth.login( context.request, user, backend=settings.AUTHENTICATION_BACKENDS[0], ) result = mutation("{ logout }", context_value=context) assert not result.errors assert result.data["logout"] is True assert context.request.user.is_anonymous def test_logout_without_logged_in(mutation, user, context): result = mutation("{ logout }", context_value=context) assert not result.errors assert result.data["logout"] is False def test_register_new_user(mutation, user, context): result = mutation( '{ register(data: {username: "new_user",' ' password: "test_password"}) { username } }', context_value=context, ) assert not result.errors assert result.data["register"] == {"username": "new_user"} user = UserModel.objects.get(username="new_user") assert user.pk assert user.check_password("test_password") def test_register_with_invalid_password(mutation, user, context): result = mutation( '{ register(data: {username: "invalid_user", password: "a"}) { username } }', context_value=context, ) assert len(result.errors) == 1 assert "too short" in result.errors[0].message with pytest.raises(ObjectDoesNotExist): assert UserModel.objects.get(username="invalid_user") strawberry-graphql-django-0.62.0/tests/auth/test_queries.py000066400000000000000000000016721502405145400240760ustar00rootroot00000000000000from typing import Optional import pytest import strawberry from django.conf import settings from django.contrib import auth as django_auth from strawberry_django import auth from tests import utils from .test_mutations import User @strawberry.type class Query: current_user: Optional[User] = auth.current_user() # type: ignore @pytest.fixture def query(db): return utils.generate_query(Query) def test_current_user(query, user, context): django_auth.login( context.request, user, backend=settings.AUTHENTICATION_BACKENDS[0], ) result = query("{ currentUser { username } }", context_value=context) assert not result.errors assert result.data == {"currentUser": {"username": "user"}} def test_current_user_not_logged_in(query, user, context): result = query("{ currentUser { username } }", context_value=context) assert result.errors assert result.data == {"currentUser": None} strawberry-graphql-django-0.62.0/tests/auth/test_types.py000066400000000000000000000020431502405145400235560ustar00rootroot00000000000000import strawberry from django.contrib.auth.models import Group, User from strawberry.types import get_object_definition from strawberry.types.base import StrawberryList import strawberry_django from strawberry_django import DjangoModelType def test_user_type(): @strawberry_django.type(User) class Type: username: strawberry.auto email: strawberry.auto groups: strawberry.auto object_definition = get_object_definition(Type, strict=True) assert [(f.name, f.type) for f in object_definition.fields] == [ ("username", str), ("email", str), ("groups", StrawberryList(DjangoModelType)), ] def test_group_type(): @strawberry_django.type(Group) class Type: name: strawberry.auto users: strawberry.auto = strawberry_django.field(field_name="user_set") object_definition = get_object_definition(Type, strict=True) assert [(f.name, f.type) for f in object_definition.fields] == [ ("name", str), ("users", StrawberryList(DjangoModelType)), ] strawberry-graphql-django-0.62.0/tests/conftest.py000066400000000000000000000137061502405145400222470ustar00rootroot00000000000000import contextlib import pathlib import shutil from typing import Union, cast import pytest import strawberry from django.conf import settings from django.test.client import AsyncClient, Client import strawberry_django from strawberry_django.optimizer import DjangoOptimizerExtension from tests.utils import GraphQLTestClient from . import models, types, utils _TESTS_DIR = pathlib.Path(__file__).parent _ROOT_DIR = _TESTS_DIR.parent @pytest.fixture(scope="session", autouse=True) def _cleanup(request): def cleanup_function(): shutil.rmtree(_ROOT_DIR / ".tmp_upload", ignore_errors=True) request.addfinalizer(cleanup_function) # noqa: PT021 @pytest.fixture(params=["sync", "async", "sync_no_optimizer", "async_no_optimizer"]) def gql_client(request): client, path, with_optimizer = cast( "dict[str, tuple[type[Union[Client, AsyncClient]], str, bool]]", { "sync": (Client, "/graphql/", True), "async": (AsyncClient, "/graphql_async/", True), "sync_no_optimizer": (Client, "/graphql/", False), "async_no_optimizer": (AsyncClient, "/graphql_async/", False), }, )[request.param] if with_optimizer: optimizer_ctx = contextlib.nullcontext else: optimizer_ctx = DjangoOptimizerExtension.disabled with optimizer_ctx(), GraphQLTestClient(path, client()) as c: yield c @pytest.fixture def fruits(db): fruit_names = ["strawberry", "raspberry", "banana"] return [models.Fruit.objects.create(name=name) for name in fruit_names] @pytest.fixture def vegetables(db): vegetable_names = ["carrot", "cucumber", "onion"] vegetable_world_production = [40.0e6, 75.2e6, 102.2e6] # in tons return [ models.Vegetable.objects.create(name=n, world_production=p) for n, p in zip(vegetable_names, vegetable_world_production) ] @pytest.fixture def tag(db): return models.Tag.objects.create(name="tag") @pytest.fixture def group(db, tag): group = models.Group.objects.create(name="group") group.tags.add(tag) return group @pytest.fixture def user(db, group, tag): return models.User.objects.create(name="user", group=group, tag=tag) @pytest.fixture def users(db): return [ models.User.objects.create(name="user1"), models.User.objects.create(name="user2"), models.User.objects.create(name="user3"), ] @pytest.fixture def groups(db): return [ models.Group.objects.create(name="group1"), models.Group.objects.create(name="group2"), models.Group.objects.create(name="group3"), ] if settings.GEOS_IMPORTED: @pytest.fixture def geofields(db): from django.contrib.gis.geos import ( LineString, MultiLineString, MultiPoint, MultiPolygon, Point, Polygon, ) return [ models.GeosFieldsModel.objects.create( point=Point(x=0, y=0), line_string=LineString((0, 0), (1, 1)), polygon=Polygon(((-1, -1), (-1, 1), (1, 1), (1, -1), (-1, -1))), multi_point=MultiPoint(Point(x=0, y=0), Point(x=1, y=1)), multi_line_string=MultiLineString( LineString((0, 0), (1, 1)), LineString((1, 1), (-1, -1)), ), multi_polygon=MultiPolygon( Polygon(((-1, -1), (-1, 1), (1, 1), (1, -1), (-1, -1))), Polygon(((-1, -1), (-1, 1), (1, 1), (1, -1), (-1, -1))), ), ), models.GeosFieldsModel.objects.create( point=Point(x=1, y=1), line_string=LineString((1, 1), (2, 2), (3, 3)), polygon=Polygon( ((-1, -1), (-1, 1), (1, 1), (1, -1), (-1, -1)), ((-2, -2), (-2, 2), (2, 2), (2, -2), (-2, -2)), ), multi_point=MultiPoint( Point(x=0, y=0), Point(x=-1, y=-1), Point(x=1, y=1), ), multi_line_string=MultiLineString( LineString((0, 0), (1, 1)), LineString((1, 1), (-1, -1)), LineString((2, 2), (-2, -2)), ), multi_polygon=MultiPolygon( Polygon( ((-1, -1), (-1, 1), (1, 1), (1, -1), (-1, -1)), ((-2, -2), (-2, 2), (2, 2), (2, -2), (-2, -2)), ), Polygon( ((-1, -1), (-1, 1), (1, 1), (1, -1), (-1, -1)), ((-2, -2), (-2, 2), (2, 2), (2, -2), (-2, -2)), ), ), ), models.GeosFieldsModel.objects.create(), ] @pytest.fixture(params=["optimizer_enabled", "optimizer_disabled"]) def schema(request): @strawberry.type class Query: user: types.User = strawberry_django.field() users: list[types.User] = strawberry_django.field() group: types.Group = strawberry_django.field() groups: list[types.Group] = strawberry_django.field() tag: types.Tag = strawberry_django.field() tags: list[types.Tag] = strawberry_django.field() if request.param == "optimizer_enabled": extensions = [DjangoOptimizerExtension()] elif request.param == "optimizer_disabled": extensions = [] else: raise AssertionError(f"Not able to handle param '{request.param}'") if settings.GEOS_IMPORTED: @strawberry.type class GeoQuery(Query): geofields: list[types.GeoField] = strawberry_django.field() return strawberry.Schema(query=GeoQuery, extensions=extensions) return strawberry.Schema(query=Query, extensions=extensions) @pytest.fixture( params=[ strawberry_django.type, strawberry_django.input, utils.dataclass, ], ) def testtype(request): return request.param strawberry-graphql-django-0.62.0/tests/django_settings.py000066400000000000000000000052511502405145400236000ustar00rootroot00000000000000from django.core.exceptions import ImproperlyConfigured from django.db import models from django.db.models.manager import BaseManager from django.db.models.query import QuerySet for cls in [QuerySet, BaseManager, models.ForeignKey, models.ManyToManyField]: if not hasattr(cls, "__class_getitem__"): cls.__class_getitem__ = classmethod( # type: ignore lambda cls, *args, **kwargs: cls, ) DEBUG = True SECRET_KEY = 1 USE_TZ = True TIME_ZONE = "UTC" DATABASES = { "default": { "ENGINE": "django.db.backends.sqlite3", "NAME": ":memory:", }, } INSTALLED_APPS = [ "django.contrib.auth", "django.contrib.contenttypes", "django.contrib.sessions", "django.contrib.staticfiles", "guardian", "debug_toolbar", "strawberry_django", ] ROOT_URLCONF = "tests.urls" AUTHENTICATION_BACKENDS = ( "django.contrib.auth.backends.ModelBackend", "guardian.backends.ObjectPermissionBackend", ) ANONYMOUS_USER_NAME = None MIDDLEWARE = [ "django.contrib.sessions.middleware.SessionMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware", "strawberry_django.middlewares.debug_toolbar.DebugToolbarMiddleware", ] AUTH_PASSWORD_VALIDATORS = [ { "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", "OPTIONS": { "min_length": 2, }, }, ] CACHES = { "default": { "BACKEND": "django.core.cache.backends.locmem.LocMemCache", "LOCATION": "unique-snowflake", }, } LOGGING = { "version": 1, "disable_existing_loggers": False, "formatters": { "simple": {"format": "%(levelname)s %(message)s"}, }, "filters": { "require_debug_true": { "()": "django.utils.log.RequireDebugTrue", }, }, "handlers": { "console": { "level": "DEBUG", "class": "logging.StreamHandler", "formatter": "simple", }, }, "loggers": { "django.db.backends": { "handlers": ["console"], "level": "INFO", }, "strawberry.execution": { "handlers": ["console"], "level": "INFO", }, }, } try: from django.contrib.gis.db import models assert models # ruff DATABASES["default"]["ENGINE"] = "django.contrib.gis.db.backends.spatialite" INSTALLED_APPS.append("django.contrib.gis") GEOS_IMPORTED = True except ImproperlyConfigured: GEOS_IMPORTED = False INSTALLED_APPS.extend( [ "tests", "tests.projects", "tests.polymorphism", "tests.polymorphism_custom", "tests.polymorphism_inheritancemanager", ], ) strawberry-graphql-django-0.62.0/tests/exceptions.py000066400000000000000000000012661502405145400226010ustar00rootroot00000000000000from strawberry_django.exceptions import ( ForbiddenFieldArgumentError, ) from strawberry_django.fields.filter_order import FilterOrderFieldResolver def test_forbidden_field_argument_extra_one(): resolver = FilterOrderFieldResolver(resolver_type="filter", func=lambda x: x) exc = ForbiddenFieldArgumentError(resolver, ["one"]) assert exc.extra_arguments_str == 'argument "one"' def test_forbidden_field_argument_extra_many(): resolver = FilterOrderFieldResolver(resolver_type="filter", func=lambda x: x) exc = ForbiddenFieldArgumentError(resolver, ["extra", "forbidden", "fields"]) assert exc.extra_arguments_str == 'arguments "extra, forbidden" and "fields"' strawberry-graphql-django-0.62.0/tests/extensions/000077500000000000000000000000001502405145400222405ustar00rootroot00000000000000strawberry-graphql-django-0.62.0/tests/extensions/__init__.py000066400000000000000000000000001502405145400243370ustar00rootroot00000000000000strawberry-graphql-django-0.62.0/tests/extensions/test_validation_cache.py000066400000000000000000000025501502405145400271300ustar00rootroot00000000000000from unittest.mock import patch import pytest import strawberry from graphql import validate from strawberry_django.extensions.django_validation_cache import DjangoValidationCache @pytest.mark.filterwarnings("ignore::django.core.cache.backends.base.CacheKeyWarning") @patch("strawberry_django.extensions.django_validation_cache.validate", wraps=validate) def test_validation_cache_extension(mock_validate): @strawberry.type class Query: @strawberry.field def hello(self) -> str: return "world" @strawberry.field def ping(self) -> str: return "pong" schema = strawberry.Schema(query=Query, extensions=[DjangoValidationCache()]) query = "query { hello }" result = schema.execute_sync(query) assert not result.errors assert result.data == {"hello": "world"} assert mock_validate.call_count == 1 # Run query multiple times for _ in range(3): result = schema.execute_sync(query) assert not result.errors assert result.data == {"hello": "world"} # validate is still only called once assert mock_validate.call_count == 1 # Running a second query doesn't cache query2 = "query { ping }" result = schema.execute_sync(query2) assert not result.errors assert result.data == {"ping": "pong"} assert mock_validate.call_count == 2 strawberry-graphql-django-0.62.0/tests/fields/000077500000000000000000000000001502405145400213075ustar00rootroot00000000000000strawberry-graphql-django-0.62.0/tests/fields/__init__.py000066400000000000000000000000001502405145400234060ustar00rootroot00000000000000strawberry-graphql-django-0.62.0/tests/fields/test_attributes.py000066400000000000000000000114321502405145400251070ustar00rootroot00000000000000import textwrap from typing import TYPE_CHECKING, cast import strawberry from django.db import models from django.test import override_settings from strawberry import BasePermission, auto, relay from strawberry.types import get_object_definition import strawberry_django from strawberry_django.settings import strawberry_django_settings if TYPE_CHECKING: from strawberry_django.fields.field import StrawberryDjangoField class FieldAttributeModel(models.Model): field = models.CharField(max_length=50) def test_default_django_name(): @strawberry_django.type(FieldAttributeModel) class Type: field: auto field2: auto = strawberry_django.field(field_name="field") assert [ (f.name, cast("StrawberryDjangoField", f).django_name) for f in get_object_definition(Type, strict=True).fields ] == [ ("field", "field"), ("field2", "field"), ] def test_field_permission_classes(): class TestPermission(BasePermission): def has_permission(self, source, info, **kwargs): return True @strawberry_django.type(FieldAttributeModel) class Type: field: auto = strawberry.field(permission_classes=[TestPermission]) @strawberry.field(permission_classes=[TestPermission]) def custom_resolved_field(self) -> str: return self.field assert sorted( [ (f.name, f.permission_classes) for f in get_object_definition(Type, strict=True).fields ], ) == sorted( [ ("field", [TestPermission]), ("custom_resolved_field", [TestPermission]), ], ) def test_auto_id(): @strawberry_django.filter_type(FieldAttributeModel) class MyTypeFilter: id: auto field: auto @strawberry_django.type(FieldAttributeModel) class MyType: id: auto other_id: auto = strawberry_django.field(field_name="id") field: auto @strawberry.type class Query: my_type: list[MyType] = strawberry_django.field(filters=MyTypeFilter) schema = strawberry.Schema(query=Query) expected = """\ type MyType { id: ID! otherId: ID! field: String! } input MyTypeFilter { id: ID field: String AND: MyTypeFilter OR: MyTypeFilter NOT: MyTypeFilter DISTINCT: Boolean } type Query { myType(filters: MyTypeFilter): [MyType!]! } """ assert textwrap.dedent(str(schema)) == textwrap.dedent(expected).strip() def test_auto_id_with_node(): @strawberry_django.filter_type(FieldAttributeModel) class MyTypeFilter: id: auto field: auto @strawberry_django.type(FieldAttributeModel) class MyType(relay.Node): other_id: auto = strawberry_django.field(field_name="id") field: auto @strawberry.type class Query: my_type: list[MyType] = strawberry_django.field(filters=MyTypeFilter) schema = strawberry.Schema(query=Query) expected = '''\ type MyType implements Node { """The Globally Unique ID of this object""" id: ID! otherId: ID! field: String! } input MyTypeFilter { id: ID field: String AND: MyTypeFilter OR: MyTypeFilter NOT: MyTypeFilter DISTINCT: Boolean } """An object with a Globally Unique ID""" interface Node { """The Globally Unique ID of this object""" id: ID! } type Query { myType(filters: MyTypeFilter): [MyType!]! } ''' assert textwrap.dedent(str(schema)) == textwrap.dedent(expected).strip() @override_settings( STRAWBERRY_DJANGO={ **strawberry_django_settings(), "MAP_AUTO_ID_AS_GLOBAL_ID": True, }, ) def test_auto_id_with_node_mapping_global_id(): @strawberry_django.filter_type(FieldAttributeModel) class MyTypeFilter: id: auto field: auto @strawberry_django.type(FieldAttributeModel) class MyType(relay.Node): other_id: auto = strawberry_django.field(field_name="id") field: auto @strawberry.type class Query: my_type: list[MyType] = strawberry_django.field(filters=MyTypeFilter) schema = strawberry.Schema(query=Query) expected = '''\ type MyType implements Node { """The Globally Unique ID of this object""" id: ID! otherId: ID! field: String! } input MyTypeFilter { id: ID field: String AND: MyTypeFilter OR: MyTypeFilter NOT: MyTypeFilter DISTINCT: Boolean } """An object with a Globally Unique ID""" interface Node { """The Globally Unique ID of this object""" id: ID! } type Query { myType(filters: MyTypeFilter): [MyType!]! } ''' assert textwrap.dedent(str(schema)) == textwrap.dedent(expected).strip() strawberry-graphql-django-0.62.0/tests/fields/test_get_result.py000066400000000000000000000052261502405145400251020ustar00rootroot00000000000000import pytest from django.db.models import QuerySet from strawberry import relay from strawberry.annotation import StrawberryAnnotation from strawberry.relay.types import ListConnection from strawberry_django.fields.field import StrawberryDjangoField from tests.types import FruitType @pytest.mark.django_db def test_resolve_returns_queryset_with_fetched_results(): field = StrawberryDjangoField(type_annotation=StrawberryAnnotation(list[FruitType])) result = field.get_result(None, None, [], {}) assert isinstance(result, QuerySet) assert result._result_cache is not None # type: ignore @pytest.mark.django_db async def test_resolve_returns_queryset_with_fetched_results_async(): field = StrawberryDjangoField(type_annotation=StrawberryAnnotation(list[FruitType])) result = await field.get_result(None, None, [], {}) assert isinstance(result, QuerySet) assert result._result_cache is not None # type: ignore @pytest.mark.django_db def test_resolve_returns_queryset_without_fetching_results_when_disabling_it(): field = StrawberryDjangoField(type_annotation=StrawberryAnnotation(list[FruitType])) field.disable_fetch_list_results = True result = field.get_result(None, None, [], {}) assert isinstance(result, QuerySet) assert result._result_cache is None # type: ignore @pytest.mark.django_db async def test_resolve_returns_queryset_without_fetching_results_when_disabling_it_async(): field = StrawberryDjangoField(type_annotation=StrawberryAnnotation(list[FruitType])) field.disable_fetch_list_results = True result = await field.get_result(None, None, [], {}) assert isinstance(result, QuerySet) assert result._result_cache is None # type: ignore @pytest.mark.django_db def test_resolve_returns_queryset_without_fetching_results_for_connections(): class FruitImplementingNode(relay.Node, FruitType): ... field = StrawberryDjangoField( type_annotation=StrawberryAnnotation(ListConnection[FruitImplementingNode]) ) field.disable_fetch_list_results = True result = field.get_result(None, None, [], {}) assert isinstance(result, QuerySet) assert result._result_cache is None # type: ignore @pytest.mark.django_db async def test_resolve_returns_queryset_without_fetching_results_for_connections_async(): class FruitImplementingNode(relay.Node, FruitType): ... field = StrawberryDjangoField( type_annotation=StrawberryAnnotation(ListConnection[FruitImplementingNode]) ) field.disable_fetch_list_results = True result = await field.get_result(None, None, [], {}) assert isinstance(result, QuerySet) assert result._result_cache is None # type: ignore strawberry-graphql-django-0.62.0/tests/fields/test_input.py000066400000000000000000000060311502405145400240570ustar00rootroot00000000000000from typing import cast import strawberry from django.db import models from strawberry import auto from strawberry.types import get_object_definition from strawberry.types.base import StrawberryOptional import strawberry_django class InputFieldsModel(models.Model): mandatory = models.IntegerField() default = models.IntegerField(default=1) blank = models.IntegerField(blank=True) null = models.IntegerField(null=True) def test_input_type(): @strawberry_django.input(InputFieldsModel) class InputType: id: auto mandatory: auto default: auto blank: auto null: auto assert [ (f.name, f.type) for f in get_object_definition(InputType, strict=True).fields ] == [ ("id", StrawberryOptional(cast("type", strawberry.ID))), ("mandatory", int), ("default", StrawberryOptional(int)), ("blank", StrawberryOptional(int)), ("null", StrawberryOptional(int)), ] def test_input_type_for_partial_update(): @strawberry_django.input(InputFieldsModel, partial=True) class InputType: id: auto mandatory: auto default: auto blank: auto null: auto assert [ (f.name, f.type) for f in get_object_definition(InputType, strict=True).fields ] == [ ("id", StrawberryOptional(cast("type", strawberry.ID))), ("mandatory", StrawberryOptional(int)), ("default", StrawberryOptional(int)), ("blank", StrawberryOptional(int)), ("null", StrawberryOptional(int)), ] def test_input_type_basic(): from tests import models @strawberry_django.input(models.User) class UserInput: name: auto assert [ (f.name, f.type) for f in get_object_definition(UserInput, strict=True).fields ] == [ ("name", str), ] def test_partial_input_type(): from tests import models @strawberry_django.input(models.User, partial=True) class UserPartialInput: name: auto assert [ (f.name, f.type) for f in get_object_definition(UserPartialInput, strict=True).fields ] == [ ("name", StrawberryOptional(str)), ] def test_partial_input_type_inheritance(): from tests import models @strawberry_django.input(models.User) class UserInput: name: auto @strawberry_django.input(models.User, partial=True) class UserPartialInput(UserInput): pass assert [ (f.name, f.type) for f in get_object_definition(UserPartialInput, strict=True).fields ] == [ ("name", StrawberryOptional(str)), ] def test_input_type_inheritance_from_type(): from tests import models @strawberry_django.type(models.User) class User: id: auto name: auto @strawberry_django.input(models.User) class UserInput(User): pass assert [ (f.name, f.type) for f in get_object_definition(UserInput, strict=True).fields ] == [ ("id", StrawberryOptional(cast("type", strawberry.ID))), ("name", str), ] strawberry-graphql-django-0.62.0/tests/fields/test_ref.py000066400000000000000000000011551502405145400234760ustar00rootroot00000000000000from django.db import models from strawberry import auto from strawberry.types import get_object_definition import strawberry_django def test_forward_reference(): global MyBytes class ForwardReferenceModel(models.Model): string = models.CharField(max_length=50) @strawberry_django.type(ForwardReferenceModel) class Type: bytes0: "MyBytes" string: auto class MyBytes(bytes): pass assert [ (f.name, f.type) for f in get_object_definition(Type, strict=True).fields ] == [ ("bytes0", MyBytes), ("string", str), ] del MyBytes strawberry-graphql-django-0.62.0/tests/fields/test_relations.py000066400000000000000000000065311502405145400247250ustar00rootroot00000000000000from typing import TYPE_CHECKING, Optional, cast import strawberry from django.db import models from strawberry import auto from strawberry.types import get_object_definition from strawberry.types.base import ( StrawberryList, StrawberryOptional, ) import strawberry_django if TYPE_CHECKING: from strawberry_django.fields.field import StrawberryDjangoField class ParentModel(models.Model): name = models.CharField(max_length=50) class OneToOneModel(models.Model): name = models.CharField(max_length=50) parent = models.OneToOneField( ParentModel, on_delete=models.SET_NULL, related_name="one_to_one", null=True, blank=True, ) class ChildModel(models.Model): name = models.CharField(max_length=50) parents = models.ManyToManyField(ParentModel, related_name="children") @strawberry_django.type(ParentModel) class Parent: id: auto name: auto children: list["Child"] one_to_one: Optional["OneToOne"] @strawberry_django.type(OneToOneModel) class OneToOne: id: auto name: auto parent: Optional["Parent"] @strawberry_django.type(ChildModel) class Child: id: auto name: auto parents: list[Parent] def test_relation(): assert [ (f.name, f.type, cast("StrawberryDjangoField", f).is_list) for f in get_object_definition(Parent, strict=True).fields ] == [ ("id", strawberry.ID, False), ("name", str, False), ("children", StrawberryList(Child), True), ("one_to_one", StrawberryOptional(OneToOne), False), ] def test_reversed_relation(): assert [ (f.name, f.type, cast("StrawberryDjangoField", f).is_list) for f in get_object_definition(Child, strict=True).fields ] == [ ("id", strawberry.ID, False), ("name", str, False), ("parents", StrawberryList(Parent), True), ] def test_relation_query(transactional_db): @strawberry.type class Query: parent: Parent = strawberry_django.field() one_to_one: OneToOne = strawberry_django.field() schema = strawberry.Schema(query=Query) query = """\ query Query ($pk: ID!) { parent (pk: $pk) { name oneToOne { name } children { id name } } } """ parent = ParentModel.objects.create(name="Parent") result = schema.execute_sync(query, {"pk": parent.pk}) assert result.errors is None assert result.data == { "parent": {"children": [], "name": "Parent", "oneToOne": None}, } OneToOneModel.objects.create(name="OneToOne", parent=parent) result = schema.execute_sync(query, {"pk": parent.pk}) assert result.errors is None assert result.data == { "parent": {"children": [], "name": "Parent", "oneToOne": {"name": "OneToOne"}}, } child1 = ChildModel.objects.create(name="Child1") child2 = ChildModel.objects.create(name="Child2") ChildModel.objects.create(name="Child3") child1.parents.add(parent) child2.parents.add(parent) result = schema.execute_sync(query, {"pk": parent.pk}) assert result.errors is None assert result.data == { "parent": { "children": [{"id": "1", "name": "Child1"}, {"id": "2", "name": "Child2"}], "name": "Parent", "oneToOne": {"name": "OneToOne"}, }, } strawberry-graphql-django-0.62.0/tests/fields/test_types.py000066400000000000000000000345061502405145400240740ustar00rootroot00000000000000import datetime import decimal import enum import uuid from typing import Any, Optional, Union, cast import django import pytest import strawberry from django.conf import settings from django.contrib.postgres.fields import ArrayField from django.core.exceptions import FieldDoesNotExist from django.db import models from strawberry import auto from strawberry.scalars import JSON from strawberry.types import get_object_definition from strawberry.types.base import ( StrawberryContainer, StrawberryList, StrawberryOptional, ) from strawberry.types.enum import EnumDefinition, EnumValue import strawberry_django from strawberry_django.fields.field import StrawberryDjangoField from strawberry_django.type import _process_type # noqa: PLC2701 if django.VERSION >= (5, 0): from django.db.models import GeneratedField # type: ignore else: GeneratedField = None class FieldTypesModel(models.Model): boolean = models.BooleanField() char = models.CharField(max_length=50) date = models.DateField() date_time = models.DateTimeField() decimal = models.DecimalField() email = models.EmailField() file = models.FileField() file_path = models.FilePathField() float = models.FloatField() generic_ip_address = models.GenericIPAddressField() integer = models.IntegerField() image = models.ImageField() positive_big_integer = models.PositiveBigIntegerField() positive_integer = models.PositiveIntegerField() positive_small_integer = models.PositiveSmallIntegerField() slug = models.SlugField() small_integer = models.SmallIntegerField() text = models.TextField() time = models.TimeField() url = models.URLField() uuid = models.UUIDField() json = models.JSONField() generated_decimal = ( GeneratedField( expression=models.F("decimal") * 2, db_persist=True, output_field=models.DecimalField(), ) if GeneratedField is not None else None ) generated_nullable_decimal = ( GeneratedField( expression=models.F("decimal") * 2, db_persist=True, output_field=models.DecimalField(null=True, blank=True), ) if GeneratedField is not None else None ) foreign_key = models.ForeignKey( "FieldTypesModel", blank=True, related_name="related_foreign_key", on_delete=models.CASCADE, ) one_to_one = models.OneToOneField( "FieldTypesModel", blank=True, related_name="related_one_to_one", on_delete=models.CASCADE, ) many_to_many = models.ManyToManyField( "FieldTypesModel", related_name="related_many_to_many", ) def test_field_types(): @strawberry_django.type(FieldTypesModel) class Type: id: auto boolean: auto char: auto date: auto date_time: auto decimal: auto email: auto file: auto file_path: auto float: auto generic_ip_address: auto integer: auto image: auto positive_big_integer: auto positive_integer: auto positive_small_integer: auto slug: auto small_integer: auto text: auto time: auto url: auto uuid: auto json: auto expected_types: list[tuple[str, Any]] = [ ("id", strawberry.ID), ("boolean", bool), ("char", str), ("date", datetime.date), ("date_time", datetime.datetime), ("decimal", decimal.Decimal), ("email", str), ("file", strawberry_django.DjangoFileType), ("file_path", str), ("float", float), ("generic_ip_address", str), ("integer", int), ("image", strawberry_django.DjangoImageType), ("positive_big_integer", int), ("positive_integer", int), ("positive_small_integer", int), ("slug", str), ("small_integer", int), ("text", str), ("time", datetime.time), ("url", str), ("uuid", uuid.UUID), ("json", JSON), ] if django.VERSION >= (5, 0): Type.__annotations__["generated_decimal"] = auto expected_types.append(("generated_decimal", decimal.Decimal)) Type.__annotations__["generated_nullable_decimal"] = auto expected_types.append(("generated_nullable_decimal", Optional[decimal.Decimal])) type_to_test = _process_type(Type, model=FieldTypesModel) object_definition = get_object_definition(type_to_test, strict=True) assert [(f.name, f.type) for f in object_definition.fields] == expected_types def test_field_types_for_array_fields(): class ModelWithArrays(models.Model): str_array = ArrayField(models.CharField(max_length=50)) int_array = ArrayField(models.IntegerField()) @strawberry_django.type(ModelWithArrays) class Type: str_array: auto int_array: auto type_to_test = _process_type(Type, model=ModelWithArrays) object_definition = get_object_definition(type_to_test, strict=True) str_array_field = object_definition.get_field("str_array") assert str_array_field assert isinstance(str_array_field.type, StrawberryList) assert str_array_field.type.of_type is str int_array_field = object_definition.get_field("int_array") assert int_array_field assert isinstance(int_array_field.type, StrawberryList) assert int_array_field.type.of_type is int def test_field_types_for_matrix_fields(): class ModelWithMatrixes(models.Model): str_matrix = ArrayField(ArrayField(models.CharField(max_length=50))) int_matrix = ArrayField(ArrayField(models.IntegerField())) @strawberry_django.type(ModelWithMatrixes) class Type: str_matrix: auto int_matrix: auto type_to_test = _process_type(Type, model=ModelWithMatrixes) object_definition = get_object_definition(type_to_test, strict=True) str_matrix_field = object_definition.get_field("str_matrix") assert str_matrix_field assert isinstance(str_matrix_field.type, StrawberryList) assert isinstance(str_matrix_field.type.of_type, StrawberryList) assert str_matrix_field.type.of_type.of_type is str int_matrix_field = object_definition.get_field("int_matrix") assert int_matrix_field assert isinstance(int_matrix_field.type, StrawberryList) assert isinstance(int_matrix_field.type.of_type, StrawberryList) assert int_matrix_field.type.of_type.of_type is int def test_subset_of_fields(): @strawberry_django.type(FieldTypesModel) class Type: id: auto integer: auto text: auto object_definition = get_object_definition(Type, strict=True) assert [(f.name, f.type) for f in object_definition.fields] == [ ("id", strawberry.ID), ("integer", int), ("text", str), ] def test_type_extension(): @strawberry_django.type(FieldTypesModel) class Type: char: auto text: bytes # override type @strawberry.field @staticmethod def my_field() -> int: return 0 object_definition = get_object_definition(Type, strict=True) assert [(f.name, f.type) for f in object_definition.fields] == [ ("char", str), ("text", bytes), ("my_field", int), ] def test_field_does_not_exist(): with pytest.raises(FieldDoesNotExist): @strawberry_django.type(FieldTypesModel) class Type: unknown_field: auto def test_override_field_type(): @strawberry.enum class EnumType(enum.Enum): a = "A" @strawberry_django.type(FieldTypesModel) class Type: char: EnumType object_definition = get_object_definition(Type, strict=True) assert [(f.name, f.type) for f in object_definition.fields] == [ ( "char", EnumDefinition( wrapped_cls=EnumType, name="EnumType", values=[EnumValue(name="a", value="A")], description=None, ), ), ] def test_override_field_default_value(): @strawberry_django.type(FieldTypesModel) class Type: char: str = "my value" object_definition = get_object_definition(Type, strict=True) assert [(f.name, f.type) for f in object_definition.fields] == [ ("char", str), ] assert Type().char == "my value" def test_related_fields(): @strawberry_django.type(FieldTypesModel) class Type: foreign_key: auto one_to_one: auto many_to_many: auto related_foreign_key: auto related_one_to_one: auto related_many_to_many: auto object_definition = get_object_definition(Type, strict=True) assert [ (f.name, f.type, cast("StrawberryDjangoField", f).is_list) for f in object_definition.fields ] == [ ("foreign_key", strawberry_django.DjangoModelType, False), ("one_to_one", strawberry_django.DjangoModelType, False), ( "many_to_many", StrawberryList(strawberry_django.DjangoModelType), True, ), ( "related_foreign_key", StrawberryList(strawberry_django.DjangoModelType), True, ), ( "related_one_to_one", StrawberryOptional(strawberry_django.DjangoModelType), False, ), ( "related_many_to_many", StrawberryList(strawberry_django.DjangoModelType), True, ), ] def test_related_input_fields(): @strawberry_django.input(FieldTypesModel) class Input: foreign_key: auto one_to_one: auto many_to_many: auto related_foreign_key: auto related_one_to_one: auto related_many_to_many: auto expected_fields: dict[str, tuple[Union[type, StrawberryContainer], bool]] = { "foreign_key": ( strawberry_django.OneToManyInput, True, ), "one_to_one": ( strawberry_django.OneToOneInput, True, ), "many_to_many": ( strawberry_django.ManyToManyInput, True, ), "related_foreign_key": ( strawberry_django.ManyToOneInput, True, ), "related_one_to_one": ( strawberry_django.OneToOneInput, True, ), "related_many_to_many": ( strawberry_django.ManyToManyInput, True, ), } object_definition = get_object_definition(Input, strict=True) assert len(object_definition.fields) == len(expected_fields) for f in object_definition.fields: expected_type, expected_is_optional = expected_fields[f.name] assert isinstance(f, StrawberryDjangoField) assert f.is_optional == expected_is_optional assert isinstance(f.type, StrawberryOptional) assert f.type.of_type == expected_type @pytest.mark.skipif( not settings.GEOS_IMPORTED, reason="Test requires GEOS to be imported and properly configured", ) def test_geos_fields(): from strawberry_django.fields import types from tests.models import GeosFieldsModel @strawberry_django.type(GeosFieldsModel) class GeoFieldType: point: auto line_string: auto polygon: auto multi_point: auto multi_line_string: auto multi_polygon: auto geometry: auto object_definition = get_object_definition(GeoFieldType, strict=True) assert [ (f.name, cast("StrawberryOptional", f.type).of_type) for f in object_definition.fields ] == [ ("point", types.Point), ("line_string", types.LineString), ("polygon", types.Polygon), ("multi_point", types.MultiPoint), ("multi_line_string", types.MultiLineString), ("multi_polygon", types.MultiPolygon), ("geometry", types.Geometry), ] def test_inherit_type(): global Type @strawberry_django.type(FieldTypesModel) class Base: char: auto one_to_one: "Type" @strawberry_django.type(FieldTypesModel) class Type(Base): # type: ignore many_to_many: list["Type"] object_definition = get_object_definition(Type, strict=True) assert [(f.name, f.type) for f in object_definition.fields] == [ ("char", str), ("one_to_one", Type), ("many_to_many", StrawberryList(Type)), ] def test_inherit_input(): global Type @strawberry_django.type(FieldTypesModel) class Type: # type: ignore char: auto one_to_one: "Type" many_to_many: list["Type"] @strawberry_django.input(FieldTypesModel) class Input(Type): id: auto my_data: str object_definition = get_object_definition(Input, strict=True) assert [(f.name, f.type) for f in object_definition.fields] == [ ("char", str), ("one_to_one", StrawberryOptional(strawberry_django.OneToOneInput)), ( "many_to_many", StrawberryOptional(strawberry_django.ManyToManyInput), ), ("id", StrawberryOptional(cast("type", strawberry.ID))), ("my_data", str), ] def test_inherit_partial_input(): global Type @strawberry_django.type(FieldTypesModel) class Type: char: auto one_to_one: "Type" @strawberry_django.input(FieldTypesModel) class Input(Type): pass @strawberry_django.input(FieldTypesModel, partial=True) class PartialInput(Input): pass object_definition = get_object_definition(PartialInput, strict=True) assert [ (f.name, f.type, cast("StrawberryDjangoField", f).is_optional) for f in object_definition.fields ] == [ ("char", StrawberryOptional(str), True), ( "one_to_one", StrawberryOptional(strawberry_django.OneToOneInput), True, ), ] def test_notimplemented(): """Test that an unrecognized field raises `NotImplementedError`.""" class UnknownField(models.Field): """A field unknown to Strawberry.""" class UnknownModel(models.Model): """A model with UnknownField.""" field = UnknownField() @strawberry_django.type(UnknownModel) class UnknownType: field: auto @strawberry.type class Query: unknown_type: UnknownType with pytest.raises(TypeError, match=r"UnknownModel\.field"): strawberry.Schema(query=Query) strawberry-graphql-django-0.62.0/tests/filters/000077500000000000000000000000001502405145400215115ustar00rootroot00000000000000strawberry-graphql-django-0.62.0/tests/filters/__init__.py000066400000000000000000000000001502405145400236100ustar00rootroot00000000000000strawberry-graphql-django-0.62.0/tests/filters/test_filters.py000066400000000000000000000404141502405145400245750ustar00rootroot00000000000000import textwrap from enum import Enum from typing import Generic, Optional, TypeVar, cast import pytest import strawberry from django.test import override_settings from strawberry import auto from strawberry.annotation import StrawberryAnnotation from strawberry.types import ExecutionResult import strawberry_django from tests import models, utils with override_settings(STRAWBERRY_DJANGO={"USE_DEPRECATED_FILTERS": True}): @strawberry_django.filter_type(models.NameDescriptionMixin) class NameDescriptionFilter: name: auto description: auto @strawberry_django.filter_type(models.Vegetable, lookups=True) class VegetableFilter(NameDescriptionFilter): id: auto world_production: auto @strawberry_django.filters.filter_type(models.Color, lookups=True) class ColorFilter: id: auto name: auto @strawberry_django.filters.filter_type(models.Fruit, lookups=True) class FruitFilter: id: auto name: auto color: Optional[ColorFilter] @strawberry.enum class FruitEnum(Enum): strawberry = "strawberry" banana = "banana" @strawberry_django.filters.filter_type(models.Fruit) class EnumFilter: name: Optional[FruitEnum] = strawberry.UNSET _T = TypeVar("_T") @strawberry.input class FilterInLookup(Generic[_T]): exact: Optional[_T] = strawberry.UNSET in_list: Optional[list[_T]] = strawberry.UNSET @strawberry_django.filters.filter_type(models.Fruit) class EnumLookupFilter: name: Optional[FilterInLookup[FruitEnum]] = strawberry.UNSET @strawberry.input class NonFilter: name: FruitEnum def filter(self, queryset): raise NotImplementedError @strawberry_django.filters.filter_type(models.Fruit) class FieldFilter: search: str def filter_search(self, queryset): return queryset.filter(name__icontains=self.search) @strawberry_django.filters.filter_type(models.Fruit) class TypeFilter: name: auto def filter(self, queryset): if not self.name: return queryset return queryset.filter(name__icontains=self.name) @strawberry_django.type(models.Vegetable, filters=VegetableFilter) class Vegetable: id: auto name: auto description: auto world_production: auto @strawberry_django.type(models.Fruit, filters=FruitFilter) class Fruit: id: auto name: auto @strawberry.type class Query: fruits: list[Fruit] = strawberry_django.field() field_filter: list[Fruit] = strawberry_django.field(filters=FieldFilter) type_filter: list[Fruit] = strawberry_django.field(filters=TypeFilter) enum_filter: list[Fruit] = strawberry_django.field(filters=EnumFilter) enum_lookup_filter: list[Fruit] = strawberry_django.field( filters=EnumLookupFilter ) _ = strawberry.Schema(query=Query) @pytest.fixture(autouse=True) def _autouse_old_filters(settings): settings.STRAWBERRY_DJANGO = {"USE_DEPRECATED_FILTERS": True} @pytest.fixture def query(): return utils.generate_query(Query) def test_field_filter_definition(): from strawberry_django.fields.field import StrawberryDjangoField field = StrawberryDjangoField(type_annotation=StrawberryAnnotation(Fruit)) assert field.get_filters() == FruitFilter field = StrawberryDjangoField( type_annotation=StrawberryAnnotation(Fruit), filters=None, ) assert field.get_filters() is None def test_without_filtering(query, fruits): result = query("{ fruits { id name } }") assert not result.errors assert result.data["fruits"] == [ {"id": "1", "name": "strawberry"}, {"id": "2", "name": "raspberry"}, {"id": "3", "name": "banana"}, ] def test_exact(query, fruits): result = query('{ fruits(filters: { name: { exact: "strawberry" } }) { id name } }') assert not result.errors assert result.data["fruits"] == [ {"id": "1", "name": "strawberry"}, ] def test_lt_gt(query, fruits): result = query("{ fruits(filters: { id: { gt: 1, lt: 3 } }) { id name } }") assert not result.errors assert result.data["fruits"] == [ {"id": "2", "name": "raspberry"}, ] def test_in_list(query, fruits): result = query("{ fruits(filters: { id: { inList: [ 1, 3 ] } }) { id name } }") assert not result.errors assert result.data["fruits"] == [ {"id": "1", "name": "strawberry"}, {"id": "3", "name": "banana"}, ] def test_not(query, fruits): result = query("""{ fruits( filters: { NOT: { name: { endsWith: "berry" } } } ) { id name } }""") assert not result.errors assert result.data["fruits"] == [ {"id": "3", "name": "banana"}, ] def test_and(query, fruits): result = query( """{ fruits(filters: { name: { endsWith: "berry" }, AND: { id: { exact: 2 } } }) { id name } }""", ) assert not result.errors assert result.data["fruits"] == [ {"id": "2", "name": "raspberry"}, ] def test_or(query, fruits): result = query( """{ fruits(filters: { id: { exact: 1 }, OR: { id: { exact: 3 } } }) { id name } }""", ) assert not result.errors assert result.data["fruits"] == [ {"id": "1", "name": "strawberry"}, {"id": "3", "name": "banana"}, ] def test_relationship(query, fruits): color = models.Color.objects.create(name="red") color.fruits.set([fruits[0], fruits[1]]) result = query( '{ fruits(filters: { color: { name: { iExact: "RED" } } }) { id name } }', ) assert not result.errors assert result.data["fruits"] == [ {"id": "1", "name": "strawberry"}, {"id": "2", "name": "raspberry"}, ] def test_field_filter_method(query, fruits): result = query('{ fruits: fieldFilter(filters: { search: "berry" }) { id name } }') assert not result.errors assert result.data["fruits"] == [ {"id": "1", "name": "strawberry"}, {"id": "2", "name": "raspberry"}, ] def test_type_filter_method(query, fruits): result = query('{ fruits: typeFilter(filters: { name: "anana" }) { id name } }') assert not result.errors assert result.data["fruits"] == [ {"id": "3", "name": "banana"}, ] def test_resolver_filter(fruits): @strawberry.type class Query: @strawberry.field def fruits(self, filters: FruitFilter) -> list[Fruit]: queryset = models.Fruit.objects.all() return cast( "list[Fruit]", strawberry_django.filters.apply(filters, queryset) ) query = utils.generate_query(Query) result = query('{ fruits(filters: { name: { exact: "strawberry" } }) { id name } }') assert isinstance(result, ExecutionResult) assert not result.errors assert result.data is not None assert result.data["fruits"] == [ {"id": "1", "name": "strawberry"}, ] def test_empty_resolver_filter(): @strawberry.type class Query: @strawberry.field def fruits(self, filters: FruitFilter) -> list[Fruit]: queryset = models.Fruit.objects.none() return cast( "list[Fruit]", strawberry_django.filters.apply(filters, queryset) ) query = utils.generate_query(Query) result = query('{ fruits(filters: { name: { exact: "strawberry" } }) { id name } }') assert isinstance(result, ExecutionResult) assert not result.errors assert result.data is not None assert result.data["fruits"] == [] @pytest.mark.asyncio @pytest.mark.django_db(transaction=True) async def test_async_resolver_filter(fruits): @strawberry.type class Query: @strawberry.field async def fruits(self, filters: FruitFilter) -> list[Fruit]: queryset = models.Fruit.objects.all() queryset = strawberry_django.filters.apply(filters, queryset) # cast fixes funny typing issue between list and List return cast("list[Fruit]", [fruit async for fruit in queryset]) query = utils.generate_query(Query) result = await query( # type: ignore '{ fruits(filters: { name: { exact: "strawberry" } }) { id name } }' ) assert isinstance(result, ExecutionResult) assert not result.errors assert result.data is not None assert len(result.data["fruits"]) == 1 assert result.data["fruits"][0]["name"] == "strawberry" def test_resolver_filter_with_inheritance(vegetables): @strawberry.type class Query: @strawberry.field def vegetables(self, filters: VegetableFilter) -> list[Vegetable]: queryset = models.Vegetable.objects.all() return cast( "list[Vegetable]", strawberry_django.filters.apply(filters, queryset) ) query = utils.generate_query(Query) result = query(""" { vegetables( filters: { worldProduction: { gt: 100e6 } OR: { name: { exact: "cucumber" } } } ) { id name } } """) assert isinstance(result, ExecutionResult) assert not result.errors assert result.data is not None assert result.data["vegetables"] == [ {"id": "2", "name": "cucumber"}, {"id": "3", "name": "onion"}, ] def test_resolver_filter_with_info(fruits): from strawberry.types.info import Info @strawberry_django.filters.filter_type(models.Fruit, lookups=True) class FruitFilterWithInfo: id: auto name: auto custom_field: bool def filter_custom_field(self, queryset, info: Info): # Test here is to prove that info can be passed properly assert isinstance(info, Info) return queryset.filter(name="banana") @strawberry.type class Query: @strawberry.field def fruits(self, filters: FruitFilterWithInfo, info: Info) -> list[Fruit]: queryset = models.Fruit.objects.all() return cast( "list[Fruit]", strawberry_django.filters.apply(filters, queryset, info=info), ) query = utils.generate_query(Query) result = query("{ fruits(filters: { customField: true }) { id name } }") assert isinstance(result, ExecutionResult) assert not result.errors assert result.data is not None assert result.data["fruits"] == [ {"id": "3", "name": "banana"}, ] def test_resolver_filter_override_with_info(fruits): from strawberry.types.info import Info @strawberry_django.filters.filter_type(models.Fruit, lookups=True) class FruitFilterWithInfo: custom_field: bool def filter(self, queryset, info: Info): # Test here is to prove that info can be passed properly assert isinstance(info, Info) return queryset.filter(name="banana") @strawberry.type class Query: @strawberry.field def fruits(self, filters: FruitFilterWithInfo, info: Info) -> list[Fruit]: queryset = models.Fruit.objects.all() return cast( "list[Fruit]", strawberry_django.filters.apply(filters, queryset, info=info), ) query = utils.generate_query(Query) result = query("{ fruits(filters: { customField: true }) { id name } }") assert isinstance(result, ExecutionResult) assert not result.errors assert result.data is not None assert result.data["fruits"] == [ {"id": "3", "name": "banana"}, ] def test_resolver_nonfilter(fruits): @strawberry.type class Query: @strawberry.field def fruits(self, filters: NonFilter) -> list[Fruit]: queryset = models.Fruit.objects.all() return cast( "list[Fruit]", strawberry_django.filters.apply(filters, queryset) ) query = utils.generate_query(Query) result = query("{ fruits(filters: { name: strawberry } ) { id name } }") assert isinstance(result, ExecutionResult) assert not result.errors def test_enum(query, fruits): result = query("{ fruits: enumFilter(filters: { name: strawberry }) { id name } }") assert not result.errors assert result.data["fruits"] == [ {"id": "1", "name": "strawberry"}, ] def test_enum_lookup_exact(query, fruits): result = query( """{ fruits: enumLookupFilter(filters: { name: { exact: strawberry } }) { id name } }""", ) assert not result.errors assert result.data["fruits"] == [ {"id": "1", "name": "strawberry"}, ] def test_enum_lookup_in(query, fruits): result = query( """{ fruits: enumLookupFilter(filters: { name: { inList: [strawberry] } }) { id name } }""", ) assert not result.errors assert result.data["fruits"] == [ {"id": "1", "name": "strawberry"}, ] result = query( """{ fruits: enumLookupFilter(filters: { name: { inList: [strawberry, banana] } }) { id name } }""", ) assert not result.errors assert result.data["fruits"] == [ {"id": "1", "name": "strawberry"}, {"id": "3", "name": "banana"}, ] @pytest.mark.django_db(transaction=True) @pytest.mark.parametrize("use_pk", [True, False]) def test_adds_id_filter(use_pk: bool): with override_settings( STRAWBERRY_DJANGO={"DEFAULT_PK_FIELD_NAME": "pk" if use_pk else "id"}, ): field_name = "pk" if use_pk else "id" @strawberry_django.type(models.User) class UserType: name: strawberry.auto @strawberry.type class Query: user: UserType = strawberry_django.field() schema = strawberry.Schema(query=Query) assert ( textwrap.dedent(str(schema)) == textwrap.dedent( f"""\ type Query {{ user({field_name}: ID!): UserType! }} type UserType {{ name: String! }} """, ).strip() ) user = models.User.objects.create(name="Some User") res = schema.execute_sync( f"""\ query GetUser ($id: ID!) {{ user({field_name}: $id) {{ name }} }} """, variable_values={"id": user.pk}, ) assert res.errors is None assert res.data == { "user": { "name": "Some User", }, } @pytest.mark.django_db(transaction=True) def test_pk_inserted_for_root_field_only(): @strawberry_django.filters.filter_type(models.Group) class GroupFilter: name: str @strawberry_django.type(models.Group, filters=GroupFilter) class GroupType: name: strawberry.auto @strawberry_django.type(models.User) class UserType: name: strawberry.auto group: Optional[GroupType] get_group: GroupType group_prop: GroupType @strawberry.type class Query: user: UserType = strawberry_django.field() schema = strawberry.Schema(query=Query) assert ( textwrap.dedent(str(schema)) == textwrap.dedent( """\ type GroupType { name: String! } type Query { user(pk: ID!): UserType! } type UserType { name: String! group: GroupType getGroup: GroupType! groupProp: GroupType! } """, ).strip() ) group = models.Group.objects.create(name="Some Group") user = models.User.objects.create(name="Some User", group=group) res = schema.execute_sync( """\ query GetUser ($pk: ID!) { user(pk: $pk) { name group { name } getGroup { name } groupProp { name } } } """, variable_values={"pk": user.pk}, ) assert res.errors is None assert res.data == { "user": { "name": "Some User", "group": {"name": "Some Group"}, "getGroup": {"name": "Some Group"}, "groupProp": {"name": "Some Group"}, }, } strawberry-graphql-django-0.62.0/tests/filters/test_filters_v2.py000066400000000000000000000407441502405145400252120ustar00rootroot00000000000000# ruff: noqa: B904, BLE001, F811, PT012, A001 from enum import Enum from typing import Annotated, Any, Optional, cast import pytest import strawberry from django.db.models import Case, Count, Q, QuerySet, Value, When from strawberry import auto from strawberry.exceptions import MissingArgumentsAnnotationsError from strawberry.relay import GlobalID from strawberry.types import ExecutionResult, get_object_definition from strawberry.types.base import WithStrawberryObjectDefinition, get_object_definition from typing_extensions import Self import strawberry_django from strawberry_django.exceptions import ( ForbiddenFieldArgumentError, MissingFieldArgumentError, ) from strawberry_django.fields import filter_types from strawberry_django.fields.field import StrawberryDjangoField from strawberry_django.fields.filter_order import ( FilterOrderField, FilterOrderFieldResolver, filter_field, ) from strawberry_django.filters import process_filters, resolve_value from tests import models, utils from tests.types import Fruit, FruitType, Vegetable @strawberry.enum class Version(Enum): ONE = "first" TWO = "second" THREE = "third" @strawberry_django.filter_type(models.Vegetable, lookups=True) class VegetableFilter: id: auto name: auto AND: Optional[list[Self]] = strawberry.UNSET OR: Optional[list[Self]] = strawberry.UNSET NOT: Optional[list[Self]] = strawberry.UNSET @strawberry_django.filter_type(models.Color, lookups=True) class ColorFilter: id: auto name: auto @strawberry_django.filter_field def name_simple(self, prefix: str, value: str): return Q(**{f"{prefix}name": value}) @strawberry_django.filter_type(models.FruitType, lookups=True) class FruitTypeFilter: name: auto fruits: Optional[ Annotated["FruitFilter", strawberry.lazy("tests.filters.test_filters_v2")] ] @strawberry_django.filter_type(models.Fruit, lookups=True) class FruitFilter: color_id: auto name: auto sweetness: auto types: Optional[FruitTypeFilter] color: Optional[ColorFilter] = filter_field(filter_none=True) @strawberry_django.filter_field def types_number( self, info, queryset: QuerySet, prefix, value: filter_types.ComparisonFilterLookup[int], ): return process_filters( cast("WithStrawberryObjectDefinition", value), queryset.annotate( count=Count(f"{prefix}types__id"), count_nulls=Case( When(count=0, then=Value(None)), default="count", ), ), info, "count_nulls__", ) @strawberry_django.filter_field def double( self, queryset: QuerySet, prefix, value: bool, ): return queryset.union(queryset, all=True), Q() @strawberry_django.filter_field def filter(self, info, queryset: QuerySet, prefix): return process_filters( cast("WithStrawberryObjectDefinition", self), queryset.filter(~Q(**{f"{prefix}name": "DARK_BERRY"})), info, prefix, skip_object_filter_method=True, ) @strawberry.type class Query: types: list[FruitType] = strawberry_django.field(filters=FruitTypeFilter) fruits: list[Fruit] = strawberry_django.field(filters=FruitFilter) vegetables: list[Vegetable] = strawberry_django.field(filters=VegetableFilter) @pytest.fixture def query(): return utils.generate_query(Query) @pytest.mark.parametrize( ("value", "resolved"), [ (2, 2), ("something", "something"), (GlobalID("", "24"), "24"), (Version.ONE, Version.ONE.value), ( [1, "str", GlobalID("", "24"), Version.THREE], [1, "str", "24", Version.THREE.value], ), ], ) def test_resolve_value(value, resolved): assert resolve_value(value) == resolved def test_filter_field_missing_prefix(): with pytest.raises( MissingFieldArgumentError, match=r".*\"prefix\".*\"field_method\".*" ): @strawberry_django.filter_field def field_method(): pass def test_filter_field_missing_value(): with pytest.raises( MissingFieldArgumentError, match=r".*\"value\".*\"field_method\".*" ): @strawberry_django.filter_field def field_method(prefix): pass def test_filter_field_missing_value_annotation(): with pytest.raises( MissingArgumentsAnnotationsError, match=r"Missing annotation.*\"value\".*\"field_method\".*", ): @strawberry_django.filter_field def field_method(prefix, value): pass def test_filter_field(): try: @strawberry_django.filter_field def field_method(self, root, info, prefix, value: str, queryset): pass except Exception as exc: raise pytest.fail(f"DID RAISE {exc}") def test_filter_field_sequence(): with pytest.raises( ForbiddenFieldArgumentError, match=r".*\"sequence\".*\"field_method\".*", ): @strawberry_django.filter_field def field_method(prefix, value: auto, sequence, queryset): pass def test_filter_field_forbidden_param_annotation(): with pytest.raises( MissingArgumentsAnnotationsError, match=r".*\"forbidden_param\".*\"field_method\".*", ): @strawberry_django.filter_field def field_method(prefix, value: auto, queryset, forbidden_param): pass def test_filter_field_forbidden_param(): with pytest.raises( ForbiddenFieldArgumentError, match=r".*\"forbidden_param\".*\"field_method\".*", ): @strawberry_django.filter_field def field_method(prefix, value: auto, queryset, forbidden_param: str): pass def test_filter_field_missing_queryset(): with pytest.raises( MissingFieldArgumentError, match=r".*\"queryset\".*\"filter\".*" ): @strawberry_django.filter_field def filter(prefix): pass def test_filter_field_value_forbidden_on_object(): with pytest.raises(ForbiddenFieldArgumentError, match=r".*\"value\".*\"filter\".*"): @strawberry_django.filter_field def field_method(prefix, queryset, value: auto): pass @strawberry_django.filter_field def filter(prefix, queryset, value: auto): pass def test_filter_field_on_object(): try: @strawberry_django.filter_field def filter(self, root, info, prefix, queryset): pass except Exception as exc: raise pytest.fail(f"DID RAISE {exc}") def test_filter_field_method(): @strawberry_django.filter_type(models.Fruit) class Filter: @strawberry_django.order_field def custom_filter(self, root, info, prefix, value: auto, queryset): assert self == filter_, "Unexpected self passed" assert root == filter_, "Unexpected root passed" assert info == fake_info, "Unexpected info passed" assert prefix == "ROOT", "Unexpected prefix passed" assert value == "SOMETHING", "Unexpected value passed" assert queryset == qs, "Unexpected queryset passed" return Q(name=1) filter_: Any = Filter(custom_filter="SOMETHING") # type: ignore fake_info: Any = object() qs: Any = object() q_object = process_filters(filter_, qs, fake_info, prefix="ROOT")[1] assert q_object, "Filter was not called" def test_filter_object_method(): @strawberry_django.filters.filter_type(models.Fruit) class Filter: @strawberry_django.filter_field def field_filter(self, value: str, prefix): raise AssertionError("Never called due to object filter override") @strawberry_django.filter_field def filter(self, root, info, prefix, queryset): assert self == filter_, "Unexpected self passed" assert root == filter_, "Unexpected root passed" assert info == fake_info, "Unexpected info passed" assert prefix == "ROOT", "Unexpected prefix passed" assert queryset == qs, "Unexpected queryset passed" return queryset, Q(name=1) filter_: Any = Filter() fake_info: Any = object() qs: Any = object() q_object = process_filters(filter_, qs, fake_info, prefix="ROOT")[1] assert q_object, "Filter was not called" def test_filter_value_resolution(): @strawberry_django.filters.filter_type(models.Fruit) class Filter: id: Optional[strawberry_django.ComparisonFilterLookup[GlobalID]] gid = GlobalID("FruitNode", "125") filter_: Any = Filter( id=strawberry_django.ComparisonFilterLookup( exact=gid, range=strawberry_django.RangeLookup(start=gid, end=gid) ) ) object_: Any = object() q = process_filters(filter_, object_, object_)[1] assert q == Q(id__exact="125", id__range=["125", "125"]) def test_filter_method_value_resolution(): @strawberry_django.filters.filter_type(models.Fruit) class Filter: @strawberry_django.filter_field(resolve_value=True) def field_filter_resolved(self, value: GlobalID, prefix): assert isinstance(value, str) return Q() @strawberry_django.filter_field def field_filter_skip_resolved(self, value: GlobalID, prefix): assert isinstance(value, GlobalID) return Q() gid = GlobalID("FruitNode", "125") filter_: Any = Filter(field_filter_resolved=gid, field_filter_skip_resolved=gid) # type: ignore object_: Any = object() process_filters(filter_, object_, object_) def test_filter_type(): @strawberry_django.filter_type(models.Fruit, lookups=True) class FruitOrder: id: auto name: auto sweetness: auto @strawberry_django.filter_field def custom_filter(self, value: str, prefix: str): pass @strawberry_django.filter_field def custom_filter2( self, value: filter_types.BaseFilterLookup[str], prefix: str ): pass assert [ ( f.name, f.__class__, f.type.of_type.__name__, # type: ignore f.base_resolver.__class__ if f.base_resolver else None, ) for f in get_object_definition(FruitOrder, strict=True).fields if f.name not in {"NOT", "AND", "OR", "DISTINCT"} ] == [ ("id", StrawberryDjangoField, "BaseFilterLookup", None), ("name", StrawberryDjangoField, "FilterLookup", None), ("sweetness", StrawberryDjangoField, "ComparisonFilterLookup", None), ( "custom_filter", FilterOrderField, "str", FilterOrderFieldResolver, ), ( "custom_filter2", FilterOrderField, "BaseFilterLookup", FilterOrderFieldResolver, ), ] def test_filter_methods(query, db, fruits): t1 = models.FruitType.objects.create(name="Type1") t2 = models.FruitType.objects.create(name="Type2") f1, f2, f3 = models.Fruit.objects.all() _ = models.Fruit.objects.create(name="DARK_BERRY") f2.types.add(t1) f3.types.add(t1, t2) result = query(""" { fruits(filters: { typesNumber: { gt: 1 } NOT: { color: { nameSimple: "sample" } } OR: { typesNumber: { isNull: true } } }) { id } } """) assert not result.errors assert result.data["fruits"] == [ {"id": str(f1.id)}, {"id": str(f3.id)}, ] def test_filter_distinct(query, db, fruits): t1 = models.FruitType.objects.create(name="type_1") t2 = models.FruitType.objects.create(name="type_2") f1 = models.Fruit.objects.all()[0] f1.types.add(t1, t2) result = query(""" { fruits( filters: {types: { name: { iContains: "type" } } } ) { id name } } """) assert not result.errors assert len(result.data["fruits"]) == 2 result = query(""" { fruits( filters: { DISTINCT: true, types: { name: { iContains: "type" } } } ) { id name } } """) assert not result.errors assert len(result.data["fruits"]) == 1 def test_filter_and_or_not(query, db): v1 = models.Vegetable.objects.create( name="v1", description="d1", world_production=100 ) v2 = models.Vegetable.objects.create( name="v2", description="d2", world_production=200 ) v3 = models.Vegetable.objects.create( name="v3", description="d3", world_production=300 ) # Test impossible AND result = query(""" { vegetables(filters: { AND: [{ name: { exact: "v1" } }, { name: { exact: "v2" } }] }) { id } } """) assert not result.errors assert len(result.data["vegetables"]) == 0 # Test AND with contains result = query(""" { vegetables(filters: { AND: [{ name: { contains: "v" } }, { name: { contains: "2" } }] }) { id } } """) assert not result.errors assert len(result.data["vegetables"]) == 1 assert result.data["vegetables"][0]["id"] == str(v2.pk) # Test OR result = query(""" { vegetables(filters: { OR: [{ name: { exact: "v1" } }, { name: { exact: "v3" } }] }) { id } } """) assert not result.errors assert len(result.data["vegetables"]) == 2 assert { result.data["vegetables"][0]["id"], result.data["vegetables"][1]["id"], } == {str(v1.pk), str(v3.pk)} # Test NOT result = query(""" { vegetables(filters: { NOT: [{ name: { exact: "v1" } }, { name: { exact: "v2" } }] }) { id } } """) assert not result.errors assert len(result.data["vegetables"]) == 1 assert result.data["vegetables"][0]["id"] == str(v3.pk) # Test interaction with simple filters. No matches due to AND logic relative to simple filters. result = query( """ { vegetables(filters: { id: { exact: """ + str(v1.pk) + """ }, AND: [{ name: { exact: "v2" } }] }) { id } } """ ) assert not result.errors assert len(result.data["vegetables"]) == 0 # Test interaction with simple filters. Match on same record result = query( """ { vegetables(filters: { id: { exact: """ + str(v1.pk) + """ }, AND: [{ name: { exact: "v1" } }] }) { id } } """ ) assert not result.errors assert len(result.data["vegetables"]) == 1 assert result.data["vegetables"][0]["id"] == str(v1.pk) def test_filter_none(query, db): yellow = models.Color.objects.create(name="yellow") models.Fruit.objects.create(name="banana", color=yellow) f1 = models.Fruit.objects.create(name="unknown") f2 = models.Fruit.objects.create(name="unknown2") result = query(""" { fruits(filters: {color: null}) { id } } """) assert not result.errors assert result.data["fruits"] == [ {"id": str(f1.id)}, {"id": str(f2.id)}, ] def test_empty_resolver_filter(): @strawberry.type class Query: @strawberry.field def fruits(self, filters: FruitFilter) -> list[Fruit]: queryset = models.Fruit.objects.none() info: Any = object() return cast( "list[Fruit]", strawberry_django.filters.apply(filters, queryset, info) ) query = utils.generate_query(Query) result = query('{ fruits(filters: { name: { exact: "strawberry" } }) { name } }') assert isinstance(result, ExecutionResult) assert not result.errors assert result.data is not None assert result.data["fruits"] == [] @pytest.mark.asyncio @pytest.mark.django_db(transaction=True) async def test_async_resolver_filter(fruits): @strawberry.type class Query: @strawberry.field async def fruits(self, filters: FruitFilter) -> list[Fruit]: queryset = models.Fruit.objects.all() info: Any = object() queryset = strawberry_django.filters.apply(filters, queryset, info) # cast fixes funny typing issue between list and List return cast("list[Fruit]", [fruit async for fruit in queryset]) query = utils.generate_query(Query) result = await query( # type: ignore '{ fruits(filters: { name: { exact: "strawberry" } }) { name } }' ) assert isinstance(result, ExecutionResult) assert not result.errors assert result.data is not None assert result.data["fruits"] == [ {"name": "strawberry"}, ] strawberry-graphql-django-0.62.0/tests/filters/test_types.py000066400000000000000000000101031502405145400242610ustar00rootroot00000000000000from typing import Optional, cast import pytest import strawberry from strawberry import auto from strawberry.types import get_object_definition from strawberry.types.base import StrawberryOptional import strawberry_django from strawberry_django.filters import get_django_model_filter_input_type from strawberry_django.settings import strawberry_django_settings from tests import models DjangoModelFilterInput = get_django_model_filter_input_type() @pytest.fixture(autouse=True) def _filter_order_settings(settings): settings.STRAWBERRY_DJANGO = { **strawberry_django_settings(), "USE_DEPRECATED_FILTERS": True, } def test_filter(): @strawberry_django.filters.filter_type(models.Fruit) class Filter: id: auto name: auto color: auto types: auto object_definition = get_object_definition(Filter, strict=True) assert [(f.name, f.type) for f in object_definition.fields] == [ ("id", StrawberryOptional(cast("type", strawberry.ID))), ("name", StrawberryOptional(str)), ("color", StrawberryOptional(DjangoModelFilterInput)), ("types", StrawberryOptional(DjangoModelFilterInput)), ("AND", StrawberryOptional(Filter)), ("OR", StrawberryOptional(Filter)), ("NOT", StrawberryOptional(Filter)), ("DISTINCT", StrawberryOptional(bool)), ] def test_lookups(): @strawberry_django.filters.filter_type(models.Fruit, lookups=True) class Filter: id: auto name: auto color: auto types: auto object_definition = get_object_definition(Filter, strict=True) assert [ (f.name, f.type.of_type.__name__) # type: ignore for f in object_definition.fields ] == [ ("id", "FilterLookup"), ("name", "FilterLookup"), ("color", "DjangoModelFilterInput"), ("types", "DjangoModelFilterInput"), ("AND", "Filter"), ("OR", "Filter"), ("NOT", "Filter"), ("DISTINCT", "bool"), ] def test_inherit(testtype): @testtype(models.Fruit) class Base: id: auto name: auto color: auto types: auto @strawberry_django.filters.filter_type(models.Fruit) class Filter(Base): pass object_definition = get_object_definition(Filter, strict=True) assert [(f.name, f.type) for f in object_definition.fields] == [ ("id", StrawberryOptional(cast("type", strawberry.ID))), ("name", StrawberryOptional(str)), ("color", StrawberryOptional(DjangoModelFilterInput)), ("types", StrawberryOptional(DjangoModelFilterInput)), ("AND", StrawberryOptional(Filter)), ("OR", StrawberryOptional(Filter)), ("NOT", StrawberryOptional(Filter)), ("DISTINCT", StrawberryOptional(bool)), ] def test_relationship(): @strawberry_django.filters.filter_type(models.Color) class ColorFilter: name: auto @strawberry_django.filters.filter_type(models.Fruit) class Filter: color: Optional[ColorFilter] object_definition = get_object_definition(Filter, strict=True) assert [(f.name, f.type) for f in object_definition.fields] == [ ("color", StrawberryOptional(ColorFilter)), ("AND", StrawberryOptional(Filter)), ("OR", StrawberryOptional(Filter)), ("NOT", StrawberryOptional(Filter)), ("DISTINCT", StrawberryOptional(bool)), ] def test_relationship_with_inheritance(): @strawberry_django.filters.filter_type(models.Color) class ColorFilter: name: auto @strawberry_django.type(models.Fruit) class Base: color: auto @strawberry_django.filters.filter_type(models.Fruit) class Filter(Base): color: Optional[ColorFilter] object_definition = get_object_definition(Filter, strict=True) assert [(f.name, f.type) for f in object_definition.fields] == [ ("color", StrawberryOptional(ColorFilter)), ("AND", StrawberryOptional(Filter)), ("OR", StrawberryOptional(Filter)), ("NOT", StrawberryOptional(Filter)), ("DISTINCT", StrawberryOptional(bool)), ] strawberry-graphql-django-0.62.0/tests/models.py000066400000000000000000000070061502405145400217010ustar00rootroot00000000000000from typing import TYPE_CHECKING, Optional from django.core.exceptions import ImproperlyConfigured, ValidationError from django.db import models from strawberry_django.descriptors import model_property if TYPE_CHECKING: from django.db.models.manager import RelatedManager def validate_fruit_type(value: str): if "rotten" in value: raise ValidationError("We do not allow rotten fruits.") class NameDescriptionMixin(models.Model): name = models.CharField(max_length=20) description = models.TextField() class Meta: abstract = True class Vegetable(NameDescriptionMixin): world_production = models.FloatField() class Fruit(models.Model): id: Optional[int] name = models.CharField(max_length=20) color_id: Optional[int] color = models.ForeignKey( "Color", null=True, blank=True, related_name="fruits", on_delete=models.CASCADE, ) types = models.ManyToManyField("FruitType", related_name="fruits") sweetness = models.IntegerField( default=5, help_text="Level of sweetness, from 1 to 10", ) picture = models.ImageField( null=True, blank=True, default=None, upload_to=".tmp_upload", ) def name_upper(self): return self.name.upper() @property def name_lower(self): return self.name.lower() @model_property def name_length(self) -> int: return len(self.name) class TomatoWithRequiredPicture(models.Model): name = models.CharField(max_length=20) picture = models.ImageField( null=False, blank=False, upload_to=".tmp_upload", ) class Color(models.Model): fruits: "RelatedManager[Fruit]" name = models.CharField(max_length=20) class FruitType(models.Model): id: Optional[int] name = models.CharField(max_length=20, validators=[validate_fruit_type]) class User(models.Model): name = models.CharField(max_length=50) group_id: Optional[int] group = models.ForeignKey( "Group", null=True, blank=True, related_name="users", on_delete=models.CASCADE, ) tag = models.OneToOneField("Tag", null=True, on_delete=models.CASCADE) @property def group_prop(self) -> Optional["Group"]: return self.group def get_group(self) -> Optional["Group"]: return self.group class Group(models.Model): users: "RelatedManager[User]" name = models.CharField(max_length=50) tags = models.ManyToManyField("Tag", null=True, related_name="groups") class Tag(models.Model): name = models.CharField(max_length=50) class Book(models.Model): """Model with lots of extra metadata.""" title = models.CharField( max_length=20, blank=False, null=False, help_text="The name by which the book is known.", ) try: from django.contrib.gis.db import models as geos_fields GEOS_IMPORTED = True class GeosFieldsModel(models.Model): point = geos_fields.PointField(null=True, blank=True) line_string = geos_fields.LineStringField(null=True, blank=True) polygon = geos_fields.PolygonField(null=True, blank=True) multi_point = geos_fields.MultiPointField(null=True, blank=True) multi_line_string = geos_fields.MultiLineStringField(null=True, blank=True) multi_polygon = geos_fields.MultiPolygonField(null=True, blank=True) geometry = geos_fields.GeometryField(null=True, blank=True) except ImproperlyConfigured: GEOS_IMPORTED = False strawberry-graphql-django-0.62.0/tests/mutations/000077500000000000000000000000001502405145400220645ustar00rootroot00000000000000strawberry-graphql-django-0.62.0/tests/mutations/__init__.py000066400000000000000000000000001502405145400241630ustar00rootroot00000000000000strawberry-graphql-django-0.62.0/tests/mutations/conftest.py000066400000000000000000000062741502405145400242740ustar00rootroot00000000000000from typing import cast import pytest import strawberry from django.conf import settings from django.utils.functional import SimpleLazyObject from strawberry import auto import strawberry_django from strawberry_django import mutations from strawberry_django.mutations import resolvers from tests import models, utils from tests.types import ( Color, ColorInput, ColorPartialInput, Fruit, FruitInput, FruitPartialInput, FruitType, FruitTypeInput, FruitTypePartialInput, TomatoWithRequiredPictureInput, TomatoWithRequiredPicturePartialInput, TomatoWithRequiredPictureType, ) @strawberry_django.filters.filter_type(models.Fruit, lookups=True) class FruitFilter: id: auto name: auto @strawberry.type class Mutation: create_fruit: Fruit = mutations.create(FruitInput) create_fruits: list[Fruit] = mutations.create(list[FruitInput]) patch_fruits: list[Fruit] = mutations.update(list[FruitPartialInput], key_attr="id") update_fruits: list[Fruit] = mutations.update( FruitPartialInput, filters=FruitFilter, key_attr="id" ) create_tomato_with_required_picture: TomatoWithRequiredPictureType = ( mutations.create(TomatoWithRequiredPictureInput) ) update_tomato_with_required_picture: TomatoWithRequiredPictureType = ( mutations.update(TomatoWithRequiredPicturePartialInput) ) @strawberry_django.mutation def update_lazy_fruit(self, info, data: FruitPartialInput) -> Fruit: fruit = SimpleLazyObject(models.Fruit.objects.get) return cast( "Fruit", resolvers.update( info, fruit, resolvers.parse_input(info, vars(data), key_attr="id"), key_attr="id", ), ) @strawberry_django.mutation def delete_lazy_fruit(self, info) -> Fruit: fruit = SimpleLazyObject(models.Fruit.objects.get) return cast( "Fruit", resolvers.delete( info, fruit, ), ) delete_fruits: list[Fruit] = mutations.delete(filters=FruitFilter) create_color: Color = mutations.create(ColorInput) create_colors: list[Color] = mutations.create(ColorInput) update_colors: list[Color] = mutations.update(ColorPartialInput) delete_colors: list[Color] = mutations.delete() create_fruit_type: FruitType = mutations.create(FruitTypeInput) create_fruit_types: list[FruitType] = mutations.create(FruitTypeInput) update_fruit_types: list[FruitType] = mutations.update(FruitTypePartialInput) delete_fruit_types: list[FruitType] = mutations.delete() @pytest.fixture def mutation(db): if settings.GEOS_IMPORTED: from tests.types import GeoField, GeoFieldInput, GeoFieldPartialInput @strawberry.type class GeoMutation(Mutation): create_geo_field: GeoField = mutations.create(GeoFieldInput) update_geo_fields: list[GeoField] = mutations.update(GeoFieldPartialInput) mutation = GeoMutation else: mutation = Mutation return utils.generate_query(mutation=mutation) @pytest.fixture def fruit(db): return models.Fruit.objects.create(name="Strawberry") strawberry-graphql-django-0.62.0/tests/mutations/test_batch_mutations.py000066400000000000000000000046061502405145400266670ustar00rootroot00000000000000"""Tests for batched mutations. Batched mutations are mutations that mutate multiple objects at once. Mutations with a filter function or accept a list of objects that return a list. """ def test_batch_create(mutation, fruits): result = mutation( """ mutation { fruits: createFruits( data: [{ name: "banana" }, { name: "cherry" }] ) { id name } } """ ) assert not result.errors assert result.data["fruits"] == [ {"id": "4", "name": "banana"}, {"id": "5", "name": "cherry"}, ] def test_batch_delete_with_filter(mutation, fruits): result = mutation( """ mutation($ids: [ID!]) { fruits: deleteFruits( filters: {id: {inList: $ids}} ) { id name } } """, {"ids": ["2"]}, ) assert not result.errors assert result.data["fruits"] == [ {"id": "2", "name": "raspberry"}, ] def test_batch_delete_with_filter_empty_list(mutation, fruits): result = mutation( """ { fruits: deleteFruits( filters: {id: {inList: []}} ) { id name } } """ ) assert not result.errors def test_batch_update_with_filter(mutation, fruits): result = mutation( """ { fruits: updateFruits( data: { name: "orange" } filters: {id: {inList: [1]}} ) { id name } } """ ) assert not result.errors assert result.data["fruits"] == [ {"id": "1", "name": "orange"}, ] def test_batch_update_with_filter_empty_list(mutation, fruits): result = mutation( """ { fruits: updateFruits( data: { name: "orange" } filters: {id: {inList: []}} ) { id name } } """ ) assert not result.errors def test_batch_patch(mutation, fruits): result = mutation( """ { fruits: patchFruits( data: [{ id: 2, name: "orange" }] ) { id name } } """ ) assert not result.errors assert result.data["fruits"] == [ {"id": "2", "name": "orange"}, ] strawberry-graphql-django-0.62.0/tests/mutations/test_hooks.py000066400000000000000000000000001502405145400246060ustar00rootroot00000000000000strawberry-graphql-django-0.62.0/tests/mutations/test_mutations.py000066400000000000000000000357671502405145400255420ustar00rootroot00000000000000import io import pytest from django.conf import settings from django.core.files.uploadedfile import SimpleUploadedFile from PIL import Image from tests import models from tests.utils import deep_tuple_to_list def prep_image(fname): """Return an SimpleUploadedFile.""" img_f = io.BytesIO() img = Image.new(mode="RGB", size=(1, 1), color="red") img.save(img_f, format="jpeg") return SimpleUploadedFile(fname, img_f.getvalue()) def test_create(mutation): result = mutation( '{ fruit: createFruit(data: { name: "strawberry" }) { id name } }', ) assert not result.errors assert result.data["fruit"] == {"id": "1", "name": "strawberry"} assert list(models.Fruit.objects.values("id", "name")) == [ {"id": 1, "name": "strawberry"}, ] def test_create_with_optional_file(mutation): fname = "test_create_with_optional_file.png" upload = prep_image(fname) result = mutation( """\ CreateFruit($picture: Upload!) { createFruit(data: { name: "strawberry", picture: $picture }) { id name picture { name } } } """, variable_values={"picture": upload}, ) assert not result.errors assert result.data["createFruit"] == { "id": "1", "name": "strawberry", "picture": {"name": f".tmp_upload/{fname}"}, } def test_create_with_optional_file_when_not_setting_it(mutation): result = mutation( """\ CreateFruit($picture: Upload) { createFruit(data: { name: "strawberry", picture: $picture }) { id name picture { name } } } """, variable_values={"picture": None}, ) assert not result.errors assert result.data["createFruit"] == { "id": "1", "name": "strawberry", "picture": None, } def test_update_with_optional_file_when_unsetting_it(mutation): fname = "test_update_with_optional_file.png" upload = prep_image(fname) fruit = models.Fruit.objects.create(name="strawberry", picture=upload) result = mutation( """\ UpdateFruit($id: ID! $picture: Upload) { updateFruits( filters: { id: { exact: $id } } data: { picture: $picture } ) { id name picture { name } } } """, variable_values={"id": fruit.pk, "picture": None}, ) assert not result.errors assert result.data["updateFruits"] == [ { "id": str(fruit.pk), "name": "strawberry", "picture": None, } ] def test_with_required_file_fails(mutation): # The query input will not have the required field listed # as we want to test the failback of the django-model full_clean # method on the create to trigger validation errors. result = mutation( """\ createTomatoWithRequiredPicture { createTomatoWithRequiredPicture(data: {name: "strawberry"}) { id name picture { name } } } """, variable_values={}, ) assert result.errors is not None assert "'This field cannot be blank" in str(result.errors) @pytest.mark.asyncio @pytest.mark.django_db(transaction=True) async def test_create_async(mutation): result = await mutation( '{ fruit: createFruit(data: { name: "strawberry" }) { name } }', ) assert not result.errors assert result.data["fruit"] == {"name": "strawberry"} def test_create_many(mutation): result = mutation( '{ fruits: createFruits(data: [{ name: "strawberry" },' ' { name: "raspberry" }]) { id name } }', ) assert not result.errors assert result.data["fruits"] == [ {"id": "1", "name": "strawberry"}, {"id": "2", "name": "raspberry"}, ] assert list(models.Fruit.objects.values("id", "name")) == [ {"id": 1, "name": "strawberry"}, {"id": 2, "name": "raspberry"}, ] def test_update(mutation, fruits): result = mutation( '{ fruits: updateFruits(data: { name: "orange" }, filters: {}) { id name } }' ) assert not result.errors assert result.data["fruits"] == [ {"id": "1", "name": "orange"}, {"id": "2", "name": "orange"}, {"id": "3", "name": "orange"}, ] assert list(models.Fruit.objects.values("id", "name")) == [ {"id": 1, "name": "orange"}, {"id": 2, "name": "orange"}, {"id": 3, "name": "orange"}, ] def test_update_m2m_with_validation_error(mutation, fruit): result = mutation( '{ fruits: updateFruits(data: { types: [{ name: "rotten"} ] }, filters: {}) { id types {' " name } }}", ) assert result.errors assert result.errors[0].message == "{'name': ['We do not allow rotten fruits.']}" def test_update_m2m_with_new_different_objects(mutation, fruit): result = mutation( '{ fruits: updateFruits(data: { types: [{name: "apple"}, {name: "strawberry"}]}, filters: {}) { id types { id name }}}' ) assert not result.errors assert result.data["fruits"][0]["types"] == [ {"id": "1", "name": "apple"}, {"id": "2", "name": "strawberry"}, ] result = mutation( '{ fruits: updateFruits(data: { types: [{id: "1", name: "apple updated"}, {name: "raspberry"}]}, filters: {}) { id types { id name }}}' ) assert result.data["fruits"][0]["types"] == [ {"id": "1", "name": "apple updated"}, {"id": "3", "name": "raspberry"}, ] def test_update_m2m_with_duplicates(mutation, fruit): result = mutation( '{ fruits: updateFruits(data: { types: [{name: "apple"}, {name: "apple"}]}, filters: {}) { id types { id name }}}' ) assert not result.errors assert result.data["fruits"][0]["types"] == [ {"id": "1", "name": "apple"}, {"id": "2", "name": "apple"}, ] def test_update_lazy_object(mutation, fruit): result = mutation( '{ fruit: updateLazyFruit(data: { name: "orange" }) { id name } }', ) assert not result.errors assert result.data["fruit"] == {"id": "1", "name": "orange"} def test_update_with_filters(mutation, fruits): result = mutation( '{ fruits: updateFruits(data: { name: "orange" },' " filters: { id: { inList: [1, 2] } } ) { id name } }", ) assert not result.errors assert result.data["fruits"] == [ {"id": "1", "name": "orange"}, {"id": "2", "name": "orange"}, ] assert list(models.Fruit.objects.values("id", "name")) == [ {"id": 1, "name": "orange"}, {"id": 2, "name": "orange"}, {"id": 3, "name": "banana"}, ] def test_delete(mutation, fruits): result = mutation("{ fruits: deleteFruits(filters: {}) { id name } }") assert not result.errors assert result.data["fruits"] == [ {"id": "1", "name": "strawberry"}, {"id": "2", "name": "raspberry"}, {"id": "3", "name": "banana"}, ] assert list(models.Fruit.objects.values("id", "name")) == [] def test_delete_lazy_object(mutation, fruit): result = mutation("{ fruit: deleteLazyFruit { id name } }") assert not result.errors assert result.data["fruit"] == {"id": "1", "name": "Strawberry"} assert list(models.Fruit.objects.values("id", "name")) == [] def test_delete_with_filters(mutation, fruits): result = mutation( '{ fruits: deleteFruits(filters: { name: { contains: "berry" } }) { id' " name } }", ) assert not result.errors assert result.data["fruits"] == [ {"id": "1", "name": "strawberry"}, {"id": "2", "name": "raspberry"}, ] assert list(models.Fruit.objects.values("id", "name")) == [ {"id": 3, "name": "banana"}, ] @pytest.mark.skipif( not settings.GEOS_IMPORTED, reason="Test requires GEOS to be imported and properly configured", ) @pytest.mark.django_db(transaction=True) def test_create_geo(mutation): from tests.models import GeosFieldsModel # Test for point point = (0.0, 1.0) result = mutation( f"{{ geofield: createGeoField(data: {{ point: {list(point)} }} ) {{ id }} }}", ) assert not result.errors assert ( GeosFieldsModel.objects.get(id=result.data["geofield"]["id"]).point.tuple == point ) # Test for lineString line_string = ((0.0, 0.0), (1.0, 1.0)) result = mutation( f""" {{ geofield: createGeoField(data: {{ lineString: {deep_tuple_to_list(line_string)} }}) {{ id }} }} """, ) assert not result.errors assert ( GeosFieldsModel.objects.get(id=result.data["geofield"]["id"]).line_string.tuple == line_string ) # Test for polygon polygon = ( ((-1.0, -1.0), (-1.0, 1.0), (1.0, 1.0), (1.0, -1.0), (-1.0, -1.0)), ((-2.0, -2.0), (-2.0, 2.0), (2.0, 2.0), (2.0, -2.0), (-2.0, -2.0)), ) result = mutation( f""" {{ geofield: createGeoField(data: {{ polygon: {deep_tuple_to_list(polygon)} }}) {{ id }} }} """, ) assert not result.errors assert ( GeosFieldsModel.objects.get(id=result.data["geofield"]["id"]).polygon.tuple == polygon ) # Test for multi_point multi_point = ((0.0, 0.0), (-1.0, -1.0), (1.0, 1.0)) result = mutation( f""" {{ geofield: createGeoField(data: {{ multiPoint: {deep_tuple_to_list(multi_point)} }}) {{ id }} }} """, ) assert not result.errors assert ( GeosFieldsModel.objects.get(id=result.data["geofield"]["id"]).multi_point.tuple == multi_point ) # Test for multiLineString multi_line_string = ( ((0.0, 0.0), (1.0, 1.0)), ((1.0, 1.0), (-1.0, -1.0)), ((2.0, 2.0), (-2.0, -2.0)), ) result = mutation( f""" {{ geofield: createGeoField(data: {{ multiLineString: {deep_tuple_to_list(multi_line_string)} }}) {{ id }} }} """, ) assert not result.errors assert ( GeosFieldsModel.objects.get( id=result.data["geofield"]["id"], ).multi_line_string.tuple == multi_line_string ) # Test for multiPolygon multi_polygon = ( ( ((-1.0, -1.0), (-1.0, 1.0), (1.0, 1.0), (1.0, -1.0), (-1.0, -1.0)), ((-2.0, -2.0), (-2.0, 2.0), (2.0, 2.0), (2.0, -2.0), (-2.0, -2.0)), ), ( ((-1.0, -1.0), (-1.0, 1.0), (1.0, 1.0), (1.0, -1.0), (-1.0, -1.0)), ((-2.0, -2.0), (-2.0, 2.0), (2.0, 2.0), (2.0, -2.0), (-2.0, -2.0)), ), ) result = mutation( f""" {{ geofield: createGeoField(data: {{ multiPolygon: {deep_tuple_to_list(multi_polygon)} }}) {{ id }} }} """, ) assert not result.errors assert ( GeosFieldsModel.objects.get( id=result.data["geofield"]["id"], ).multi_polygon.tuple == multi_polygon ) @pytest.mark.skipif( not settings.GEOS_IMPORTED, reason="Test requires GEOS to be imported and properly configured", ) @pytest.mark.django_db(transaction=True) def test_update_geo(mutation): from tests.models import GeosFieldsModel geofield_obj = GeosFieldsModel.objects.create() assert geofield_obj.point is None assert geofield_obj.line_string is None assert geofield_obj.polygon is None assert geofield_obj.multi_point is None assert geofield_obj.multi_line_string is None assert geofield_obj.multi_polygon is None # Test for point point = [0.0, 1.0] result = mutation( f""" {{ geofield: updateGeoFields(data: {{ point: {point} }}) {{ id }} }} """, ) assert not result.errors geofield_obj.refresh_from_db() assert deep_tuple_to_list(geofield_obj.point.tuple) == point # Test for lineString line_string = [[0.0, 0.0], [1.0, 1.0]] result = mutation( f""" {{ geofield: updateGeoFields(data: {{ lineString: {line_string} }}) {{ id }} }} """, ) assert not result.errors geofield_obj.refresh_from_db() assert deep_tuple_to_list(geofield_obj.line_string.tuple) == line_string # Test for polygon polygon = [ [[-1.0, -1.0], [-1.0, 1.0], [1.0, 1.0], [1.0, -1.0], [-1.0, -1.0]], [[-2.0, -2.0], [-2.0, 2.0], [2.0, 2.0], [2.0, -2.0], [-2.0, -2.0]], ] result = mutation( f""" {{ geofield: updateGeoFields(data: {{ polygon: {polygon} }}) {{ id }} }} """, ) assert not result.errors geofield_obj.refresh_from_db() assert deep_tuple_to_list(geofield_obj.polygon.tuple) == polygon # Test for multi_point multi_point = [[0.0, 0.0], [-1.0, -1.0], [1.0, 1.0]] result = mutation( f""" {{ geofield: updateGeoFields(data: {{ multiPoint: {multi_point} }}) {{ id }} }} """, ) assert not result.errors geofield_obj.refresh_from_db() assert deep_tuple_to_list(geofield_obj.multi_point.tuple) == multi_point # Test for multiLineString multi_line_string = [ [[0.0, 0.0], [1.0, 1.0]], [[1.0, 1.0], [-1.0, -1.0]], [[2.0, 2.0], [-2.0, -2.0]], ] result = mutation( f""" {{ geofield: updateGeoFields(data: {{ multiLineString: {multi_line_string} }}) {{ id }} }} """, ) assert not result.errors geofield_obj.refresh_from_db() assert deep_tuple_to_list(geofield_obj.multi_line_string.tuple) == multi_line_string # Test for multiPolygon multi_polygon = [ [ [[-1.0, -1.0], [-1.0, 1.0], [1.0, 1.0], [1.0, -1.0], [-1.0, -1.0]], [[-2.0, -2.0], [-2.0, 2.0], [2.0, 2.0], [2.0, -2.0], [-2.0, -2.0]], ], [ [[-1.0, -1.0], [-1.0, 1.0], [1.0, 1.0], [1.0, -1.0], [-1.0, -1.0]], [[-2.0, -2.0], [-2.0, 2.0], [2.0, 2.0], [2.0, -2.0], [-2.0, -2.0]], ], ] result = mutation( f""" {{ geofield: updateGeoFields(data: {{ multiPolygon: {multi_polygon} }}) {{ id }} }} """, ) assert not result.errors geofield_obj.refresh_from_db() assert deep_tuple_to_list(geofield_obj.multi_polygon.tuple) == multi_polygon # Test everything not overwritten geofield_obj.refresh_from_db() assert deep_tuple_to_list(geofield_obj.point.tuple) == point assert deep_tuple_to_list(geofield_obj.line_string.tuple) == line_string assert deep_tuple_to_list(geofield_obj.polygon.tuple) == polygon assert deep_tuple_to_list(geofield_obj.multi_point.tuple) == multi_point assert deep_tuple_to_list(geofield_obj.multi_line_string.tuple) == multi_line_string assert deep_tuple_to_list(geofield_obj.multi_polygon.tuple) == multi_polygon strawberry-graphql-django-0.62.0/tests/mutations/test_partial_updates.py000066400000000000000000001024531502405145400266630ustar00rootroot00000000000000"""Tests the behaviour of partial input optional fields in mutations. This module tests Strawberry-Django's handling of partial input optional fields in mutations, specifically when their values are omitted or explicitly set to `null`, for different variations of model fields: * Required fields * Optional and nullable fields * Optional and non-nullable fields * Required foreign key fields * Optional foreign key fields * Many-to-many fields These tests stem from the fact that the GraphQL type-system doesn't distinguish between optional and nullable. That is, type `T!` is both required and non-nullable (i.e., must be supplied and cannot be `null`), but type `T` is both optional and nullable (i.e., can be omitted and can be `null`). """ import pytest import strawberry from django.test import override_settings from strawberry.relay import to_base64 import strawberry_django from strawberry_django.settings import strawberry_django_settings from tests.projects.faker import ( IssueFactory, MilestoneFactory, ProjectFactory, TagFactory, ) from tests.projects.models import Issue, Milestone, Project, Tag from tests.utils import generate_query @pytest.fixture def mutation(db): @strawberry_django.type(Issue) class IssueType: id: strawberry.auto name: strawberry.auto kind: strawberry.auto priority: strawberry.auto milestone: strawberry.auto tags: strawberry.auto @strawberry_django.partial(Issue) class IssueInputPartial: id: strawberry.auto name: strawberry.auto kind: strawberry.auto priority: strawberry.auto milestone: strawberry.auto tags: strawberry.auto @strawberry_django.type(Milestone) class MilestoneType: id: strawberry.auto project: strawberry.auto @strawberry_django.partial(Milestone) class MilestoneInputPartial: id: strawberry.auto project: strawberry.auto @strawberry_django.type(Project) class ProjectType: id: strawberry.auto @strawberry_django.type(Tag) class TagType: id: strawberry.auto @strawberry.type class Query: issue: IssueType milestone: MilestoneType project: ProjectType tag: TagType @strawberry.type class Mutation: update_issue: IssueType = strawberry_django.mutations.update( IssueInputPartial, handle_django_errors=True, ) update_milestone: MilestoneType = strawberry_django.mutations.update( MilestoneInputPartial, handle_django_errors=True, ) return generate_query(query=Query, mutation=Mutation) def test_field_required(mutation): """Tests behaviour for a required model field.""" query = """mutation UpdateIssueName($id: ID!, $name: String) { updateIssue(data: { id: $id, name: $name }) { ...on IssueType { name } ... on OperationInfo { messages { kind code message field } } } } """ # Create an issue issue_name = "Original name" issue = IssueFactory.create(name=issue_name) # Update the issue, omitting the `name` field # We expect the mutation to succeed and the name to remain unchanged result = mutation(query, {"id": issue.pk}) assert result.errors is None assert result.data == {"updateIssue": {"name": issue_name}} issue.refresh_from_db() assert issue.name == issue_name # Update the issue, explicitly providing `null` for the `name` field # We expect the mutation to fail and the name to remain unchanged # Note that this failure occurs at the model level, not the GraphQL level result = mutation(query, {"id": issue.pk, "name": None}) assert result.errors is None assert result.data == { "updateIssue": { "messages": [ { "kind": "VALIDATION", "code": "null", "message": "This field cannot be null.", "field": "name", } ] } } issue.refresh_from_db() assert issue.name == issue_name def test_field_optional_and_non_nullable(mutation): """Tests behaviour for an optional & non-nullable model field.""" query = """mutation UpdateIssuePriority($id: ID!, $priority: Int) { updateIssue(data: { id: $id, priority: $priority }) { ...on IssueType { priority } ... on OperationInfo { messages { kind code message field } } } } """ # Create an issue issue_priority = 42 issue = IssueFactory.create(priority=issue_priority) # Update the issue, omitting the `priority` field # We expect the mutation to succeed and the priority to remain unchanged result = mutation(query, {"id": issue.pk}) assert result.errors is None assert result.data == {"updateIssue": {"priority": issue_priority}} issue.refresh_from_db() assert issue.priority == issue_priority # Update the issue, explicitly providing `null` for the `priority` field # We expect the mutation to fail and the priority to remain unchanged # Note that this failure occurs at the model level, not the GraphQL level result = mutation(query, {"id": issue.pk, "priority": None}) assert result.errors is None assert result.data == { "updateIssue": { "messages": [ { "kind": "VALIDATION", "code": "null", "message": "This field cannot be null.", "field": "priority", } ] } } issue.refresh_from_db() assert issue.priority == issue_priority def test_field_optional_and_nullable(mutation): """Tests behaviour for an optional & nullable model field.""" query = """mutation UpdateIssueKind($id: ID!, $kind: String) { updateIssue(data: { id: $id, kind: $kind }) { ...on IssueType { kind } ... on OperationInfo { messages { kind code message field } } } } """ # Create an issue issue_kind = Issue.Kind.FEATURE.value issue = IssueFactory.create(kind=issue_kind) # Update the issue, omitting the `kind` field # We expect the mutation to succeed and the kind to remain unchanged result = mutation(query, {"id": issue.pk}) assert result.errors is None assert result.data == {"updateIssue": {"kind": issue_kind}} issue.refresh_from_db() assert issue.kind == issue_kind # Update the issue, explicitly providing `null` for the `kind` field # We expect the mutation to succeed and the kind to be set to `None` result = mutation(query, {"id": issue.pk, "kind": None}) assert result.errors is None assert result.data == {"updateIssue": {"kind": None}} issue.refresh_from_db() assert issue.kind is None def test_foreign_key_required(mutation): """Tests behaviour for a required foreign key field.""" query = """mutation UpdateMilestoneProject($id: ID!, $project: OneToManyInput) { updateMilestone(data: { id: $id, project: $project }) { ...on MilestoneType { project { pk } } ... on OperationInfo { messages { kind code message field } } } } """ # Create a milestone project = ProjectFactory.create() milestone = MilestoneFactory.create(project=project) # Update the milestone, omitting the `project` field # We expect the mutation to succeed and the project to remain unchanged result = mutation(query, {"id": milestone.pk}) assert result.errors is None assert result.data == {"updateMilestone": {"project": {"pk": str(project.pk)}}} milestone.refresh_from_db() assert milestone.project == project # Update the milestone, explicitly providing `null` for the `project` field # We expect the mutation to fail and the project to remain unchanged # Note that this failure occurs at the model level, not the GraphQL level result = mutation(query, {"id": milestone.pk, "project": None}) assert result.errors is None assert result.data == { "updateMilestone": { "messages": [ { "kind": "VALIDATION", "code": "null", "message": "This field cannot be null.", "field": "project", } ] } } milestone.refresh_from_db() assert milestone.project == project def test_foreign_key_optional(mutation): """Tests behaviour for an optional foreign key field.""" query = """mutation UpdateIssueMilestone($id: ID!, $milestone: OneToManyInput) { updateIssue(data: { id: $id, milestone: $milestone }) { ...on IssueType { milestone { pk } } ... on OperationInfo { messages { kind code message field } } } } """ # Create an issue milestone = MilestoneFactory.create() issue = IssueFactory.create(milestone=milestone) # Update the issue, omitting the `milestone` field # We expect the mutation to succeed and the milestone to remain unchanged result = mutation(query, {"id": issue.pk}) assert result.errors is None assert result.data == {"updateIssue": {"milestone": {"pk": str(milestone.pk)}}} issue.refresh_from_db() assert issue.milestone == milestone # Update the issue, explicitly providing `null` for the `milestone` field # We expect the mutation to succeed and the milestone to be set to `None` result = mutation(query, {"id": issue.pk, "milestone": None}) assert result.errors is None assert result.data == {"updateIssue": {"milestone": None}} issue.refresh_from_db() assert issue.milestone is None def test_many_to_many(mutation): """Tests behaviour for a many to many field.""" query = """mutation UpdateIssueTags($id: ID!, $tags: ManyToManyInput) { updateIssue(data: { id: $id, tags: $tags }) { ...on IssueType { tags { pk } } ... on OperationInfo { messages { kind code message field } } } } """ # Create an issue tags = TagFactory.create_batch(3) issue = IssueFactory.create() issue.tags.set(tags) # Update the issue, omitting the `tags` field # We expect the mutation to succeed and the tags to remain unchanged result = mutation(query, {"id": issue.pk}) assert result.errors is None assert result.data == { "updateIssue": {"tags": [{"pk": str(tag.pk)} for tag in tags]} } issue.refresh_from_db() assert list(issue.tags.all()) == tags # Update the issue, explicitly providing `null` for the `tags` field # We expect the mutation to succeed, but the tags to remain unchanged result = mutation(query, {"id": issue.pk, "tags": None}) assert result.errors is None assert result.data == { "updateIssue": {"tags": [{"pk": str(tag.pk)} for tag in tags]} } issue.refresh_from_db() assert list(issue.tags.all()) == tags def test_many_to_many_set(mutation): """Tests behaviour for `set` on a many to many field.""" query = """mutation SetIssueTags($id: ID!, $setTags: [ID!]) { updateIssue(data: { id: $id, tags: { set: $setTags } }) { ...on IssueType { tags { pk } } ... on OperationInfo { messages { kind code message field } } } } """ # Create an issue tags = TagFactory.create_batch(3) issue = IssueFactory.create() issue.tags.set(tags) # Update the issue, omitting the `setTags` field # We expect the mutation to succeed and the tags to remain unchanged result = mutation(query, {"id": issue.pk}) assert result.errors is None assert result.data == { "updateIssue": {"tags": [{"pk": str(tag.pk)} for tag in tags]} } issue.refresh_from_db() assert list(issue.tags.all()) == tags # Update the issue, explicitly providing `null` for the `setTags` field # We expect the mutation to succeed, but the tags to remain unchanged result = mutation(query, {"id": issue.pk, "setTags": None}) assert result.errors is None assert result.data == { "updateIssue": {"tags": [{"pk": str(tag.pk)} for tag in tags]} } issue.refresh_from_db() assert list(issue.tags.all()) == tags # Update the issue, explicitly providing an empty list for the `setTags` field # We expect the mutation to succeed, and the tags to be cleared result = mutation(query, {"id": issue.pk, "setTags": []}) assert result.errors is None assert result.data == {"updateIssue": {"tags": []}} issue.refresh_from_db() assert list(issue.tags.all()) == [] def test_many_to_many_add(mutation): """Tests behaviour for `add` on a many to many field.""" query = """mutation AddIssueTags($id: ID!, $addTags: [ID!]) { updateIssue(data: { id: $id, tags: { add: $addTags } }) { ...on IssueType { tags { pk } } ... on OperationInfo { messages { kind code message field } } } } """ # Create an issue tags = TagFactory.create_batch(3) issue = IssueFactory.create() issue.tags.set(tags) # Update the issue, omitting the `addTags` field # We expect the mutation to succeed and the tags to remain unchanged result = mutation(query, {"id": issue.pk}) assert result.errors is None assert result.data == { "updateIssue": {"tags": [{"pk": str(tag.pk)} for tag in tags]} } issue.refresh_from_db() assert list(issue.tags.all()) == tags # Update the issue, explicitly providing `null` for the `addTags` field # We expect the mutation to succeed, but the tags to remain unchanged result = mutation(query, {"id": issue.pk, "addTags": None}) assert result.errors is None assert result.data == { "updateIssue": {"tags": [{"pk": str(tag.pk)} for tag in tags]} } issue.refresh_from_db() assert list(issue.tags.all()) == tags # Update the issue, explicitly providing an empty list for the `addTags` field # We expect the mutation to succeed, but the tags to remain unchanged result = mutation(query, {"id": issue.pk, "addTags": []}) assert result.errors is None assert result.data == { "updateIssue": {"tags": [{"pk": str(tag.pk)} for tag in tags]} } issue.refresh_from_db() assert list(issue.tags.all()) == tags def test_many_to_many_remove(mutation): """Tests behaviour for `remove` on a many to many field.""" query = """mutation RemoveIssueTags($id: ID!, $removeTags: [ID!]) { updateIssue(data: { id: $id, tags: { remove: $removeTags } }) { ...on IssueType { tags { pk } } ... on OperationInfo { messages { kind code message field } } } } """ # Create an issue tags = TagFactory.create_batch(3) issue = IssueFactory.create() issue.tags.set(tags) # Update the issue, omitting the `removeTags` field # We expect the mutation to succeed and the tags to remain unchanged result = mutation(query, {"id": issue.pk}) assert result.errors is None assert result.data == { "updateIssue": {"tags": [{"pk": str(tag.pk)} for tag in tags]} } issue.refresh_from_db() assert list(issue.tags.all()) == tags # Update the issue, explicitly providing `null` for the `removeTags` field # We expect the mutation to succeed, but the tags to remain unchanged result = mutation(query, {"id": issue.pk, "removeTags": None}) assert result.errors is None assert result.data == { "updateIssue": {"tags": [{"pk": str(tag.pk)} for tag in tags]} } issue.refresh_from_db() assert list(issue.tags.all()) == tags # Update the issue, explicitly providing an empty list for the `removeTags` field # We expect the mutation to succeed, but the tags to remain unchanged result = mutation(query, {"id": issue.pk, "removeTags": []}) assert result.errors is None assert result.data == { "updateIssue": {"tags": [{"pk": str(tag.pk)} for tag in tags]} } issue.refresh_from_db() assert list(issue.tags.all()) == tags @pytest.fixture @override_settings( STRAWBERRY_DJANGO={ **strawberry_django_settings(), "MAP_AUTO_ID_AS_GLOBAL_ID": True, }, ) def relay_mutation(db): @strawberry_django.type(Issue) class IssueType(strawberry.relay.Node): name: strawberry.auto kind: strawberry.auto priority: strawberry.auto milestone: strawberry.auto tags: strawberry.auto @strawberry_django.partial(Issue) class IssueInputPartial(strawberry_django.NodeInput): name: strawberry.auto kind: strawberry.auto priority: strawberry.auto milestone: strawberry.auto tags: strawberry.auto @strawberry_django.type(Milestone) class MilestoneType(strawberry.relay.Node): project: strawberry.auto @strawberry_django.partial(Milestone) class MilestoneInputPartial(strawberry_django.NodeInput): project: strawberry.auto @strawberry_django.type(Project) class ProjectType(strawberry.relay.Node): pass @strawberry_django.type(Tag) class TagType(strawberry.relay.Node): pass @strawberry.type class Query: issue: IssueType milestone: MilestoneType project: ProjectType tag: TagType @strawberry.type class Mutation: update_issue: IssueType = strawberry_django.mutations.update( IssueInputPartial, handle_django_errors=True, ) update_milestone: MilestoneType = strawberry_django.mutations.update( MilestoneInputPartial, handle_django_errors=True, ) return generate_query(query=Query, mutation=Mutation) def test_relay_field_required(relay_mutation): """Tests Relay behaviour for a required model field.""" query = """mutation UpdateIssueName($id: ID!, $name: String) { updateIssue(data: { id: $id, name: $name }) { ...on IssueType { name } ... on OperationInfo { messages { kind code message field } } } } """ # Create an issue issue_name = "Original name" issue = IssueFactory.create(name=issue_name) issue_id = to_base64("IssueType", issue.pk) # Update the issue, omitting the `name` field # We expect the mutation to succeed and the name to remain unchanged result = relay_mutation(query, {"id": issue_id}) assert result.errors is None assert result.data == {"updateIssue": {"name": issue_name}} issue.refresh_from_db() assert issue.name == issue_name # Update the issue, explicitly providing `null` for the `name` field # We expect the mutation to fail and the name to remain unchanged # Note that this failure occurs at the model level, not the GraphQL level result = relay_mutation(query, {"id": issue_id, "name": None}) assert result.errors is None assert result.data == { "updateIssue": { "messages": [ { "kind": "VALIDATION", "code": "null", "message": "This field cannot be null.", "field": "name", } ] } } issue.refresh_from_db() assert issue.name == issue_name def test_relay_field_optional_and_non_nullable(relay_mutation): """Tests Relay behaviour for an optional & non-nullable model field.""" query = """mutation UpdateIssuePriority($id: ID!, $priority: Int) { updateIssue(data: { id: $id, priority: $priority }) { ...on IssueType { priority } ... on OperationInfo { messages { kind code message field } } } } """ # Create an issue issue_priority = 42 issue = IssueFactory.create(priority=issue_priority) issue_id = to_base64("IssueType", issue.pk) # Update the issue, omitting the `priority` field # We expect the mutation to succeed and the priority to remain unchanged result = relay_mutation(query, {"id": issue_id}) assert result.errors is None assert result.data == {"updateIssue": {"priority": issue_priority}} issue.refresh_from_db() assert issue.priority == issue_priority # Update the issue, explicitly providing `null` for the `priority` field # We expect the mutation to fail and the priority to remain unchanged # Note that this failure occurs at the model level, not the GraphQL level result = relay_mutation(query, {"id": issue_id, "priority": None}) assert result.errors is None assert result.data == { "updateIssue": { "messages": [ { "kind": "VALIDATION", "code": "null", "message": "This field cannot be null.", "field": "priority", } ] } } issue.refresh_from_db() assert issue.priority == issue_priority def test_relay_field_optional_and_nullable(relay_mutation): """Tests Relay behaviour for an optional & nullable model field.""" query = """mutation UpdateIssueKind($id: ID!, $kind: String) { updateIssue(data: { id: $id, kind: $kind }) { ...on IssueType { kind } ... on OperationInfo { messages { kind code message field } } } } """ # Create an issue issue_kind = Issue.Kind.FEATURE.value issue = IssueFactory.create(kind=issue_kind) issue_id = to_base64("IssueType", issue.pk) # Update the issue, omitting the `kind` field # We expect the mutation to succeed and the kind to remain unchanged result = relay_mutation(query, {"id": issue_id}) assert result.errors is None assert result.data == {"updateIssue": {"kind": issue_kind}} issue.refresh_from_db() assert issue.kind == issue_kind # Update the issue, explicitly providing `null` for the `kind` field # We expect the mutation to succeed and the kind to be set to `None` result = relay_mutation(query, {"id": issue_id, "kind": None}) assert result.errors is None assert result.data == {"updateIssue": {"kind": None}} issue.refresh_from_db() assert issue.kind is None def test_relay_foreign_key_required(relay_mutation): """Tests Relay behaviour for a required foreign key field.""" query = """mutation UpdateMilestoneProject($id: ID!, $project: NodeInput) { updateMilestone(data: { id: $id, project: $project }) { ...on MilestoneType { project { id } } ... on OperationInfo { messages { kind code message field } } } } """ # Create a milestone project = ProjectFactory.create() project_id = to_base64("ProjectType", project.pk) milestone = MilestoneFactory.create(project=project) milestone_id = to_base64("MilestoneType", milestone.pk) # Update the milestone, omitting the `project` field # We expect the mutation to succeed and the project to remain unchanged result = relay_mutation(query, {"id": milestone_id}) assert result.errors is None assert result.data == {"updateMilestone": {"project": {"id": project_id}}} milestone.refresh_from_db() assert milestone.project == project # Update the milestone, explicitly providing `null` for the `project` field # We expect the mutation to fail and the project to remain unchanged # Note that this failure occurs at the model level, not the GraphQL level result = relay_mutation(query, {"id": milestone_id, "project": None}) assert result.errors is None assert result.data == { "updateMilestone": { "messages": [ { "kind": "VALIDATION", "code": "null", "message": "This field cannot be null.", "field": "project", } ] } } milestone.refresh_from_db() assert milestone.project == project def test_relay_foreign_key_optional(relay_mutation): """Tests Relay behaviour for an optional foreign key field.""" query = """mutation UpdateIssueMilestone($id: ID!, $milestone: NodeInput) { updateIssue(data: { id: $id, milestone: $milestone }) { ...on IssueType { milestone { id } } ... on OperationInfo { messages { kind code message field } } } } """ # Create an issue milestone = MilestoneFactory.create() milestone_id = to_base64("MilestoneType", milestone.pk) issue = IssueFactory.create(milestone=milestone) issue_id = to_base64("IssueType", issue.pk) # Update the issue, omitting the `milestone` field # We expect the mutation to succeed and the milestone to remain unchanged result = relay_mutation(query, {"id": issue_id}) assert result.errors is None assert result.data == {"updateIssue": {"milestone": {"id": milestone_id}}} issue.refresh_from_db() assert issue.milestone == milestone # Update the issue, explicitly providing `null` for the `milestone` field # We expect the mutation to succeed and the milestone to be set to `None` result = relay_mutation(query, {"id": issue_id, "milestone": None}) assert result.errors is None assert result.data == {"updateIssue": {"milestone": None}} issue.refresh_from_db() assert issue.milestone is None def test_relay_many_to_many(relay_mutation): """Tests Relay behaviour for a many to many field.""" query = """mutation UpdateIssueTags($id: ID!, $tags: NodeInputListInput) { updateIssue(data: { id: $id, tags: $tags }) { ...on IssueType { tags { id } } ... on OperationInfo { messages { kind code message field } } } } """ # Create an issue tags = TagFactory.create_batch(3) tag_ids = [to_base64("TagType", tag.pk) for tag in tags] issue = IssueFactory.create() issue.tags.set(tags) issue_id = to_base64("IssueType", issue.pk) # Update the issue, omitting the `tags` field # We expect the mutation to succeed and the tags to remain unchanged result = relay_mutation(query, {"id": issue_id}) assert result.errors is None assert result.data == { "updateIssue": {"tags": [{"id": tag_id} for tag_id in tag_ids]} } issue.refresh_from_db() assert list(issue.tags.all()) == tags # Update the issue, explicitly providing `null` for the `tags` field # We expect the mutation to succeed, but the tags to remain unchanged result = relay_mutation(query, {"id": issue_id, "tags": None}) assert result.errors is None assert result.data == { "updateIssue": {"tags": [{"id": tag_id} for tag_id in tag_ids]} } issue.refresh_from_db() assert list(issue.tags.all()) == tags def test_relay_many_to_many_set(relay_mutation): """Tests Relay behaviour for `set` on a many to many field.""" query = """mutation SetIssueTags($id: ID!, $setTags: [NodeInput!]) { updateIssue(data: { id: $id, tags: { set: $setTags } }) { ...on IssueType { tags { id } } ... on OperationInfo { messages { kind code message field } } } } """ # Create an issue tags = TagFactory.create_batch(3) tag_ids = [to_base64("TagType", tag.pk) for tag in tags] issue = IssueFactory.create() issue.tags.set(tags) issue_id = to_base64("IssueType", issue.pk) # Update the issue, omitting the `setTags` field # We expect the mutation to succeed and the tags to remain unchanged result = relay_mutation(query, {"id": issue_id}) assert result.errors is None assert result.data == { "updateIssue": {"tags": [{"id": tag_id} for tag_id in tag_ids]} } issue.refresh_from_db() assert list(issue.tags.all()) == tags # Update the issue, explicitly providing `null` for the `setTags` field # We expect the mutation to succeed, but the tags to remain unchanged result = relay_mutation(query, {"id": issue_id, "setTags": None}) assert result.errors is None assert result.data == { "updateIssue": {"tags": [{"id": tag_id} for tag_id in tag_ids]} } issue.refresh_from_db() assert list(issue.tags.all()) == tags # Update the issue, explicitly providing an empty list for the `setTags` field # We expect the mutation to succeed, and the tags to be cleared result = relay_mutation(query, {"id": issue_id, "setTags": []}) assert result.errors is None assert result.data == {"updateIssue": {"tags": []}} issue.refresh_from_db() assert list(issue.tags.all()) == [] def test_relay_many_to_many_add(relay_mutation): """Tests Relay behaviour for `add` on a many to many field.""" query = """mutation AddIssueTags($id: ID!, $addTags: [NodeInput!]) { updateIssue(data: { id: $id, tags: { add: $addTags } }) { ...on IssueType { tags { id } } ... on OperationInfo { messages { kind code message field } } } } """ # Create an issue tags = TagFactory.create_batch(3) tag_ids = [to_base64("TagType", tag.pk) for tag in tags] issue = IssueFactory.create() issue.tags.set(tags) issue_id = to_base64("IssueType", issue.pk) # Update the issue, omitting the `addTags` field # We expect the mutation to succeed and the tags to remain unchanged result = relay_mutation(query, {"id": issue_id}) assert result.errors is None assert result.data == { "updateIssue": {"tags": [{"id": tag_id} for tag_id in tag_ids]} } issue.refresh_from_db() assert list(issue.tags.all()) == tags # Update the issue, explicitly providing `null` for the `addTags` field # We expect the mutation to succeed, but the tags to remain unchanged result = relay_mutation(query, {"id": issue_id, "addTags": None}) assert result.errors is None assert result.data == { "updateIssue": {"tags": [{"id": tag_id} for tag_id in tag_ids]} } issue.refresh_from_db() assert list(issue.tags.all()) == tags # Update the issue, explicitly providing an empty list for the `addTags` field # We expect the mutation to succeed, but the tags to remain unchanged result = relay_mutation(query, {"id": issue_id, "addTags": []}) assert result.errors is None assert result.data == { "updateIssue": {"tags": [{"id": tag_id} for tag_id in tag_ids]} } issue.refresh_from_db() assert list(issue.tags.all()) == tags def test_relay_many_to_many_remove(relay_mutation): """Tests Relay behaviour for `remove` on a many to many field.""" query = """mutation RemoveIssueTags($id: ID!, $removeTags: [NodeInput!]) { updateIssue(data: { id: $id, tags: { remove: $removeTags } }) { ...on IssueType { tags { id } } ... on OperationInfo { messages { kind code message field } } } } """ # Create an issue tags = TagFactory.create_batch(3) tag_ids = [to_base64("TagType", tag.pk) for tag in tags] issue = IssueFactory.create() issue.tags.set(tags) issue_id = to_base64("IssueType", issue.pk) # Update the issue, omitting the `removeTags` field # We expect the mutation to succeed and the tags to remain unchanged result = relay_mutation(query, {"id": issue_id}) assert result.errors is None assert result.data == { "updateIssue": {"tags": [{"id": tag_id} for tag_id in tag_ids]} } issue.refresh_from_db() assert list(issue.tags.all()) == tags # Update the issue, explicitly providing `null` for the `removeTags` field # We expect the mutation to succeed, but the tags to remain unchanged result = relay_mutation(query, {"id": issue_id, "removeTags": None}) assert result.errors is None assert result.data == { "updateIssue": {"tags": [{"id": tag_id} for tag_id in tag_ids]} } issue.refresh_from_db() assert list(issue.tags.all()) == tags # Update the issue, explicitly providing an empty list for the `removeTags` field # We expect the mutation to succeed, but the tags to remain unchanged result = relay_mutation(query, {"id": issue_id, "removeTags": []}) assert result.errors is None assert result.data == { "updateIssue": {"tags": [{"id": tag_id} for tag_id in tag_ids]} } issue.refresh_from_db() assert list(issue.tags.all()) == tags strawberry-graphql-django-0.62.0/tests/mutations/test_permission_classes.py000066400000000000000000000024111502405145400274000ustar00rootroot00000000000000import pytest import strawberry from strawberry.permission import BasePermission from strawberry_django import mutations from tests import utils from tests.types import Fruit, FruitInput, FruitPartialInput class PermissionClass(BasePermission): message = "Permission Denied" def has_permission(self, source, info, **kwargs): return False @strawberry.type class Mutation: create_fruits: list[Fruit] = mutations.create( FruitInput, permission_classes=[PermissionClass], ) update_fruits: list[Fruit] = mutations.update( FruitPartialInput, permission_classes=[PermissionClass], ) delete_fruits: list[Fruit] = mutations.delete(permission_classes=[PermissionClass]) @pytest.fixture def mutation(db): return utils.generate_query(mutation=Mutation) def test_create(mutation): result = mutation('{ createFruits(data: { name: "strawberry" }) { id name } }') assert "Permission Denied" in str(result.errors) def test_update(mutation): result = mutation('{ updateFruits(data: { name: "strawberry" }) { id name } }') assert "Permission Denied" in str(result.errors) def test_delete(mutation): result = mutation("{ deleteFruits { id name } }") assert "Permission Denied" in str(result.errors) strawberry-graphql-django-0.62.0/tests/mutations/test_relationship.py000066400000000000000000000074141502405145400262040ustar00rootroot00000000000000"""Test the functionality of CUD relationships. Foreign key relationships in a GraphQL API context. It includes tests for one-to-many, many-to-one, and many-to-many relationships. """ import pytest from tests import models @pytest.fixture def color(db): return models.Color.objects.create(name="red") @pytest.fixture def fruit_type(db): return models.FruitType.objects.create(name="Berries") def test_create_one_to_many(mutation, color): result = mutation( '{ fruit: createFruit(data: { name: "strawberry",' " color: { set: 1 } }) { color { name } } }", ) assert not result.errors assert result.data["fruit"] == {"color": {"name": color.name}} def test_update_one_to_many(mutation, fruit, color): result = mutation( "{ fruits: updateFruits(data: { color: { set: 1 } }, filters: {}) { color { name } } }", ) assert not result.errors assert result.data["fruits"] == [{"color": {"name": color.name}}] result = mutation( "{ fruits: updateFruits(data: { color: { set: null } }, filters: {}) { color { name } } }", ) assert not result.errors assert result.data["fruits"] == [{"color": None}] def test_patch_one_to_many(mutation, fruit, color, django_assert_max_num_queries): # Issue 487: At maximum, 11 queries are expected to be executed: # 6x SAVEPOINT, 4x SELECT, 1x UPDATE with django_assert_max_num_queries(12): result = mutation( '{ fruits: updateFruits(filters: { id: { exact: "1"} }, ' "data: { color: { set: 1 } }) { color { name } } }", ) assert not result.errors assert result.data["fruits"] == [{"color": {"name": color.name}}] def test_update_many_to_one(mutation, fruit, color): result = mutation( "{ colors: updateColors(data: { fruits: { add: [1] } }) { fruits { name } } }", ) assert not result.errors assert result.data["colors"] == [{"fruits": [{"name": fruit.name}]}] result = mutation( "{ colors: updateColors(data: { fruits: { remove: [1] } }) { fruits { name" " } } }", ) assert not result.errors assert result.data["colors"] == [{"fruits": []}] result = mutation( "{ colors: updateColors(data: { fruits: { set: [1] } }) { fruits { name } } }", ) assert not result.errors assert result.data["colors"] == [{"fruits": [{"name": fruit.name}]}] result = mutation( "{ colors: updateColors(data: { fruits: { set: [] } }) { fruits { name } } }", ) assert not result.errors assert result.data["colors"] == [{"fruits": []}] def test_create_many_to_many(mutation, fruit): result = mutation( '{ types: createFruitType(data: { name: "Berries",' " fruits: { set: [1] } }) { fruits { name } } }", ) assert not result.errors assert result.data["types"] == {"fruits": [{"name": fruit.name}]} def test_update_many_to_many(mutation, fruit, fruit_type): result = mutation( "{ types: updateFruitTypes(data: { fruits: { add: [1] } }) { fruits { name" " } } }", ) assert not result.errors assert result.data["types"] == [{"fruits": [{"name": fruit.name}]}] result = mutation( "{ types: updateFruitTypes(data: { fruits: { remove: [1] } })" " { fruits { name } } }", ) assert not result.errors assert result.data["types"] == [{"fruits": []}] result = mutation( "{ types: updateFruitTypes(data: { fruits: { set: [1] } }) { fruits { name" " } } }", ) assert not result.errors assert result.data["types"] == [{"fruits": [{"name": fruit.name}]}] result = mutation( "{ types: updateFruitTypes(data: { fruits: { set: [] } }) { fruits { name" " } } }", ) assert not result.errors assert result.data["types"] == [{"fruits": []}] strawberry-graphql-django-0.62.0/tests/node_polymorphism/000077500000000000000000000000001502405145400236105ustar00rootroot00000000000000strawberry-graphql-django-0.62.0/tests/node_polymorphism/__init__.py000066400000000000000000000000001502405145400257070ustar00rootroot00000000000000strawberry-graphql-django-0.62.0/tests/node_polymorphism/models.py000066400000000000000000000004711502405145400254470ustar00rootroot00000000000000from django.db import models from polymorphic.models import PolymorphicModel class Project(PolymorphicModel): topic = models.CharField(max_length=30) class ArtProject(Project): artist = models.CharField(max_length=30) class ResearchProject(Project): supervisor = models.CharField(max_length=30) strawberry-graphql-django-0.62.0/tests/node_polymorphism/schema.py000066400000000000000000000014201502405145400254170ustar00rootroot00000000000000import strawberry from strawberry.relay import ListConnection import strawberry_django from strawberry_django.optimizer import DjangoOptimizerExtension from .models import ArtProject, Project, ResearchProject @strawberry_django.interface(Project) class ProjectType(strawberry.relay.Node): topic: strawberry.auto @strawberry_django.type(ArtProject) class ArtProjectType(ProjectType): artist: strawberry.auto @strawberry_django.type(ResearchProject) class ResearchProjectType(ProjectType): supervisor: strawberry.auto @strawberry.type class Query: projects: ListConnection[ProjectType] = strawberry_django.connection() schema = strawberry.Schema( query=Query, types=[ArtProjectType, ResearchProjectType], extensions=[DjangoOptimizerExtension], ) strawberry-graphql-django-0.62.0/tests/node_polymorphism/test_optimizer.py000066400000000000000000000026221502405145400272450ustar00rootroot00000000000000import pytest from tests.utils import assert_num_queries from .models import ArtProject, ResearchProject from .schema import schema @pytest.mark.django_db(transaction=True) def test_polymorphic_interface_query(): ap = ArtProject.objects.create(topic="Art", artist="Artist") rp = ResearchProject.objects.create(topic="Research", supervisor="Supervisor") query = """\ query { projects { edges { node { __typename topic ... on ArtProjectType { artist } ... on ResearchProjectType { supervisor } } } } } """ # ContentType, base table, two subtables = 4 queries with assert_num_queries(4): result = schema.execute_sync(query) assert not result.errors assert result.data == { "projects": { "edges": [ { "node": { "__typename": "ArtProjectType", "topic": ap.topic, "artist": ap.artist, } }, { "node": { "__typename": "ResearchProjectType", "topic": rp.topic, "supervisor": rp.supervisor, } }, ] } } strawberry-graphql-django-0.62.0/tests/polymorphism/000077500000000000000000000000001502405145400226035ustar00rootroot00000000000000strawberry-graphql-django-0.62.0/tests/polymorphism/__init__.py000066400000000000000000000000001502405145400247020ustar00rootroot00000000000000strawberry-graphql-django-0.62.0/tests/polymorphism/models.py000066400000000000000000000030501502405145400244360ustar00rootroot00000000000000from django.db import models from polymorphic.models import PolymorphicModel from strawberry_django.descriptors import model_property class Company(models.Model): name = models.CharField(max_length=100) main_project = models.ForeignKey("Project", on_delete=models.CASCADE, null=True) class Meta: ordering = ("name",) class Project(PolymorphicModel): company = models.ForeignKey( Company, null=True, blank=True, on_delete=models.CASCADE, related_name="projects", ) topic = models.CharField(max_length=30) class ArtProject(Project): artist = models.CharField(max_length=30) art_style = models.CharField(max_length=30) @model_property(only=("art_style",)) def art_style_upper(self) -> str: return self.art_style.upper() class ResearchProject(Project): supervisor = models.CharField(max_length=30) research_notes = models.TextField() class TechnicalProject(Project): timeline = models.CharField(max_length=30) class Meta: # pyright: ignore [reportIncompatibleVariableOverride] abstract = True class SoftwareProject(TechnicalProject): repository = models.CharField(max_length=255) class EngineeringProject(TechnicalProject): lead_engineer = models.CharField(max_length=255) class AppProject(TechnicalProject): repository = models.CharField(max_length=255) class AndroidProject(AppProject): android_version = models.CharField(max_length=15) class IOSProject(AppProject): ios_version = models.CharField(max_length=15) strawberry-graphql-django-0.62.0/tests/polymorphism/schema.py000066400000000000000000000047201502405145400244200ustar00rootroot00000000000000from typing import Optional import strawberry import strawberry_django from strawberry_django.optimizer import DjangoOptimizerExtension from strawberry_django.pagination import OffsetPaginated from .models import ( AndroidProject, AppProject, ArtProject, Company, EngineeringProject, IOSProject, Project, ResearchProject, SoftwareProject, TechnicalProject, ) @strawberry_django.interface(Project) class ProjectType: topic: strawberry.auto @strawberry_django.field(only=("topic",)) def topic_upper(self) -> str: return self.topic.upper() @strawberry_django.type(ArtProject) class ArtProjectType(ProjectType): artist: strawberry.auto art_style_upper: strawberry.auto @strawberry_django.field(only=("artist",)) def artist_upper(self) -> str: return self.artist.upper() @strawberry_django.type(ResearchProject) class ResearchProjectType(ProjectType): supervisor: strawberry.auto @strawberry_django.interface(TechnicalProject) class TechnicalProjectType(ProjectType): timeline: strawberry.auto @strawberry_django.type(SoftwareProject) class SoftwareProjectType(TechnicalProjectType): repository: strawberry.auto @strawberry_django.type(EngineeringProject) class EngineeringProjectType(TechnicalProjectType): lead_engineer: strawberry.auto @strawberry_django.interface(AppProject) class AppProjectType(TechnicalProjectType): repository: strawberry.auto @strawberry_django.type(AndroidProject) class AndroidProjectType(AppProjectType): android_version: strawberry.auto @strawberry_django.type(IOSProject) class IOSProjectType(AppProjectType): ios_version: strawberry.auto @strawberry_django.type(Company) class CompanyType: name: strawberry.auto projects: list[ProjectType] main_project: Optional[ProjectType] @strawberry.type class Query: companies: list[CompanyType] = strawberry_django.field() projects: list[ProjectType] = strawberry_django.field() projects_paginated: list[ProjectType] = strawberry_django.field(pagination=True) projects_offset_paginated: OffsetPaginated[ProjectType] = ( strawberry_django.offset_paginated() ) schema = strawberry.Schema( query=Query, types=[ ArtProjectType, ResearchProjectType, TechnicalProjectType, EngineeringProjectType, SoftwareProjectType, AndroidProjectType, IOSProjectType, ], extensions=[DjangoOptimizerExtension], ) strawberry-graphql-django-0.62.0/tests/polymorphism/test_optimizer.py000066400000000000000000000315241502405145400262430ustar00rootroot00000000000000import pytest from django.db import DEFAULT_DB_ALIAS, connections from django.test.utils import CaptureQueriesContext from tests.utils import assert_num_queries from .models import ( AndroidProject, ArtProject, Company, EngineeringProject, IOSProject, ResearchProject, SoftwareProject, ) from .schema import schema @pytest.mark.django_db(transaction=True) def test_polymorphic_interface_query(): ap = ArtProject.objects.create(topic="Art", artist="Artist") rp = ResearchProject.objects.create(topic="Research", supervisor="Supervisor") query = """\ query { projects { __typename topic ... on ArtProjectType { artist } ... on ResearchProjectType { supervisor } } } """ # ContentType, base table, two subtables = 4 queries with assert_num_queries(4): result = schema.execute_sync(query) assert not result.errors assert result.data == { "projects": [ {"__typename": "ArtProjectType", "topic": ap.topic, "artist": ap.artist}, { "__typename": "ResearchProjectType", "topic": rp.topic, "supervisor": rp.supervisor, }, ] } @pytest.mark.django_db(transaction=True) def test_polymorphic_query_abstract_model(): ap = ArtProject.objects.create(topic="Art", artist="Artist") sp = SoftwareProject.objects.create( topic="Software", repository="https://example.com", timeline="3 months" ) ep = EngineeringProject.objects.create( topic="Engineering", lead_engineer="Elara Voss", timeline="6 years" ) query = """\ query { projects { __typename topic ... on ArtProjectType { artist } ...on TechnicalProjectType { timeline } ... on SoftwareProjectType { repository } ...on EngineeringProjectType { leadEngineer } } } """ with assert_num_queries(5): result = schema.execute_sync(query) assert not result.errors assert result.data == { "projects": [ {"__typename": "ArtProjectType", "topic": ap.topic, "artist": ap.artist}, { "__typename": "SoftwareProjectType", "topic": sp.topic, "repository": sp.repository, "timeline": sp.timeline, }, { "__typename": "EngineeringProjectType", "topic": ep.topic, "leadEngineer": ep.lead_engineer, "timeline": ep.timeline, }, ] } @pytest.mark.django_db(transaction=True) def test_polymorphic_query_multiple_inheritance_levels(): app1 = AndroidProject.objects.create( topic="Software", repository="https://example.com/android", timeline="3 months", android_version="14", ) app2 = IOSProject.objects.create( topic="Software", repository="https://example.com/ios", timeline="5 months", ios_version="16", ) ep = EngineeringProject.objects.create( topic="Engineering", lead_engineer="Elara Voss", timeline="6 years" ) query = """\ query { projects { __typename topic ...on TechnicalProjectType { timeline } ...on AppProjectType { repository } ...on AndroidProjectType { androidVersion } ...on IOSProjectType { iosVersion } ...on EngineeringProjectType { leadEngineer } } } """ # Project Table, Content Type, AndroidProject, IOSProject, EngineeringProject = 5 with assert_num_queries(5): result = schema.execute_sync(query) assert not result.errors assert result.data == { "projects": [ { "__typename": "AndroidProjectType", "topic": app1.topic, "repository": app1.repository, "timeline": app1.timeline, "androidVersion": app1.android_version, }, { "__typename": "IOSProjectType", "topic": app2.topic, "repository": app2.repository, "timeline": app2.timeline, "iosVersion": app2.ios_version, }, { "__typename": "EngineeringProjectType", "topic": ep.topic, "leadEngineer": ep.lead_engineer, "timeline": ep.timeline, }, ] } @pytest.mark.django_db(transaction=True) def test_polymorphic_query_abstract_model_on_field(): ep = EngineeringProject.objects.create( topic="Engineering", lead_engineer="Elara Voss", timeline="6 years" ) company = Company.objects.create(name="Company", main_project=ep) query = """\ query { companies { name mainProject { __typename topic ...on TechnicalProjectType { timeline } ...on EngineeringProjectType { leadEngineer } } } } """ with assert_num_queries(4): result = schema.execute_sync(query) assert not result.errors assert result.data == { "companies": [ { "name": company.name, "mainProject": { "__typename": "EngineeringProjectType", "topic": ep.topic, "leadEngineer": ep.lead_engineer, "timeline": ep.timeline, }, } ] } @pytest.mark.django_db(transaction=True) def test_polymorphic_query_optimization_working(): ap = ArtProject.objects.create(topic="Art", artist="Artist") rp = ResearchProject.objects.create(topic="Research", supervisor="Supervisor") query = """\ query { projects { __typename topic ... on ArtProjectType { artist } ... on ResearchProjectType { supervisor } } } """ with CaptureQueriesContext(connection=connections[DEFAULT_DB_ALIAS]) as ctx: result = schema.execute_sync(query) # validate that we're not selecting extra fields assert any("artist" in q["sql"] for q in ctx.captured_queries) assert not any("research_notes" in q["sql"] for q in ctx.captured_queries) assert not any("art_style" in q["sql"] for q in ctx.captured_queries) assert not result.errors assert result.data == { "projects": [ {"__typename": "ArtProjectType", "topic": ap.topic, "artist": ap.artist}, { "__typename": "ResearchProjectType", "topic": rp.topic, "supervisor": rp.supervisor, }, ] } @pytest.mark.django_db(transaction=True) def test_polymorphic_paginated_query(): ap = ArtProject.objects.create(topic="Art", artist="Artist") rp = ResearchProject.objects.create(topic="Research", supervisor="Supervisor") query = """\ query { projectsPaginated { __typename topic ... on ArtProjectType { artist } ... on ResearchProjectType { supervisor } } } """ # ContentType, base table, two subtables = 4 queries with assert_num_queries(4): result = schema.execute_sync(query) assert not result.errors assert result.data == { "projectsPaginated": [ {"__typename": "ArtProjectType", "topic": ap.topic, "artist": ap.artist}, { "__typename": "ResearchProjectType", "topic": rp.topic, "supervisor": rp.supervisor, }, ] } @pytest.mark.django_db(transaction=True) def test_polymorphic_offset_paginated_query(): ap = ArtProject.objects.create(topic="Art", artist="Artist") rp = ResearchProject.objects.create(topic="Research", supervisor="Supervisor") query = """\ query { projectsOffsetPaginated { totalCount results { __typename topic ... on ArtProjectType { artist } ... on ResearchProjectType { supervisor } } } } """ # ContentType, base table, two subtables = 4 queries + 1 query for total count with assert_num_queries(5): result = schema.execute_sync(query) assert not result.errors assert result.data == { "projectsOffsetPaginated": { "totalCount": 2, "results": [ { "__typename": "ArtProjectType", "topic": ap.topic, "artist": ap.artist, }, { "__typename": "ResearchProjectType", "topic": rp.topic, "supervisor": rp.supervisor, }, ], } } @pytest.mark.django_db(transaction=True) def test_polymorphic_relation(): ap = ArtProject.objects.create(topic="Art", artist="Artist") art_company = Company.objects.create(name="ArtCompany", main_project=ap) rp = ResearchProject.objects.create(topic="Research", supervisor="Supervisor") research_company = Company.objects.create(name="ResearchCompany", main_project=rp) query = """\ query { companies { name mainProject { __typename topic ... on ArtProjectType { artist } ... on ResearchProjectType { supervisor } } } } """ # Company, ContentType, base table, two subtables = 5 queries with assert_num_queries(5): result = schema.execute_sync(query) assert not result.errors assert result.data == { "companies": [ { "name": art_company.name, "mainProject": { "__typename": "ArtProjectType", "topic": ap.topic, "artist": ap.artist, }, }, { "name": research_company.name, "mainProject": { "__typename": "ResearchProjectType", "topic": rp.topic, "supervisor": rp.supervisor, }, }, ] } @pytest.mark.django_db(transaction=True) def test_polymorphic_nested_list(): company = Company.objects.create(name="Company") ap = ArtProject.objects.create(company=company, topic="Art", artist="Artist") rp = ResearchProject.objects.create( company=company, topic="Research", supervisor="Supervisor" ) query = """\ query { companies { name projects { __typename topic ... on ArtProjectType { artist } ... on ResearchProjectType { supervisor } } } } """ # Company, ContentType, base table, two subtables = 5 queries with assert_num_queries(5): result = schema.execute_sync(query) assert not result.errors assert result.data == { "companies": [ { "name": "Company", "projects": [ { "__typename": "ArtProjectType", "topic": ap.topic, "artist": ap.artist, }, { "__typename": "ResearchProjectType", "topic": rp.topic, "supervisor": rp.supervisor, }, ], } ] } @pytest.mark.django_db(transaction=True) def test_optimizer_hints_polymorphic(): ap = ArtProject.objects.create(topic="Art", artist="Artist", art_style="abstract") rp = ResearchProject.objects.create(topic="Research", supervisor="Supervisor") query = """\ query { projects { __typename topicUpper ... on ArtProjectType { artistUpper artStyleUpper } } } """ # ContentType, base table, two subtables = 4 queries with assert_num_queries(4): result = schema.execute_sync(query) assert not result.errors assert result.data == { "projects": [ { "__typename": "ArtProjectType", "topicUpper": ap.topic.upper(), "artistUpper": ap.artist.upper(), "artStyleUpper": ap.art_style.upper(), }, { "__typename": "ResearchProjectType", "topicUpper": rp.topic.upper(), }, ] } strawberry-graphql-django-0.62.0/tests/polymorphism_custom/000077500000000000000000000000001502405145400241755ustar00rootroot00000000000000strawberry-graphql-django-0.62.0/tests/polymorphism_custom/__init__.py000066400000000000000000000000001502405145400262740ustar00rootroot00000000000000strawberry-graphql-django-0.62.0/tests/polymorphism_custom/models.py000066400000000000000000000016631502405145400260400ustar00rootroot00000000000000from django.db import models class Company(models.Model): name = models.CharField(max_length=100) main_project = models.ForeignKey( "Project", null=True, blank=True, on_delete=models.CASCADE ) class Meta: ordering = ("name",) class Project(models.Model): company = models.ForeignKey( Company, null=True, blank=True, on_delete=models.CASCADE, related_name="projects", ) topic = models.CharField(max_length=30) artist = models.CharField(max_length=30, blank=True) supervisor = models.CharField(max_length=30, blank=True) research_notes = models.TextField(blank=True) class Meta: constraints = ( models.CheckConstraint( check=(models.Q(artist="") | models.Q(supervisor="")) & (~models.Q(topic="") | ~models.Q(topic="")), name="artist_xor_supervisor", ), ) strawberry-graphql-django-0.62.0/tests/polymorphism_custom/schema.py000066400000000000000000000035671502405145400260220ustar00rootroot00000000000000from typing import Any, Optional import strawberry from graphql import GraphQLAbstractType, GraphQLResolveInfo from strawberry import Info from strawberry.relay import Node import strawberry_django from strawberry_django.optimizer import DjangoOptimizerExtension from strawberry_django.pagination import OffsetPaginated from strawberry_django.relay import DjangoListConnection from .models import Company, Project @strawberry_django.interface(Project) class ProjectType(Node): topic: strawberry.auto @classmethod def resolve_type( cls, value: Any, info: GraphQLResolveInfo, parent_type: GraphQLAbstractType ) -> str: if not isinstance(value, Project): raise TypeError if value.artist: return "ArtProjectType" if value.supervisor: return "ResearchProjectType" raise TypeError @classmethod def get_queryset(cls, qs, info: Info): return qs @strawberry_django.type(Project) class ArtProjectType(ProjectType): artist: strawberry.auto @strawberry_django.type(Project) class ResearchProjectType(ProjectType): supervisor: strawberry.auto @strawberry_django.type(Company) class CompanyType: name: strawberry.auto main_project: Optional[ProjectType] projects: list[ProjectType] @strawberry.type class Query: companies: list[CompanyType] = strawberry_django.field() projects: list[ProjectType] = strawberry_django.field() projects_paginated: list[ProjectType] = strawberry_django.field(pagination=True) projects_offset_paginated: OffsetPaginated[ProjectType] = ( strawberry_django.offset_paginated() ) projects_connection: DjangoListConnection[ProjectType] = ( strawberry_django.connection() ) schema = strawberry.Schema( query=Query, types=[ArtProjectType, ResearchProjectType], extensions=[DjangoOptimizerExtension], ) strawberry-graphql-django-0.62.0/tests/polymorphism_custom/test_optimizer.py000066400000000000000000000201011502405145400276220ustar00rootroot00000000000000import pytest from django.db import DEFAULT_DB_ALIAS, connections from django.test.utils import CaptureQueriesContext from tests.utils import assert_num_queries from .models import Company, Project from .schema import schema @pytest.mark.django_db(transaction=True) def test_polymorphic_interface_query(): ap = Project.objects.create(topic="Art", artist="Artist") rp = Project.objects.create(topic="Research", supervisor="Supervisor") query = """\ query { projects { __typename topic ... on ArtProjectType { artist } ... on ResearchProjectType { supervisor } } } """ with assert_num_queries(1): result = schema.execute_sync(query) assert not result.errors assert result.data == { "projects": [ {"__typename": "ArtProjectType", "topic": ap.topic, "artist": ap.artist}, { "__typename": "ResearchProjectType", "topic": rp.topic, "supervisor": rp.supervisor, }, ] } @pytest.mark.django_db(transaction=True) def test_polymorphic_query_optimization_working(): ap = Project.objects.create(topic="Art", artist="Artist") rp = Project.objects.create(topic="Research", supervisor="Supervisor") query = """\ query { projects { __typename topic ... on ArtProjectType { artist } ... on ResearchProjectType { supervisor } } } """ with CaptureQueriesContext(connection=connections[DEFAULT_DB_ALIAS]) as ctx: result = schema.execute_sync(query) # validate that we're not selecting extra fields assert any("artist" in q["sql"] for q in ctx.captured_queries) assert not any("research_notes" in q["sql"] for q in ctx.captured_queries) assert not result.errors assert result.data == { "projects": [ {"__typename": "ArtProjectType", "topic": ap.topic, "artist": ap.artist}, { "__typename": "ResearchProjectType", "topic": rp.topic, "supervisor": rp.supervisor, }, ] } @pytest.mark.django_db(transaction=True) def test_polymorphic_interface_paginated(): ap = Project.objects.create(topic="Art", artist="Artist") rp = Project.objects.create(topic="Research", supervisor="Supervisor") query = """\ query { projectsPaginated { __typename topic ... on ArtProjectType { artist } ... on ResearchProjectType { supervisor } } } """ with assert_num_queries(1): result = schema.execute_sync(query) assert not result.errors assert result.data == { "projectsPaginated": [ {"__typename": "ArtProjectType", "topic": ap.topic, "artist": ap.artist}, { "__typename": "ResearchProjectType", "topic": rp.topic, "supervisor": rp.supervisor, }, ] } @pytest.mark.django_db(transaction=True) def test_polymorphic_interface_offset_paginated(): ap = Project.objects.create(topic="Art", artist="Artist") rp = Project.objects.create(topic="Research", supervisor="Supervisor") query = """\ query { projectsOffsetPaginated { totalCount results { __typename topic ... on ArtProjectType { artist } ... on ResearchProjectType { supervisor } } } } """ with assert_num_queries(2): result = schema.execute_sync(query) assert not result.errors assert result.data == { "projectsOffsetPaginated": { "totalCount": 2, "results": [ { "__typename": "ArtProjectType", "topic": ap.topic, "artist": ap.artist, }, { "__typename": "ResearchProjectType", "topic": rp.topic, "supervisor": rp.supervisor, }, ], } } @pytest.mark.django_db(transaction=True) def test_polymorphic_interface_connection(): ap = Project.objects.create(topic="Art", artist="Artist") rp = Project.objects.create(topic="Research", supervisor="Supervisor") query = """\ query { projectsConnection { totalCount edges { node { __typename topic ... on ArtProjectType { artist } ... on ResearchProjectType { supervisor } } } } } """ with assert_num_queries(2): result = schema.execute_sync(query) assert not result.errors assert result.data == { "projectsConnection": { "totalCount": 2, "edges": [ { "node": { "__typename": "ArtProjectType", "topic": ap.topic, "artist": ap.artist, } }, { "node": { "__typename": "ResearchProjectType", "topic": rp.topic, "supervisor": rp.supervisor, } }, ], } } @pytest.mark.django_db(transaction=True) def test_polymorphic_relation(): ap = Project.objects.create(topic="Art", artist="Artist") art_company = Company.objects.create(name="ArtCompany", main_project=ap) rp = Project.objects.create(topic="Research", supervisor="Supervisor") research_company = Company.objects.create(name="ResearchCompany", main_project=rp) query = """\ query { companies { name mainProject { __typename topic ... on ArtProjectType { artist } ... on ResearchProjectType { supervisor } } } } """ with assert_num_queries(2): result = schema.execute_sync(query) assert not result.errors assert result.data == { "companies": [ { "name": art_company.name, "mainProject": { "__typename": "ArtProjectType", "topic": ap.topic, "artist": ap.artist, }, }, { "name": research_company.name, "mainProject": { "__typename": "ResearchProjectType", "topic": rp.topic, "supervisor": rp.supervisor, }, }, ] } @pytest.mark.django_db(transaction=True) def test_polymorphic_nested_list(): company = Company.objects.create(name="Company") ap = Project.objects.create(company=company, topic="Art", artist="Artist") rp = Project.objects.create( company=company, topic="Research", supervisor="Supervisor" ) query = """\ query { companies { name projects { __typename topic ... on ArtProjectType { artist } ... on ResearchProjectType { supervisor } } } } """ with assert_num_queries(2): result = schema.execute_sync(query) assert not result.errors assert result.data == { "companies": [ { "name": "Company", "projects": [ { "__typename": "ArtProjectType", "topic": ap.topic, "artist": ap.artist, }, { "__typename": "ResearchProjectType", "topic": rp.topic, "supervisor": rp.supervisor, }, ], } ] } strawberry-graphql-django-0.62.0/tests/polymorphism_inheritancemanager/000077500000000000000000000000001502405145400265075ustar00rootroot00000000000000strawberry-graphql-django-0.62.0/tests/polymorphism_inheritancemanager/__init__.py000066400000000000000000000000001502405145400306060ustar00rootroot00000000000000strawberry-graphql-django-0.62.0/tests/polymorphism_inheritancemanager/models.py000066400000000000000000000032601502405145400303450ustar00rootroot00000000000000from django.db import models from model_utils.managers import InheritanceManager from strawberry_django.descriptors import model_property class Company(models.Model): name = models.CharField(max_length=100) main_project = models.ForeignKey("Project", on_delete=models.CASCADE, null=True) class Meta: ordering = ("name",) class Project(models.Model): company = models.ForeignKey( Company, null=True, blank=True, on_delete=models.CASCADE, related_name="projects", ) topic = models.CharField(max_length=30) base_objects = InheritanceManager() objects = InheritanceManager() class Meta: base_manager_name = "base_objects" class ArtProject(Project): artist = models.CharField(max_length=30) art_style = models.CharField(max_length=30) @model_property(only=("art_style",)) def art_style_upper(self) -> str: return self.art_style.upper() class ResearchProject(Project): supervisor = models.CharField(max_length=30) research_notes = models.TextField() class TechnicalProject(Project): timeline = models.CharField(max_length=30) class Meta: # pyright: ignore [reportIncompatibleVariableOverride] abstract = True class SoftwareProject(TechnicalProject): repository = models.CharField(max_length=255) class EngineeringProject(TechnicalProject): lead_engineer = models.CharField(max_length=255) class AppProject(TechnicalProject): repository = models.CharField(max_length=255) class AndroidProject(AppProject): android_version = models.CharField(max_length=15) class IOSProject(AppProject): ios_version = models.CharField(max_length=15) strawberry-graphql-django-0.62.0/tests/polymorphism_inheritancemanager/schema.py000066400000000000000000000047501502405145400303270ustar00rootroot00000000000000from typing import Optional import strawberry import strawberry_django from strawberry_django.optimizer import DjangoOptimizerExtension from strawberry_django.pagination import OffsetPaginated from .models import ( AndroidProject, AppProject, ArtProject, Company, EngineeringProject, IOSProject, Project, ResearchProject, SoftwareProject, TechnicalProject, ) @strawberry_django.interface(Project) class ProjectType: topic: strawberry.auto @strawberry_django.field(only=("topic",)) def topic_upper(self) -> str: return self.topic.upper() @strawberry_django.type(ArtProject) class ArtProjectType(ProjectType): artist: strawberry.auto art_style_upper: strawberry.auto @strawberry_django.field(only=("artist",)) def artist_upper(self) -> str: return self.artist.upper() @strawberry_django.type(ResearchProject) class ResearchProjectType(ProjectType): supervisor: strawberry.auto @strawberry_django.interface(TechnicalProject) class TechnicalProjectType(ProjectType): timeline: strawberry.auto @strawberry_django.type(SoftwareProject) class SoftwareProjectType(TechnicalProjectType): repository: strawberry.auto @strawberry_django.type(EngineeringProject) class EngineeringProjectType(TechnicalProjectType): lead_engineer: strawberry.auto @strawberry_django.interface(AppProject) class AppProjectType(TechnicalProjectType): repository: strawberry.auto @strawberry_django.type(AndroidProject) class AndroidProjectType(AppProjectType): android_version: strawberry.auto @strawberry_django.type(IOSProject) class IOSProjectType(AppProjectType): ios_version: strawberry.auto @strawberry_django.type(Company) class CompanyType: name: strawberry.auto projects: list[ProjectType] main_project: Optional[ProjectType] @strawberry.type class Query: companies: list[CompanyType] = strawberry_django.field() projects: list[ProjectType] = strawberry_django.field() projects_paginated: list[ProjectType] = strawberry_django.field(pagination=True) projects_offset_paginated: OffsetPaginated[ProjectType] = ( strawberry_django.offset_paginated() ) schema = strawberry.Schema( query=Query, types=[ ArtProjectType, ResearchProjectType, TechnicalProjectType, EngineeringProjectType, SoftwareProjectType, AppProjectType, IOSProjectType, AndroidProjectType, ], extensions=[DjangoOptimizerExtension], ) strawberry-graphql-django-0.62.0/tests/polymorphism_inheritancemanager/test_optimizer.py000066400000000000000000000304211502405145400321420ustar00rootroot00000000000000import pytest from django.db import DEFAULT_DB_ALIAS, connections from django.test.utils import CaptureQueriesContext from tests.utils import assert_num_queries from .models import ( AndroidProject, ArtProject, Company, EngineeringProject, IOSProject, ResearchProject, SoftwareProject, ) from .schema import schema @pytest.mark.django_db(transaction=True) def test_polymorphic_interface_query(): ap = ArtProject.objects.create(topic="Art", artist="Artist") rp = ResearchProject.objects.create(topic="Research", supervisor="Supervisor") query = """\ query { projects { __typename topic ... on ArtProjectType { artist } ... on ResearchProjectType { supervisor } } } """ with assert_num_queries(1): result = schema.execute_sync(query) assert not result.errors assert result.data == { "projects": [ {"__typename": "ArtProjectType", "topic": ap.topic, "artist": ap.artist}, { "__typename": "ResearchProjectType", "topic": rp.topic, "supervisor": rp.supervisor, }, ] } @pytest.mark.django_db(transaction=True) def test_polymorphic_query_abstract_model(): ap = ArtProject.objects.create(topic="Art", artist="Artist") sp = SoftwareProject.objects.create( topic="Software", repository="https://example.com", timeline="3 months" ) ep = EngineeringProject.objects.create( topic="Engineering", lead_engineer="Elara Voss", timeline="6 years" ) query = """\ query { projects { __typename topic ... on ArtProjectType { artist } ...on TechnicalProjectType { timeline } ... on SoftwareProjectType { repository } ...on EngineeringProjectType { leadEngineer } } } """ with assert_num_queries(1): result = schema.execute_sync(query) assert not result.errors assert result.data == { "projects": [ {"__typename": "ArtProjectType", "topic": ap.topic, "artist": ap.artist}, { "__typename": "SoftwareProjectType", "topic": sp.topic, "repository": sp.repository, "timeline": sp.timeline, }, { "__typename": "EngineeringProjectType", "topic": ep.topic, "leadEngineer": ep.lead_engineer, "timeline": ep.timeline, }, ] } @pytest.mark.django_db(transaction=True) def test_polymorphic_query_multiple_inheritance_levels(): app1 = AndroidProject.objects.create( topic="Software", repository="https://example.com/android", timeline="3 months", android_version="14", ) app2 = IOSProject.objects.create( topic="Software", repository="https://example.com/ios", timeline="5 months", ios_version="16", ) ep = EngineeringProject.objects.create( topic="Engineering", lead_engineer="Elara Voss", timeline="6 years" ) query = """\ query { projects { __typename topic ...on TechnicalProjectType { timeline } ...on AppProjectType { repository } ...on AndroidProjectType { androidVersion } ...on IOSProjectType { iosVersion } ...on EngineeringProjectType { leadEngineer } } } """ with assert_num_queries(1): result = schema.execute_sync(query) assert not result.errors assert result.data == { "projects": [ { "__typename": "AndroidProjectType", "topic": app1.topic, "repository": app1.repository, "timeline": app1.timeline, "androidVersion": app1.android_version, }, { "__typename": "IOSProjectType", "topic": app2.topic, "repository": app2.repository, "timeline": app2.timeline, "iosVersion": app2.ios_version, }, { "__typename": "EngineeringProjectType", "topic": ep.topic, "leadEngineer": ep.lead_engineer, "timeline": ep.timeline, }, ] } @pytest.mark.django_db(transaction=True) def test_polymorphic_query_abstract_model_on_field(): ep = EngineeringProject.objects.create( topic="Engineering", lead_engineer="Elara Voss", timeline="6 years" ) company = Company.objects.create(name="Company", main_project=ep) query = """\ query { companies { name mainProject { __typename topic ...on TechnicalProjectType { timeline } ...on EngineeringProjectType { leadEngineer } } } } """ with assert_num_queries(2): result = schema.execute_sync(query) assert not result.errors assert result.data == { "companies": [ { "name": company.name, "mainProject": { "__typename": "EngineeringProjectType", "topic": ep.topic, "leadEngineer": ep.lead_engineer, "timeline": ep.timeline, }, } ] } @pytest.mark.django_db(transaction=True) def test_polymorphic_query_optimization_working(): ap = ArtProject.objects.create(topic="Art", artist="Artist") rp = ResearchProject.objects.create(topic="Research", supervisor="Supervisor") query = """\ query { projects { __typename topic ... on ArtProjectType { artist } ... on ResearchProjectType { supervisor } } } """ with CaptureQueriesContext(connection=connections[DEFAULT_DB_ALIAS]) as ctx: result = schema.execute_sync(query) # validate that we're not selecting extra fields assert not any("research_notes" in q for q in ctx.captured_queries) assert not any("art_style" in q for q in ctx.captured_queries) assert not result.errors assert result.data == { "projects": [ {"__typename": "ArtProjectType", "topic": ap.topic, "artist": ap.artist}, { "__typename": "ResearchProjectType", "topic": rp.topic, "supervisor": rp.supervisor, }, ] } @pytest.mark.django_db(transaction=True) def test_polymorphic_paginated_query(): ap = ArtProject.objects.create(topic="Art", artist="Artist") rp = ResearchProject.objects.create(topic="Research", supervisor="Supervisor") query = """\ query { projectsPaginated { __typename topic ... on ArtProjectType { artist } ... on ResearchProjectType { supervisor } } } """ with assert_num_queries(1): result = schema.execute_sync(query) assert not result.errors assert result.data == { "projectsPaginated": [ {"__typename": "ArtProjectType", "topic": ap.topic, "artist": ap.artist}, { "__typename": "ResearchProjectType", "topic": rp.topic, "supervisor": rp.supervisor, }, ] } @pytest.mark.django_db(transaction=True) def test_polymorphic_offset_paginated_query(): ap = ArtProject.objects.create(topic="Art", artist="Artist") rp = ResearchProject.objects.create(topic="Research", supervisor="Supervisor") query = """\ query { projectsOffsetPaginated { totalCount results { __typename topic ... on ArtProjectType { artist } ... on ResearchProjectType { supervisor } } } } """ with assert_num_queries(2): result = schema.execute_sync(query) assert not result.errors assert result.data == { "projectsOffsetPaginated": { "totalCount": 2, "results": [ { "__typename": "ArtProjectType", "topic": ap.topic, "artist": ap.artist, }, { "__typename": "ResearchProjectType", "topic": rp.topic, "supervisor": rp.supervisor, }, ], } } @pytest.mark.django_db(transaction=True) def test_polymorphic_relation(): ap = ArtProject.objects.create(topic="Art", artist="Artist") art_company = Company.objects.create(name="ArtCompany", main_project=ap) rp = ResearchProject.objects.create(topic="Research", supervisor="Supervisor") research_company = Company.objects.create(name="ResearchCompany", main_project=rp) query = """\ query { companies { name mainProject { __typename topic ... on ArtProjectType { artist } ... on ResearchProjectType { supervisor } } } } """ with assert_num_queries(2): result = schema.execute_sync(query) assert not result.errors assert result.data == { "companies": [ { "name": art_company.name, "mainProject": { "__typename": "ArtProjectType", "topic": ap.topic, "artist": ap.artist, }, }, { "name": research_company.name, "mainProject": { "__typename": "ResearchProjectType", "topic": rp.topic, "supervisor": rp.supervisor, }, }, ] } @pytest.mark.django_db(transaction=True) def test_polymorphic_nested_list(): company = Company.objects.create(name="Company") ap = ArtProject.objects.create(company=company, topic="Art", artist="Artist") rp = ResearchProject.objects.create( company=company, topic="Research", supervisor="Supervisor" ) query = """\ query { companies { name projects { __typename topic ... on ArtProjectType { artist } ... on ResearchProjectType { supervisor } } } } """ with assert_num_queries(2): result = schema.execute_sync(query) assert not result.errors assert result.data == { "companies": [ { "name": "Company", "projects": [ { "__typename": "ArtProjectType", "topic": ap.topic, "artist": ap.artist, }, { "__typename": "ResearchProjectType", "topic": rp.topic, "supervisor": rp.supervisor, }, ], } ] } @pytest.mark.django_db(transaction=True) def test_optimizer_hints_polymorphic(): ap = ArtProject.objects.create(topic="Art", artist="Artist") rp = ResearchProject.objects.create(topic="Research", supervisor="Supervisor") query = """\ query { projects { __typename topicUpper ... on ArtProjectType { artistUpper artStyleUpper } } } """ with assert_num_queries(1): result = schema.execute_sync(query) assert not result.errors assert result.data == { "projects": [ { "__typename": "ArtProjectType", "topicUpper": ap.topic.upper(), "artistUpper": ap.artist.upper(), "artStyleUpper": ap.art_style.upper(), }, { "__typename": "ResearchProjectType", "topicUpper": rp.topic.upper(), }, ] } strawberry-graphql-django-0.62.0/tests/projects/000077500000000000000000000000001502405145400216725ustar00rootroot00000000000000strawberry-graphql-django-0.62.0/tests/projects/__init__.py000066400000000000000000000000001502405145400237710ustar00rootroot00000000000000strawberry-graphql-django-0.62.0/tests/projects/faker.py000066400000000000000000000044461502405145400233440ustar00rootroot00000000000000from typing import Any, ClassVar, Generic, TypeVar import factory from django.contrib.auth import get_user_model from django.contrib.auth.hashers import make_password from django.contrib.auth.models import Group from factory.declarations import Iterator, LazyFunction, Sequence, SubFactory from factory.faker import Faker from .models import Favorite, Issue, Milestone, Project, Quiz, Tag _T = TypeVar("_T") User = get_user_model() class _BaseFactory(factory.django.DjangoModelFactory, Generic[_T]): Meta: ClassVar[Any] @classmethod def create(cls, **kwargs) -> _T: return super().create(**kwargs) @classmethod def create_batch(cls, size: int, **kwargs) -> list[_T]: return super().create_batch(size, **kwargs) class GroupFactory(_BaseFactory[Group]): class Meta: model = Group name = Sequence(lambda n: f"Group {n}") class UserFactory(_BaseFactory["User"]): class Meta: model = User is_active = True first_name = Faker("first_name") last_name = Faker("last_name") username = Sequence(lambda n: f"user-{n}") email = Faker("email") password = LazyFunction(lambda: make_password("foobar")) class StaffUserFactory(UserFactory): is_staff = True class SuperuserUserFactory(UserFactory): is_superuser = True class ProjectFactory(_BaseFactory[Project]): class Meta: model = Project name = Sequence(lambda n: f"Project {n}") due_date = Faker("future_date") class MilestoneFactory(_BaseFactory[Milestone]): class Meta: model = Milestone name = Sequence(lambda n: f"Milestone {n}") due_date = Faker("future_date") project = SubFactory(ProjectFactory) class FavoriteFactory(_BaseFactory[Favorite]): class Meta: model = Favorite name = Sequence(lambda n: f"Favorite {n}") class IssueFactory(_BaseFactory[Issue]): class Meta: model = Issue name = Sequence(lambda n: f"Issue {n}") kind = Iterator(Issue.Kind) milestone = SubFactory(MilestoneFactory) priority = Faker("pyint", min_value=0, max_value=5) class TagFactory(_BaseFactory[Tag]): class Meta: model = Tag name = Sequence(lambda n: f"Tag {n}") class QuizFactory(_BaseFactory[Quiz]): class Meta: model = Quiz title = Sequence(lambda n: f"Quiz {n}") strawberry-graphql-django-0.62.0/tests/projects/models.py000066400000000000000000000143271502405145400235360ustar00rootroot00000000000000from typing import TYPE_CHECKING, Annotated, Any, Optional import strawberry from django.contrib.auth import get_user_model from django.db import models from django.db.models import Count, Prefetch, QuerySet from django.utils.translation import gettext_lazy as _ from django_choices_field import TextChoicesField from strawberry_django.descriptors import model_property from strawberry_django.optimizer import OptimizerStore, optimize from strawberry_django.utils.typing import UserType if TYPE_CHECKING: from django.db.models.manager import RelatedManager from .schema import MilestoneType User = get_user_model() class NamedModel(models.Model): class Meta: abstract = True name = models.CharField( max_length=255, ) class Project(NamedModel): class Status(models.TextChoices): """Project status options.""" ACTIVE = "active", "Active" INACTIVE = "inactive", "Inactive" milestones: "RelatedManager[Milestone]" id = models.BigAutoField( verbose_name="ID", primary_key=True, ) status = TextChoicesField( help_text=_("This project's status"), choices_enum=Status, default=Status.ACTIVE, ) due_date = models.DateField( null=True, blank=True, default=None, ) cost = models.DecimalField( max_digits=20, decimal_places=2, null=True, blank=True, default=None, ) @model_property(annotate={"_milestone_count": Count("milestone")}) def is_small(self) -> bool: return self._milestone_count < 3 # type: ignore @model_property( prefetch_related=lambda info: Prefetch( "milestones", queryset=optimize( Milestone.objects.all(), info, store=OptimizerStore.with_hints(only="project_id"), ), to_attr="custom_milestones_property", ) ) def custom_milestones_model_property( self, ) -> list[Annotated["MilestoneType", strawberry.lazy("tests.projects.schema")]]: return self.custom_milestones_property # type: ignore class Milestone(NamedModel): issues: "RelatedManager[Issue]" id = models.BigAutoField( verbose_name="ID", primary_key=True, ) due_date = models.DateField( null=True, blank=True, default=None, ) project_id: int project = models.ForeignKey[Project]( Project, on_delete=models.CASCADE, related_name="milestones", related_query_name="milestone", ) class FavoriteQuerySet(QuerySet): def by_user(self, user: UserType): if user.is_anonymous: return self.none() return self.filter(user__pk=user.pk) class Favorite(models.Model): """A user's favorite issues.""" class Meta: # Needed to allow type's get_queryset() to access a model's custom QuerySet ordering = ("name",) base_manager_name = "objects" id = models.BigAutoField( verbose_name="ID", primary_key=True, ) name = models.CharField(max_length=32) user = models.ForeignKey( User, related_name="favorite_set", on_delete=models.CASCADE, ) issue = models.ForeignKey( "Issue", related_name="favorite_set", on_delete=models.CASCADE, ) objects = FavoriteQuerySet.as_manager() class Issue(NamedModel): class Meta: # type: ignore ordering = ("id",) class Kind(models.TextChoices): """Issue kind options.""" BUG = "b", "Bug" FEATURE = "f", "Feature" comments: "RelatedManager[Issue]" issue_assignees: "RelatedManager[Assignee]" id = models.BigAutoField( verbose_name="ID", primary_key=True, ) kind = models.CharField( verbose_name="kind", help_text="the kind of the issue", choices=Kind.choices, max_length=max(len(k.value) for k in Kind), default=None, blank=True, null=True, ) priority = models.IntegerField( default=0, ) milestone_id: Optional[int] milestone = models.ForeignKey( Milestone, on_delete=models.SET_NULL, related_name="issues", related_query_name="issue", null=True, blank=True, default=None, ) tags = models.ManyToManyField["Tag", Any]( "Tag", related_name="issues", related_query_name="issue", ) assignees = models.ManyToManyField["User", "Assignee"]( User, through="Assignee", related_name="+", ) @property def name_with_kind(self) -> str: return f"{self.kind}: {self.name}" @model_property(only=["kind", "priority"]) def name_with_priority(self) -> str: """Field doc.""" return f"{self.kind}: {self.priority}" class Assignee(models.Model): class Meta: unique_together = [ # noqa: RUF012 ("issue", "user"), ] issues: "RelatedManager[Issue]" id = models.BigAutoField( verbose_name="ID", primary_key=True, ) issue_id: int issue = models.ForeignKey[Issue]( Issue, on_delete=models.CASCADE, related_name="issue_assignees", related_query_name="issue_assignee", ) user_id: int user = models.ForeignKey["User"]( User, on_delete=models.CASCADE, related_name="issue_assignees", related_query_name="issue_assignee", ) owner = models.BooleanField( default=False, ) class Tag(NamedModel): class Meta: # type: ignore ordering = ("id",) issues: "RelatedManager[Issue]" id = models.BigAutoField( verbose_name="ID", primary_key=True, ) class Quiz(models.Model): title = models.CharField( "title", max_length=255, ) sequence = models.PositiveIntegerField( "sequence", default=1, unique=True, ) def save(self, *args, **kwargs): if self._state.adding: max_ = self.__class__.objects.aggregate(max=models.Max("sequence"))["max"] if max_ is not None: self.sequence = max_ + 1 super().save(*args, **kwargs) strawberry-graphql-django-0.62.0/tests/projects/schema.py000066400000000000000000000511061502405145400235070ustar00rootroot00000000000000import asyncio import datetime import decimal from collections.abc import Iterable from typing import Annotated, Optional, cast import strawberry from django.contrib.auth import get_user_model from django.contrib.auth.models import AbstractUser from django.core.exceptions import ValidationError from django.db.models import ( BooleanField, Count, Exists, ExpressionWrapper, OuterRef, Prefetch, Q, Subquery, Value, ) from django.db.models.fields import CharField from django.db.models.functions import Now from django.db.models.query import QuerySet from strawberry import UNSET, relay from strawberry.types.info import Info import strawberry_django from strawberry_django import mutations from strawberry_django.auth.queries import get_current_user from strawberry_django.fields.types import ListInput, NodeInput, NodeInputPartial from strawberry_django.mutations import resolvers from strawberry_django.optimizer import ( DjangoOptimizerExtension, OptimizerStore, optimize, ) from strawberry_django.pagination import OffsetPaginated from strawberry_django.permissions import ( HasPerm, HasRetvalPerm, IsAuthenticated, IsStaff, IsSuperuser, filter_for_user, ) from strawberry_django.relay import DjangoListConnection from .models import ( Assignee, Favorite, FavoriteQuerySet, Issue, Milestone, NamedModel, Project, Quiz, Tag, ) UserModel = get_user_model() @strawberry_django.interface(NamedModel) class Named: name: strawberry.auto @strawberry_django.type(UserModel) class UserType(relay.Node): username: relay.NodeID[str] email: strawberry.auto is_active: strawberry.auto is_superuser: strawberry.auto is_staff: strawberry.auto @strawberry_django.field(only=["first_name", "last_name"]) def full_name(self, root: AbstractUser) -> str: return f"{root.first_name or ''} {root.last_name or ''}".strip() @strawberry_django.type(UserModel) class StaffType(relay.Node): username: relay.NodeID[str] email: strawberry.auto is_active: strawberry.auto is_superuser: strawberry.auto is_staff: strawberry.auto @classmethod def get_queryset( cls, queryset: QuerySet[AbstractUser], info: Info, **kwargs, ) -> QuerySet[AbstractUser]: return queryset.filter(is_staff=True) @strawberry_django.filter_type(Project, lookups=True) class ProjectFilter: name: strawberry.auto due_date: strawberry.auto @strawberry_django.type(Project, filters=ProjectFilter, pagination=True) class ProjectType(relay.Node, Named): due_date: strawberry.auto is_small: strawberry.auto is_delayed: bool = strawberry_django.field( annotate=ExpressionWrapper( Q(due_date__lt=Now()), output_field=BooleanField(), ), ) cost: strawberry.auto = strawberry_django.field(extensions=[IsAuthenticated()]) milestones: list["MilestoneType"] = strawberry_django.field(pagination=True) milestones_count: int = strawberry_django.field(annotate=Count("milestone")) custom_milestones_model_property: strawberry.auto first_milestone: Optional["MilestoneType"] = strawberry_django.field( field_name="milestones" ) first_milestone_required: "MilestoneType" = strawberry_django.field( field_name="milestones" ) milestone_conn: DjangoListConnection["MilestoneType"] = ( strawberry_django.connection(field_name="milestones") ) milestones_paginated: OffsetPaginated["MilestoneType"] = ( strawberry_django.offset_paginated(field_name="milestones") ) @strawberry_django.field( prefetch_related=lambda info: Prefetch( "milestones", queryset=optimize( Milestone.objects.all(), info, store=OptimizerStore.with_hints(only="project_id"), ), to_attr="custom_milestones", ) ) @staticmethod def custom_milestones( parent: strawberry.Parent, info: Info ) -> list["MilestoneType"]: return parent.custom_milestones @strawberry_django.filter_type(Milestone, lookups=True) class MilestoneFilter: name: strawberry.auto project: strawberry.auto search: Optional[str] def filter_search(self, queryset: QuerySet[Milestone]): return queryset.filter(name__contains=self.search) @strawberry_django.order(Project) class ProjectOrder: id: strawberry.auto name: strawberry.auto @strawberry_django.order(Milestone) class MilestoneOrder: name: strawberry.auto project: Optional[ProjectOrder] @strawberry_django.filter_type(Issue, lookups=True) class IssueFilter: name: strawberry.auto @strawberry_django.filter_field() def search(self, value: str, prefix: str) -> Q: return Q(name__contains=value) @strawberry_django.order(Issue) class IssueOrder: name: strawberry.auto @strawberry_django.type( Milestone, filters=MilestoneFilter, order=MilestoneOrder, pagination=True ) class MilestoneType(relay.Node, Named): due_date: strawberry.auto project: ProjectType issues: list["IssueType"] = strawberry_django.field( filters=IssueFilter, order=IssueOrder, pagination=True, ) first_issue: Optional["IssueType"] = strawberry_django.field(field_name="issues") first_issue_required: "IssueType" = strawberry_django.field(field_name="issues") graphql_path: str = strawberry_django.field( annotate=lambda info: Value( ",".join(map(str, info.path.as_list())), output_field=CharField(max_length=255), ) ) mixed_annotated_prefetch: str = strawberry_django.field( annotate=lambda info: Value("dummy", output_field=CharField(max_length=255)), prefetch_related="issues", ) mixed_prefetch_annotated: str = strawberry_django.field( annotate=Value("dummy", output_field=CharField(max_length=255)), prefetch_related=lambda info: Prefetch("issues"), ) issues_paginated: OffsetPaginated["IssueType"] = strawberry_django.offset_paginated( field_name="issues", order=IssueOrder, ) issues_with_filters: DjangoListConnection["IssueType"] = ( strawberry_django.connection( field_name="issues", filters=IssueFilter, ) ) @strawberry_django.field( prefetch_related=[ lambda info: Prefetch( "issues", queryset=Issue.objects.filter( Exists( Assignee.objects.filter( issue=OuterRef("pk"), user_id=info.context.request.user.id, ), ), ), to_attr="_my_issues", ), ], ) def my_issues(self) -> list["IssueType"]: return self._my_issues # type: ignore @strawberry_django.field( annotate={ "_my_bugs_count": lambda info: Count( "issue", filter=Q( issue__issue_assignee__user_id=info.context.request.user.id, issue__kind=Issue.Kind.BUG, ), ), }, ) def my_bugs_count(self, root: Milestone) -> int: return root._my_bugs_count # type: ignore @strawberry_django.field async def async_field(self, value: str) -> str: await asyncio.sleep(0) return f"value: {value}" @strawberry_django.type(Favorite) class FavoriteType(relay.Node): name: strawberry.auto user: UserType issue: "IssueType" @classmethod def get_queryset(cls, queryset: FavoriteQuerySet, info: Info, **kwargs) -> QuerySet: return queryset.by_user(info.context.request.user) @strawberry_django.type(Issue) class IssueType(relay.Node, Named): milestone: MilestoneType priority: strawberry.auto kind: strawberry.auto name_with_priority: strawberry.auto name_with_kind: str = strawberry_django.field(only=["kind", "name"]) tags: list["TagType"] issue_assignees: list["AssigneeType"] staff_assignees: list["StaffType"] = strawberry_django.field(field_name="assignees") favorite_set: DjangoListConnection["FavoriteType"] = strawberry_django.connection() @strawberry_django.field(select_related="milestone", only="milestone__name") def milestone_name(self) -> str: return self.milestone.name @strawberry_django.field(select_related="milestone") def milestone_name_without_only_optimization(self) -> str: return self.milestone.name @strawberry_django.field( annotate={ "_private_name": lambda info: Subquery( filter_for_user( Issue.objects.all(), info.context.request.user, ["projects.view_issue"], ) .filter(id=OuterRef("pk")) .values("name")[:1], ), }, ) def private_name(self, root: Issue) -> Optional[str]: return root._private_name # type: ignore @strawberry_django.type(Tag) class TagType(relay.Node, Named): issues: DjangoListConnection[IssueType] = strawberry_django.connection() @strawberry_django.field def issues_with_selected_related_milestone_and_project(self) -> list[IssueType]: # here, the `select_related` is on the queryset directly, and not on the field return ( self.issues.all() # type: ignore .select_related("milestone", "milestone__project") .order_by("id") ) @strawberry_django.type(Quiz) class QuizType(relay.Node): title: strawberry.auto sequence: strawberry.auto @classmethod def get_queryset( cls, queryset: QuerySet[Quiz], info: Info, **kwargs, ) -> QuerySet[Quiz]: return queryset.order_by("title") @strawberry_django.partial(Tag) class TagInputPartial(NodeInputPartial): name: strawberry.auto @strawberry_django.input(Issue) class IssueInput: name: strawberry.auto milestone: "MilestoneInputPartial" priority: strawberry.auto kind: strawberry.auto tags: Optional[list[NodeInput]] extra: Optional[str] = strawberry.field(default=UNSET, graphql_type=Optional[int]) @strawberry_django.type(Assignee) class AssigneeType(relay.Node): user: UserType owner: strawberry.auto @strawberry_django.partial(Assignee) class IssueAssigneeInputPartial(NodeInputPartial): user: Optional[NodeInputPartial] owner: strawberry.auto @strawberry.input class AssigneeThroughInputPartial: owner: Optional[bool] = strawberry.UNSET @strawberry_django.partial(UserModel) class AssigneeInputPartial(NodeInputPartial): through_defaults: Optional[AssigneeThroughInputPartial] = strawberry.UNSET @strawberry_django.partial(Issue) class IssueInputPartial(NodeInput, IssueInput): tags: Optional[ListInput[TagInputPartial]] = UNSET # type: ignore assignees: Optional[ListInput[AssigneeInputPartial]] = UNSET issue_assignees: Optional[ListInput[IssueAssigneeInputPartial]] = UNSET @strawberry_django.partial(Issue) class IssueInputPartialWithoutId(IssueInput): tags: Optional[ListInput[TagInputPartial]] = UNSET # type: ignore assignees: Optional[ListInput[AssigneeInputPartial]] = UNSET issue_assignees: Optional[ListInput[IssueAssigneeInputPartial]] = UNSET @strawberry_django.input(Issue) class MilestoneIssueInput: name: strawberry.auto @strawberry_django.partial(Issue) class MilestoneIssueInputPartial: name: strawberry.auto tags: Optional[list[TagInputPartial]] @strawberry_django.partial(Project) class ProjectInputPartial(NodeInputPartial): name: strawberry.auto milestones: Optional[list["MilestoneInputPartial"]] @strawberry_django.input(Milestone) class MilestoneInput: name: strawberry.auto project: ProjectInputPartial issues: Optional[list[MilestoneIssueInput]] @strawberry_django.partial(Milestone) class MilestoneInputPartial(NodeInputPartial): name: strawberry.auto issues: Optional[list[MilestoneIssueInputPartial]] project: Optional[ProjectInputPartial] @strawberry.type class ProjectConnection(DjangoListConnection[ProjectType]): """Project connection documentation.""" @strawberry.type class Query: """All available queries for this schema.""" node: Optional[relay.Node] = strawberry_django.node() favorite: Optional[FavoriteType] = strawberry_django.node() issue: Optional[IssueType] = strawberry_django.node(description="Foobar") milestone: Optional[ Annotated["MilestoneType", strawberry.lazy("tests.projects.schema")] ] = strawberry_django.node() milestone_mandatory: MilestoneType = strawberry_django.node() milestones: list[MilestoneType] = strawberry_django.node() project: Optional[ProjectType] = strawberry_django.node() project_mandatory: ProjectType = strawberry_django.node() project_login_required: Optional[ProjectType] = strawberry_django.node( extensions=[IsAuthenticated()], ) tag: Optional[TagType] = strawberry_django.node() staff: Optional[StaffType] = strawberry_django.node() staff_list: list[Optional[StaffType]] = strawberry_django.node() issue_list: list[IssueType] = strawberry_django.field() issues_paginated: OffsetPaginated[IssueType] = strawberry_django.offset_paginated() milestone_list: list[MilestoneType] = strawberry_django.field( order=MilestoneOrder, filters=MilestoneFilter, pagination=True, ) project_list: list[ProjectType] = strawberry_django.field() projects_paginated: OffsetPaginated[ProjectType] = ( strawberry_django.offset_paginated() ) tag_list: list[TagType] = strawberry_django.field() favorite_conn: DjangoListConnection[FavoriteType] = strawberry_django.connection() issue_conn: DjangoListConnection[ strawberry.LazyType[ "IssueType", "tests.projects.schema", # type: ignore # noqa: F821 ] ] = strawberry_django.connection() milestone_conn: DjangoListConnection[MilestoneType] = strawberry_django.connection() project_conn: ProjectConnection = strawberry_django.connection() tag_conn: DjangoListConnection[TagType] = strawberry_django.connection() staff_conn: DjangoListConnection[StaffType] = strawberry_django.connection() quiz_list: list[QuizType] = strawberry_django.field() # Login required to resolve issue_login_required: IssueType = strawberry_django.node( extensions=[IsAuthenticated()], ) issue_login_required_optional: Optional[IssueType] = strawberry_django.node( extensions=[IsAuthenticated()], ) # Staff required to resolve issue_staff_required: IssueType = strawberry_django.node(extensions=[IsStaff()]) issue_staff_required_optional: Optional[IssueType] = strawberry_django.node( extensions=[IsStaff()], ) # Superuser required to resolve issue_superuser_required: IssueType = strawberry_django.node( extensions=[IsSuperuser()], ) issue_superuser_required_optional: Optional[IssueType] = strawberry_django.node( extensions=[IsSuperuser()], ) # User permission on "projects.view_issue" to resolve issue_perm_required: IssueType = strawberry_django.node( extensions=[HasPerm(perms=["projects.view_issue"])], ) issue_perm_required_optional: Optional[IssueType] = strawberry_django.node( extensions=[HasPerm(perms=["projects.view_issue"])], ) issue_list_perm_required: list[IssueType] = strawberry_django.field( extensions=[HasPerm(perms=["projects.view_issue"])], ) issues_paginated_perm_required: OffsetPaginated[IssueType] = ( strawberry_django.offset_paginated( extensions=[HasPerm(perms=["projects.view_issue"])], ) ) issue_conn_perm_required: DjangoListConnection[IssueType] = ( strawberry_django.connection( extensions=[HasPerm(perms=["projects.view_issue"])], ) ) # User permission on the resolved object for "projects.view_issue" issue_obj_perm_required: IssueType = strawberry_django.node( extensions=[HasRetvalPerm(perms=["projects.view_issue"])], ) issue_obj_perm_required_optional: Optional[IssueType] = strawberry_django.node( extensions=[HasRetvalPerm(perms=["projects.view_issue"])], ) issue_list_obj_perm_required: list[IssueType] = strawberry_django.field( extensions=[HasRetvalPerm(perms=["projects.view_issue"])], ) issue_list_obj_perm_required_paginated: list[IssueType] = strawberry_django.field( extensions=[HasRetvalPerm(perms=["projects.view_issue"])], pagination=True ) issues_paginated_obj_perm_required: OffsetPaginated[IssueType] = ( strawberry_django.offset_paginated( extensions=[HasRetvalPerm(perms=["projects.view_issue"])], ) ) issue_conn_obj_perm_required: DjangoListConnection[IssueType] = ( strawberry_django.connection( extensions=[HasRetvalPerm(perms=["projects.view_issue"])], ) ) @strawberry_django.field( extensions=[HasPerm(perms=["projects.view_issue"], with_superuser=True)] ) async def async_user_resolve(self) -> bool: return True @strawberry_django.field def me(self, info: Info) -> Optional[UserType]: user = get_current_user(info, strict=True) if not user.is_authenticated: return None return cast("UserType", user) @strawberry_django.connection(ProjectConnection) def project_conn_with_resolver(self, root: str, name: str) -> Iterable[Project]: return Project.objects.filter(name__contains=name) @strawberry.type class Mutation: """All available mutations for this schema.""" create_issue: IssueType = mutations.create( IssueInput, handle_django_errors=True, argument_name="input", ) update_issue: IssueType = mutations.update( IssueInputPartial, handle_django_errors=True, argument_name="input", ) update_issue_with_key_attr: IssueType = mutations.update( IssueInputPartialWithoutId, handle_django_errors=True, argument_name="input", key_attr="name", ) delete_issue: IssueType = mutations.delete( NodeInput, handle_django_errors=True, argument_name="input", ) delete_issue_with_key_attr: IssueType = mutations.delete( MilestoneIssueInput, handle_django_errors=True, argument_name="input", key_attr="name", ) create_project_with_milestones: ProjectType = mutations.create( ProjectInputPartial, handle_django_errors=True, argument_name="input", ) update_project: ProjectType = mutations.update( ProjectInputPartial, handle_django_errors=True, argument_name="input", ) create_milestone: MilestoneType = mutations.create( MilestoneInput, handle_django_errors=True, argument_name="input", ) @mutations.input_mutation(handle_django_errors=True) def create_project( self, info: Info, name: str, cost: Annotated[ decimal.Decimal, strawberry.argument(description="The project's cost"), ], due_date: Optional[datetime.datetime] = None, ) -> ProjectType: """Create project documentation.""" if cost > 500: # Field error without error code: raise ValidationError({"cost": ["Cost cannot be higher than 500"]}) if cost < 0: # Field error with error code: raise ValidationError( { "cost": ValidationError( "Cost cannot be lower than zero", code="min_cost", ), }, ) project = Project( name=name, cost=cost, due_date=due_date, ) project.full_clean() project.save() return cast( "ProjectType", project, ) @mutations.input_mutation(handle_django_errors=True) def create_quiz( self, info: Info, title: str, full_clean_options: bool = False, ) -> QuizType: return cast( "QuizType", resolvers.create( info, Quiz, {"title": title}, full_clean={"exclude": ["sequence"]} if full_clean_options else True, key_attr="id", ), ) schema = strawberry.Schema( query=Query, mutation=Mutation, extensions=[ DjangoOptimizerExtension, ], ) strawberry-graphql-django-0.62.0/tests/projects/snapshots/000077500000000000000000000000001502405145400237145ustar00rootroot00000000000000strawberry-graphql-django-0.62.0/tests/projects/snapshots/schema.gql000066400000000000000000000653301502405145400256700ustar00rootroot00000000000000""" Can only be resolved by authenticated users. When the condition fails, the following can be returned (following this priority): 1) `OperationInfo`/`OperationMessage` if those types are allowed at the return type 2) `null` in case the field is not mandatory (e.g. `String` or `[String]`) 3) An empty list in case the field is a list (e.g. `[String]!`) 4) An empty `Connection` in case the return type is a relay connection 2) Otherwise, an error will be raised """ directive @isAuthenticated repeatable on FIELD_DEFINITION """ Can only be resolved by staff users. When the condition fails, the following can be returned (following this priority): 1) `OperationInfo`/`OperationMessage` if those types are allowed at the return type 2) `null` in case the field is not mandatory (e.g. `String` or `[String]`) 3) An empty list in case the field is a list (e.g. `[String]!`) 4) An empty `Connection` in case the return type is a relay connection 2) Otherwise, an error will be raised """ directive @isStaff repeatable on FIELD_DEFINITION """ Can only be resolved by superuser users. When the condition fails, the following can be returned (following this priority): 1) `OperationInfo`/`OperationMessage` if those types are allowed at the return type 2) `null` in case the field is not mandatory (e.g. `String` or `[String]`) 3) An empty list in case the field is a list (e.g. `[String]!`) 4) An empty `Connection` in case the return type is a relay connection 2) Otherwise, an error will be raised """ directive @isSuperuser repeatable on FIELD_DEFINITION """ Will check if the user has any/all permissions for the resolved value of this field before returning it. When the condition fails, the following can be returned (following this priority): 1) `OperationInfo`/`OperationMessage` if those types are allowed at the return type 2) `null` in case the field is not mandatory (e.g. `String` or `[String]`) 3) An empty list in case the field is a list (e.g. `[String]!`) 4) An empty `Connection` in case the return type is a relay connection 2) Otherwise, an error will be raised """ directive @hasRetvalPerm(permissions: [PermDefinition!]!, any: Boolean! = true) repeatable on FIELD_DEFINITION """ Will check if the user has any/all permissions to resolve this. When the condition fails, the following can be returned (following this priority): 1) `OperationInfo`/`OperationMessage` if those types are allowed at the return type 2) `null` in case the field is not mandatory (e.g. `String` or `[String]`) 3) An empty list in case the field is a list (e.g. `[String]!`) 4) An empty `Connection` in case the return type is a relay connection 2) Otherwise, an error will be raised """ directive @hasPerm(permissions: [PermDefinition!]!, any: Boolean! = true) repeatable on FIELD_DEFINITION input AssigneeInputPartial { id: ID throughDefaults: AssigneeThroughInputPartial } """Add/remove/set the selected nodes.""" input AssigneeInputPartialListInput { set: [AssigneeInputPartial!] add: [AssigneeInputPartial!] remove: [AssigneeInputPartial!] } input AssigneeThroughInputPartial { owner: Boolean } type AssigneeType implements Node { """The Globally Unique ID of this object""" id: ID! user: UserType! owner: Boolean! } union CreateIssuePayload = IssueType | OperationInfo union CreateMilestonePayload = MilestoneType | OperationInfo input CreateProjectInput { name: String! """The project's cost""" cost: Decimal! dueDate: DateTime = null } union CreateProjectPayload = ProjectType | OperationInfo union CreateProjectWithMilestonesPayload = ProjectType | OperationInfo input CreateQuizInput { title: String! fullCleanOptions: Boolean! = false } union CreateQuizPayload = QuizType | OperationInfo """Date (isoformat)""" scalar Date input DateDateFilterLookup { """Exact match. Filter will be skipped on `null` value""" exact: Date """Assignment test. Filter will be skipped on `null` value""" isNull: Boolean """ Exact match of items in a given list. Filter will be skipped on `null` value """ inList: [Date!] """Greater than. Filter will be skipped on `null` value""" gt: Date """Greater than or equal to. Filter will be skipped on `null` value""" gte: Date """Less than. Filter will be skipped on `null` value""" lt: Date """Less than or equal to. Filter will be skipped on `null` value""" lte: Date """Inclusive range test (between)""" range: DateRangeLookup year: IntComparisonFilterLookup month: IntComparisonFilterLookup day: IntComparisonFilterLookup weekDay: IntComparisonFilterLookup isoWeekDay: IntComparisonFilterLookup week: IntComparisonFilterLookup isoYear: IntComparisonFilterLookup quarter: IntComparisonFilterLookup } input DateRangeLookup { start: Date = null end: Date = null } """Date with time (isoformat)""" scalar DateTime """Decimal (fixed-point)""" scalar Decimal union DeleteIssuePayload = IssueType | OperationInfo union DeleteIssueWithKeyAttrPayload = IssueType | OperationInfo input DjangoModelFilterInput { pk: ID! } type FavoriteType implements Node { """The Globally Unique ID of this object""" id: ID! name: String! user: UserType! issue: IssueType! } """A connection to a list of items.""" type FavoriteTypeConnection { """Pagination data for this connection""" pageInfo: PageInfo! """Contains the nodes in this connection""" edges: [FavoriteTypeEdge!]! """Total quantity of existing nodes.""" totalCount: Int } """An edge in a connection.""" type FavoriteTypeEdge { """A cursor for use in pagination""" cursor: String! """The item at the end of the edge""" node: FavoriteType! } input IntComparisonFilterLookup { """Exact match. Filter will be skipped on `null` value""" exact: Int """Assignment test. Filter will be skipped on `null` value""" isNull: Boolean """ Exact match of items in a given list. Filter will be skipped on `null` value """ inList: [Int!] """Greater than. Filter will be skipped on `null` value""" gt: Int """Greater than or equal to. Filter will be skipped on `null` value""" gte: Int """Less than. Filter will be skipped on `null` value""" lt: Int """Less than or equal to. Filter will be skipped on `null` value""" lte: Int """Inclusive range test (between)""" range: IntRangeLookup } input IntRangeLookup { start: Int = null end: Int = null } input IssueAssigneeInputPartial { id: ID user: NodeInputPartial owner: Boolean } """Add/remove/set the selected nodes.""" input IssueAssigneeInputPartialListInput { set: [IssueAssigneeInputPartial!] add: [IssueAssigneeInputPartial!] remove: [IssueAssigneeInputPartial!] } input IssueFilter { name: StrFilterLookup AND: IssueFilter OR: IssueFilter NOT: IssueFilter DISTINCT: Boolean search: String } input IssueInput { name: String! milestone: MilestoneInputPartial! priority: Int kind: String tags: [NodeInput!] extra: Int } input IssueInputPartial { id: ID! name: String milestone: MilestoneInputPartial! priority: Int kind: String tags: TagInputPartialListInput extra: Int assignees: AssigneeInputPartialListInput issueAssignees: IssueAssigneeInputPartialListInput } input IssueInputPartialWithoutId { name: String milestone: MilestoneInputPartial! priority: Int kind: String tags: TagInputPartialListInput extra: Int assignees: AssigneeInputPartialListInput issueAssignees: IssueAssigneeInputPartialListInput } input IssueOrder { name: Ordering } type IssueType implements Node & Named { """The Globally Unique ID of this object""" id: ID! name: String! milestone: MilestoneType! priority: Int! kind: String nameWithPriority: String! nameWithKind: String! tags: [TagType!]! issueAssignees: [AssigneeType!]! staffAssignees: [StaffType!]! favoriteSet( """Returns the items in the list that come before the specified cursor.""" before: String = null """Returns the items in the list that come after the specified cursor.""" after: String = null """Returns the first n items from the list.""" first: Int = null """Returns the items in the list that come after the specified cursor.""" last: Int = null ): FavoriteTypeConnection! milestoneName: String! milestoneNameWithoutOnlyOptimization: String! privateName: String } """A connection to a list of items.""" type IssueTypeConnection { """Pagination data for this connection""" pageInfo: PageInfo! """Contains the nodes in this connection""" edges: [IssueTypeEdge!]! """Total quantity of existing nodes.""" totalCount: Int } """An edge in a connection.""" type IssueTypeEdge { """A cursor for use in pagination""" cursor: String! """The item at the end of the edge""" node: IssueType! } type IssueTypeOffsetPaginated { pageInfo: OffsetPaginationInfo! """Total count of existing results.""" totalCount: Int! """List of paginated results.""" results: [IssueType!]! } input MilestoneFilter { name: StrFilterLookup project: DjangoModelFilterInput search: String AND: MilestoneFilter OR: MilestoneFilter NOT: MilestoneFilter DISTINCT: Boolean } input MilestoneInput { name: String! project: ProjectInputPartial! issues: [MilestoneIssueInput!] } input MilestoneInputPartial { id: ID name: String issues: [MilestoneIssueInputPartial!] project: ProjectInputPartial } input MilestoneIssueInput { name: String! } input MilestoneIssueInputPartial { name: String tags: [TagInputPartial!] } input MilestoneOrder { name: Ordering project: ProjectOrder } type MilestoneType implements Node & Named { """The Globally Unique ID of this object""" id: ID! name: String! dueDate: Date project: ProjectType! issues(filters: IssueFilter, order: IssueOrder, pagination: OffsetPaginationInput): [IssueType!]! firstIssue: IssueType firstIssueRequired: IssueType! graphqlPath: String! mixedAnnotatedPrefetch: String! mixedPrefetchAnnotated: String! issuesPaginated(pagination: OffsetPaginationInput, order: IssueOrder): IssueTypeOffsetPaginated! issuesWithFilters( filters: IssueFilter """Returns the items in the list that come before the specified cursor.""" before: String = null """Returns the items in the list that come after the specified cursor.""" after: String = null """Returns the first n items from the list.""" first: Int = null """Returns the items in the list that come after the specified cursor.""" last: Int = null ): IssueTypeConnection! myIssues: [IssueType!]! myBugsCount: Int! asyncField(value: String!): String! } """A connection to a list of items.""" type MilestoneTypeConnection { """Pagination data for this connection""" pageInfo: PageInfo! """Contains the nodes in this connection""" edges: [MilestoneTypeEdge!]! """Total quantity of existing nodes.""" totalCount: Int } """An edge in a connection.""" type MilestoneTypeEdge { """A cursor for use in pagination""" cursor: String! """The item at the end of the edge""" node: MilestoneType! } type MilestoneTypeOffsetPaginated { pageInfo: OffsetPaginationInfo! """Total count of existing results.""" totalCount: Int! """List of paginated results.""" results: [MilestoneType!]! } type Mutation { createIssue(input: IssueInput!): CreateIssuePayload! updateIssue(input: IssueInputPartial!): UpdateIssuePayload! updateIssueWithKeyAttr(input: IssueInputPartialWithoutId!): UpdateIssueWithKeyAttrPayload! deleteIssue(input: NodeInput!): DeleteIssuePayload! deleteIssueWithKeyAttr(input: MilestoneIssueInput!): DeleteIssueWithKeyAttrPayload! createProjectWithMilestones(input: ProjectInputPartial!): CreateProjectWithMilestonesPayload! updateProject(input: ProjectInputPartial!): UpdateProjectPayload! createMilestone(input: MilestoneInput!): CreateMilestonePayload! createProject( """Input data for `createProject` mutation""" input: CreateProjectInput! ): CreateProjectPayload! createQuiz( """Input data for `createQuiz` mutation""" input: CreateQuizInput! ): CreateQuizPayload! } interface Named { name: String! } """An object with a Globally Unique ID""" interface Node { """The Globally Unique ID of this object""" id: ID! } """Input of an object that implements the `Node` interface.""" input NodeInput { id: ID! } """Input of an object that implements the `Node` interface.""" input NodeInputPartial { id: ID } type OffsetPaginationInfo { offset: Int! limit: Int } input OffsetPaginationInput { offset: Int! = 0 limit: Int } type OperationInfo { """List of messages returned by the operation.""" messages: [OperationMessage!]! } type OperationMessage { """The kind of this message.""" kind: OperationMessageKind! """The error message.""" message: String! """ The field that caused the error, or `null` if it isn't associated with any particular field. """ field: String """The error code, or `null` if no error code was set.""" code: String } enum OperationMessageKind { INFO WARNING ERROR PERMISSION VALIDATION } enum Ordering { ASC ASC_NULLS_FIRST ASC_NULLS_LAST DESC DESC_NULLS_FIRST DESC_NULLS_LAST } """Information to aid in pagination.""" type PageInfo { """When paginating forwards, are there more items?""" hasNextPage: Boolean! """When paginating backwards, are there more items?""" hasPreviousPage: Boolean! """When paginating backwards, the cursor to continue.""" startCursor: String """When paginating forwards, the cursor to continue.""" endCursor: String } type ProjectConnection { """Pagination data for this connection""" pageInfo: PageInfo! """Contains the nodes in this connection""" edges: [ProjectTypeEdge!]! """Total quantity of existing nodes.""" totalCount: Int } input ProjectFilter { name: StrFilterLookup dueDate: DateDateFilterLookup AND: ProjectFilter OR: ProjectFilter NOT: ProjectFilter DISTINCT: Boolean } input ProjectInputPartial { id: ID name: String milestones: [MilestoneInputPartial!] } input ProjectOrder { id: Ordering name: Ordering } type ProjectType implements Node & Named { """The Globally Unique ID of this object""" id: ID! name: String! dueDate: Date isSmall: Boolean! isDelayed: Boolean! cost: Decimal @isAuthenticated milestones(filters: MilestoneFilter, order: MilestoneOrder, pagination: OffsetPaginationInput): [MilestoneType!]! milestonesCount: Int! customMilestonesModelProperty: [MilestoneType!]! firstMilestone: MilestoneType firstMilestoneRequired: MilestoneType! milestoneConn( filters: MilestoneFilter order: MilestoneOrder """Returns the items in the list that come before the specified cursor.""" before: String = null """Returns the items in the list that come after the specified cursor.""" after: String = null """Returns the first n items from the list.""" first: Int = null """Returns the items in the list that come after the specified cursor.""" last: Int = null ): MilestoneTypeConnection! milestonesPaginated(pagination: OffsetPaginationInput, filters: MilestoneFilter, order: MilestoneOrder): MilestoneTypeOffsetPaginated! customMilestones: [MilestoneType!]! } """An edge in a connection.""" type ProjectTypeEdge { """A cursor for use in pagination""" cursor: String! """The item at the end of the edge""" node: ProjectType! } type ProjectTypeOffsetPaginated { pageInfo: OffsetPaginationInfo! """Total count of existing results.""" totalCount: Int! """List of paginated results.""" results: [ProjectType!]! } type Query { node( """The ID of the object.""" id: ID! ): Node favorite( """The ID of the object.""" id: ID! ): FavoriteType """Foobar""" issue( """The ID of the object.""" id: ID! ): IssueType milestone( """The ID of the object.""" id: ID! ): MilestoneType milestoneMandatory( """The ID of the object.""" id: ID! ): MilestoneType! milestones( """The IDs of the objects.""" ids: [ID!]! ): [MilestoneType!]! project( """The ID of the object.""" id: ID! ): ProjectType projectMandatory( """The ID of the object.""" id: ID! ): ProjectType! projectLoginRequired( """The ID of the object.""" id: ID! ): ProjectType @isAuthenticated tag( """The ID of the object.""" id: ID! ): TagType staff( """The ID of the object.""" id: ID! ): StaffType staffList( """The IDs of the objects.""" ids: [ID!]! ): [StaffType]! issueList: [IssueType!]! issuesPaginated(pagination: OffsetPaginationInput): IssueTypeOffsetPaginated! milestoneList(filters: MilestoneFilter, order: MilestoneOrder, pagination: OffsetPaginationInput): [MilestoneType!]! projectList(filters: ProjectFilter): [ProjectType!]! projectsPaginated(pagination: OffsetPaginationInput, filters: ProjectFilter): ProjectTypeOffsetPaginated! tagList: [TagType!]! favoriteConn( """Returns the items in the list that come before the specified cursor.""" before: String = null """Returns the items in the list that come after the specified cursor.""" after: String = null """Returns the first n items from the list.""" first: Int = null """Returns the items in the list that come after the specified cursor.""" last: Int = null ): FavoriteTypeConnection! issueConn( """Returns the items in the list that come before the specified cursor.""" before: String = null """Returns the items in the list that come after the specified cursor.""" after: String = null """Returns the first n items from the list.""" first: Int = null """Returns the items in the list that come after the specified cursor.""" last: Int = null ): IssueTypeConnection! milestoneConn( filters: MilestoneFilter order: MilestoneOrder """Returns the items in the list that come before the specified cursor.""" before: String = null """Returns the items in the list that come after the specified cursor.""" after: String = null """Returns the first n items from the list.""" first: Int = null """Returns the items in the list that come after the specified cursor.""" last: Int = null ): MilestoneTypeConnection! projectConn( filters: ProjectFilter """Returns the items in the list that come before the specified cursor.""" before: String = null """Returns the items in the list that come after the specified cursor.""" after: String = null """Returns the first n items from the list.""" first: Int = null """Returns the items in the list that come after the specified cursor.""" last: Int = null ): ProjectConnection! tagConn( """Returns the items in the list that come before the specified cursor.""" before: String = null """Returns the items in the list that come after the specified cursor.""" after: String = null """Returns the first n items from the list.""" first: Int = null """Returns the items in the list that come after the specified cursor.""" last: Int = null ): TagTypeConnection! staffConn( """Returns the items in the list that come before the specified cursor.""" before: String = null """Returns the items in the list that come after the specified cursor.""" after: String = null """Returns the first n items from the list.""" first: Int = null """Returns the items in the list that come after the specified cursor.""" last: Int = null ): StaffTypeConnection! quizList: [QuizType!]! issueLoginRequired( """The ID of the object.""" id: ID! ): IssueType! @isAuthenticated issueLoginRequiredOptional( """The ID of the object.""" id: ID! ): IssueType @isAuthenticated issueStaffRequired( """The ID of the object.""" id: ID! ): IssueType! @isStaff issueStaffRequiredOptional( """The ID of the object.""" id: ID! ): IssueType @isStaff issueSuperuserRequired( """The ID of the object.""" id: ID! ): IssueType! @isSuperuser issueSuperuserRequiredOptional( """The ID of the object.""" id: ID! ): IssueType @isSuperuser issuePermRequired( """The ID of the object.""" id: ID! ): IssueType! @hasPerm(permissions: [{app: "projects", permission: "view_issue"}], any: true) issuePermRequiredOptional( """The ID of the object.""" id: ID! ): IssueType @hasPerm(permissions: [{app: "projects", permission: "view_issue"}], any: true) issueListPermRequired: [IssueType!]! @hasPerm(permissions: [{app: "projects", permission: "view_issue"}], any: true) issuesPaginatedPermRequired(pagination: OffsetPaginationInput): IssueTypeOffsetPaginated! @hasPerm(permissions: [{app: "projects", permission: "view_issue"}], any: true) issueConnPermRequired( """Returns the items in the list that come before the specified cursor.""" before: String = null """Returns the items in the list that come after the specified cursor.""" after: String = null """Returns the first n items from the list.""" first: Int = null """Returns the items in the list that come after the specified cursor.""" last: Int = null ): IssueTypeConnection! @hasPerm(permissions: [{app: "projects", permission: "view_issue"}], any: true) issueObjPermRequired( """The ID of the object.""" id: ID! ): IssueType! @hasRetvalPerm(permissions: [{app: "projects", permission: "view_issue"}], any: true) issueObjPermRequiredOptional( """The ID of the object.""" id: ID! ): IssueType @hasRetvalPerm(permissions: [{app: "projects", permission: "view_issue"}], any: true) issueListObjPermRequired: [IssueType!]! @hasRetvalPerm(permissions: [{app: "projects", permission: "view_issue"}], any: true) issueListObjPermRequiredPaginated(pagination: OffsetPaginationInput): [IssueType!]! @hasRetvalPerm(permissions: [{app: "projects", permission: "view_issue"}], any: true) issuesPaginatedObjPermRequired(pagination: OffsetPaginationInput): IssueTypeOffsetPaginated! @hasRetvalPerm(permissions: [{app: "projects", permission: "view_issue"}], any: true) issueConnObjPermRequired( """Returns the items in the list that come before the specified cursor.""" before: String = null """Returns the items in the list that come after the specified cursor.""" after: String = null """Returns the first n items from the list.""" first: Int = null """Returns the items in the list that come after the specified cursor.""" last: Int = null ): IssueTypeConnection! @hasRetvalPerm(permissions: [{app: "projects", permission: "view_issue"}], any: true) asyncUserResolve: Boolean! @hasPerm(permissions: [{app: "projects", permission: "view_issue"}], any: true) me: UserType projectConnWithResolver( name: String! filters: ProjectFilter """Returns the items in the list that come before the specified cursor.""" before: String = null """Returns the items in the list that come after the specified cursor.""" after: String = null """Returns the first n items from the list.""" first: Int = null """Returns the items in the list that come after the specified cursor.""" last: Int = null ): ProjectConnection! } type QuizType implements Node { """The Globally Unique ID of this object""" id: ID! title: String! sequence: Int! } type StaffType implements Node { """The Globally Unique ID of this object""" id: ID! email: String! isActive: Boolean! isSuperuser: Boolean! isStaff: Boolean! } """A connection to a list of items.""" type StaffTypeConnection { """Pagination data for this connection""" pageInfo: PageInfo! """Contains the nodes in this connection""" edges: [StaffTypeEdge!]! """Total quantity of existing nodes.""" totalCount: Int } """An edge in a connection.""" type StaffTypeEdge { """A cursor for use in pagination""" cursor: String! """The item at the end of the edge""" node: StaffType! } input StrFilterLookup { """Exact match. Filter will be skipped on `null` value""" exact: String """Assignment test. Filter will be skipped on `null` value""" isNull: Boolean """ Exact match of items in a given list. Filter will be skipped on `null` value """ inList: [String!] """Case-insensitive exact match. Filter will be skipped on `null` value""" iExact: String """ Case-sensitive containment test. Filter will be skipped on `null` value """ contains: String """ Case-insensitive containment test. Filter will be skipped on `null` value """ iContains: String """Case-sensitive starts-with. Filter will be skipped on `null` value""" startsWith: String """Case-insensitive starts-with. Filter will be skipped on `null` value""" iStartsWith: String """Case-sensitive ends-with. Filter will be skipped on `null` value""" endsWith: String """Case-insensitive ends-with. Filter will be skipped on `null` value""" iEndsWith: String """ Case-sensitive regular expression match. Filter will be skipped on `null` value """ regex: String """ Case-insensitive regular expression match. Filter will be skipped on `null` value """ iRegex: String } input TagInputPartial { id: ID name: String } """Add/remove/set the selected nodes.""" input TagInputPartialListInput { set: [TagInputPartial!] add: [TagInputPartial!] remove: [TagInputPartial!] } type TagType implements Node & Named { """The Globally Unique ID of this object""" id: ID! name: String! issues( """Returns the items in the list that come before the specified cursor.""" before: String = null """Returns the items in the list that come after the specified cursor.""" after: String = null """Returns the first n items from the list.""" first: Int = null """Returns the items in the list that come after the specified cursor.""" last: Int = null ): IssueTypeConnection! issuesWithSelectedRelatedMilestoneAndProject: [IssueType!]! } """A connection to a list of items.""" type TagTypeConnection { """Pagination data for this connection""" pageInfo: PageInfo! """Contains the nodes in this connection""" edges: [TagTypeEdge!]! """Total quantity of existing nodes.""" totalCount: Int } """An edge in a connection.""" type TagTypeEdge { """A cursor for use in pagination""" cursor: String! """The item at the end of the edge""" node: TagType! } union UpdateIssuePayload = IssueType | OperationInfo union UpdateIssueWithKeyAttrPayload = IssueType | OperationInfo union UpdateProjectPayload = ProjectType | OperationInfo type UserType implements Node { """The Globally Unique ID of this object""" id: ID! email: String! isActive: Boolean! isSuperuser: Boolean! isStaff: Boolean! fullName: String! } """Permission definition for schema directives.""" input PermDefinition { """ The app to which we are requiring permission. If this is empty that means that we are checking the permission directly. """ app: String """ The permission itself. If this is empty that means that we are checking for any permission for the given app. """ permission: String }strawberry-graphql-django-0.62.0/tests/projects/snapshots/schema_with_inheritance.gql000066400000000000000000000307421502405145400312730ustar00rootroot00000000000000""" Can only be resolved by authenticated users. When the condition fails, the following can be returned (following this priority): 1) `OperationInfo`/`OperationMessage` if those types are allowed at the return type 2) `null` in case the field is not mandatory (e.g. `String` or `[String]`) 3) An empty list in case the field is a list (e.g. `[String]!`) 4) An empty `Connection` in case the return type is a relay connection 2) Otherwise, an error will be raised """ directive @isAuthenticated repeatable on FIELD_DEFINITION type AssigneeType implements Node { """The Globally Unique ID of this object""" id: ID! user: UserType! owner: Boolean! } union CreateIssuePayload = IssueType | OperationInfo """Date (isoformat)""" scalar Date """Decimal (fixed-point)""" scalar Decimal input DjangoModelFilterInput { pk: ID! } type FavoriteType implements Node { """The Globally Unique ID of this object""" id: ID! name: String! user: UserType! issue: IssueType! } """A connection to a list of items.""" type FavoriteTypeConnection { """Pagination data for this connection""" pageInfo: PageInfo! """Contains the nodes in this connection""" edges: [FavoriteTypeEdge!]! """Total quantity of existing nodes.""" totalCount: Int } """An edge in a connection.""" type FavoriteTypeEdge { """A cursor for use in pagination""" cursor: String! """The item at the end of the edge""" node: FavoriteType! } input IssueFilter { name: StrFilterLookup AND: IssueFilter OR: IssueFilter NOT: IssueFilter DISTINCT: Boolean search: String } input IssueInputSubclass { name: String! milestone: MilestoneInputPartial! priority: Int kind: String tags: [NodeInput!] extra: Int } input IssueOrder { name: Ordering } type IssueType implements Node & Named { """The Globally Unique ID of this object""" id: ID! name: String! milestone: MilestoneType! priority: Int! kind: String nameWithPriority: String! nameWithKind: String! tags: [TagType!]! issueAssignees: [AssigneeType!]! staffAssignees: [StaffType!]! favoriteSet( """Returns the items in the list that come before the specified cursor.""" before: String = null """Returns the items in the list that come after the specified cursor.""" after: String = null """Returns the first n items from the list.""" first: Int = null """Returns the items in the list that come after the specified cursor.""" last: Int = null ): FavoriteTypeConnection! milestoneName: String! milestoneNameWithoutOnlyOptimization: String! privateName: String } """A connection to a list of items.""" type IssueTypeConnection { """Pagination data for this connection""" pageInfo: PageInfo! """Contains the nodes in this connection""" edges: [IssueTypeEdge!]! """Total quantity of existing nodes.""" totalCount: Int } """An edge in a connection.""" type IssueTypeEdge { """A cursor for use in pagination""" cursor: String! """The item at the end of the edge""" node: IssueType! } type IssueTypeOffsetPaginated { pageInfo: OffsetPaginationInfo! """Total count of existing results.""" totalCount: Int! """List of paginated results.""" results: [IssueType!]! } input MilestoneFilter { name: StrFilterLookup project: DjangoModelFilterInput search: String AND: MilestoneFilter OR: MilestoneFilter NOT: MilestoneFilter DISTINCT: Boolean } input MilestoneInputPartial { id: ID name: String issues: [MilestoneIssueInputPartial!] project: ProjectInputPartial } input MilestoneIssueInputPartial { name: String tags: [TagInputPartial!] } input MilestoneOrder { name: Ordering project: ProjectOrder } type MilestoneType implements Node & Named { """The Globally Unique ID of this object""" id: ID! name: String! dueDate: Date project: ProjectType! issues(filters: IssueFilter, order: IssueOrder, pagination: OffsetPaginationInput): [IssueType!]! firstIssue: IssueType firstIssueRequired: IssueType! graphqlPath: String! mixedAnnotatedPrefetch: String! mixedPrefetchAnnotated: String! issuesPaginated(pagination: OffsetPaginationInput, order: IssueOrder): IssueTypeOffsetPaginated! issuesWithFilters( filters: IssueFilter """Returns the items in the list that come before the specified cursor.""" before: String = null """Returns the items in the list that come after the specified cursor.""" after: String = null """Returns the first n items from the list.""" first: Int = null """Returns the items in the list that come after the specified cursor.""" last: Int = null ): IssueTypeConnection! myIssues: [IssueType!]! myBugsCount: Int! asyncField(value: String!): String! } """A connection to a list of items.""" type MilestoneTypeConnection { """Pagination data for this connection""" pageInfo: PageInfo! """Contains the nodes in this connection""" edges: [MilestoneTypeEdge!]! """Total quantity of existing nodes.""" totalCount: Int } """An edge in a connection.""" type MilestoneTypeEdge { """A cursor for use in pagination""" cursor: String! """The item at the end of the edge""" node: MilestoneType! } type MilestoneTypeOffsetPaginated { pageInfo: OffsetPaginationInfo! """Total count of existing results.""" totalCount: Int! """List of paginated results.""" results: [MilestoneType!]! } type MilestoneTypeSubclass implements Node & Named { """The Globally Unique ID of this object""" id: ID! name: String! dueDate: Date project: ProjectType! issues(filters: IssueFilter, order: IssueOrder, pagination: OffsetPaginationInput): [IssueType!]! firstIssue: IssueType firstIssueRequired: IssueType! graphqlPath: String! mixedAnnotatedPrefetch: String! mixedPrefetchAnnotated: String! issuesPaginated(pagination: OffsetPaginationInput, order: IssueOrder): IssueTypeOffsetPaginated! issuesWithFilters( filters: IssueFilter """Returns the items in the list that come before the specified cursor.""" before: String = null """Returns the items in the list that come after the specified cursor.""" after: String = null """Returns the first n items from the list.""" first: Int = null """Returns the items in the list that come after the specified cursor.""" last: Int = null ): IssueTypeConnection! myIssues: [IssueType!]! myBugsCount: Int! asyncField(value: String!): String! } type Mutation { createIssue(input: IssueInputSubclass!): CreateIssuePayload! } interface Named { name: String! } """An object with a Globally Unique ID""" interface Node { """The Globally Unique ID of this object""" id: ID! } """Input of an object that implements the `Node` interface.""" input NodeInput { id: ID! } type OffsetPaginationInfo { offset: Int! limit: Int } input OffsetPaginationInput { offset: Int! = 0 limit: Int } type OperationInfo { """List of messages returned by the operation.""" messages: [OperationMessage!]! } type OperationMessage { """The kind of this message.""" kind: OperationMessageKind! """The error message.""" message: String! """ The field that caused the error, or `null` if it isn't associated with any particular field. """ field: String """The error code, or `null` if no error code was set.""" code: String } enum OperationMessageKind { INFO WARNING ERROR PERMISSION VALIDATION } enum Ordering { ASC ASC_NULLS_FIRST ASC_NULLS_LAST DESC DESC_NULLS_FIRST DESC_NULLS_LAST } """Information to aid in pagination.""" type PageInfo { """When paginating forwards, are there more items?""" hasNextPage: Boolean! """When paginating backwards, are there more items?""" hasPreviousPage: Boolean! """When paginating backwards, the cursor to continue.""" startCursor: String """When paginating forwards, the cursor to continue.""" endCursor: String } input ProjectInputPartial { id: ID name: String milestones: [MilestoneInputPartial!] } input ProjectOrder { id: Ordering name: Ordering } type ProjectType implements Node & Named { """The Globally Unique ID of this object""" id: ID! name: String! dueDate: Date isSmall: Boolean! isDelayed: Boolean! cost: Decimal @isAuthenticated milestones(filters: MilestoneFilter, order: MilestoneOrder, pagination: OffsetPaginationInput): [MilestoneType!]! milestonesCount: Int! customMilestonesModelProperty: [MilestoneType!]! firstMilestone: MilestoneType firstMilestoneRequired: MilestoneType! milestoneConn( filters: MilestoneFilter order: MilestoneOrder """Returns the items in the list that come before the specified cursor.""" before: String = null """Returns the items in the list that come after the specified cursor.""" after: String = null """Returns the first n items from the list.""" first: Int = null """Returns the items in the list that come after the specified cursor.""" last: Int = null ): MilestoneTypeConnection! milestonesPaginated(pagination: OffsetPaginationInput, filters: MilestoneFilter, order: MilestoneOrder): MilestoneTypeOffsetPaginated! customMilestones: [MilestoneType!]! } type ProjectTypeSubclass implements Node & Named { """The Globally Unique ID of this object""" id: ID! name: String! dueDate: Date isSmall: Boolean! isDelayed: Boolean! cost: Decimal @isAuthenticated milestones(filters: MilestoneFilter, order: MilestoneOrder, pagination: OffsetPaginationInput): [MilestoneType!]! milestonesCount: Int! customMilestonesModelProperty: [MilestoneType!]! firstMilestone: MilestoneType firstMilestoneRequired: MilestoneType! milestoneConn( filters: MilestoneFilter order: MilestoneOrder """Returns the items in the list that come before the specified cursor.""" before: String = null """Returns the items in the list that come after the specified cursor.""" after: String = null """Returns the first n items from the list.""" first: Int = null """Returns the items in the list that come after the specified cursor.""" last: Int = null ): MilestoneTypeConnection! milestonesPaginated(pagination: OffsetPaginationInput, filters: MilestoneFilter, order: MilestoneOrder): MilestoneTypeOffsetPaginated! customMilestones: [MilestoneType!]! } type Query { project( """The ID of the object.""" id: ID! ): ProjectTypeSubclass milestone( """The ID of the object.""" id: ID! ): MilestoneTypeSubclass } type StaffType implements Node { """The Globally Unique ID of this object""" id: ID! email: String! isActive: Boolean! isSuperuser: Boolean! isStaff: Boolean! } input StrFilterLookup { """Exact match. Filter will be skipped on `null` value""" exact: String """Assignment test. Filter will be skipped on `null` value""" isNull: Boolean """ Exact match of items in a given list. Filter will be skipped on `null` value """ inList: [String!] """Case-insensitive exact match. Filter will be skipped on `null` value""" iExact: String """ Case-sensitive containment test. Filter will be skipped on `null` value """ contains: String """ Case-insensitive containment test. Filter will be skipped on `null` value """ iContains: String """Case-sensitive starts-with. Filter will be skipped on `null` value""" startsWith: String """Case-insensitive starts-with. Filter will be skipped on `null` value""" iStartsWith: String """Case-sensitive ends-with. Filter will be skipped on `null` value""" endsWith: String """Case-insensitive ends-with. Filter will be skipped on `null` value""" iEndsWith: String """ Case-sensitive regular expression match. Filter will be skipped on `null` value """ regex: String """ Case-insensitive regular expression match. Filter will be skipped on `null` value """ iRegex: String } input TagInputPartial { id: ID name: String } type TagType implements Node & Named { """The Globally Unique ID of this object""" id: ID! name: String! issues( """Returns the items in the list that come before the specified cursor.""" before: String = null """Returns the items in the list that come after the specified cursor.""" after: String = null """Returns the first n items from the list.""" first: Int = null """Returns the items in the list that come after the specified cursor.""" last: Int = null ): IssueTypeConnection! issuesWithSelectedRelatedMilestoneAndProject: [IssueType!]! } type UserType implements Node { """The Globally Unique ID of this object""" id: ID! email: String! isActive: Boolean! isSuperuser: Boolean! isStaff: Boolean! fullName: String! }strawberry-graphql-django-0.62.0/tests/projects/test_schema.py000066400000000000000000000025511502405145400245460ustar00rootroot00000000000000import pathlib from typing import Optional import strawberry from pytest_snapshot.plugin import Snapshot import strawberry_django from strawberry_django import mutations from .models import Issue, Milestone, Project from .schema import IssueInput, IssueType, MilestoneType, ProjectType, schema SNAPSHOTS_DIR = pathlib.Path(__file__).parent / "snapshots" def test_schema(snapshot: Snapshot): snapshot.snapshot_dir = SNAPSHOTS_DIR snapshot.assert_match(str(schema), "schema.gql") def test_schema_with_inheritance(snapshot: Snapshot): @strawberry_django.type(Project) class ProjectTypeSubclass(ProjectType): ... @strawberry_django.type(Milestone) class MilestoneTypeSubclass(MilestoneType): ... @strawberry_django.input(Issue) class IssueInputSubclass(IssueInput): ... @strawberry.type class Query: project: Optional[ProjectTypeSubclass] = strawberry_django.node() milestone: Optional[MilestoneTypeSubclass] = strawberry_django.node() @strawberry.type class Mutation: create_issue: IssueType = mutations.create( IssueInputSubclass, handle_django_errors=True, argument_name="input", ) schema = strawberry.Schema(query=Query, mutation=Mutation) snapshot.snapshot_dir = SNAPSHOTS_DIR snapshot.assert_match(str(schema), "schema_with_inheritance.gql") strawberry-graphql-django-0.62.0/tests/queries/000077500000000000000000000000001502405145400215165ustar00rootroot00000000000000strawberry-graphql-django-0.62.0/tests/queries/__init__.py000066400000000000000000000000001502405145400236150ustar00rootroot00000000000000strawberry-graphql-django-0.62.0/tests/queries/conftest.py000066400000000000000000000002051502405145400237120ustar00rootroot00000000000000import pytest @pytest.fixture def query(schema): def query(query): return schema.execute_sync(query) return query strawberry-graphql-django-0.62.0/tests/queries/test_async.py000066400000000000000000000014161502405145400242460ustar00rootroot00000000000000import pytest pytestmark = pytest.mark.asyncio @pytest.fixture def query(schema): async def query(query): return await schema.execute(query) return query @pytest.mark.django_db(transaction=True) async def test_query(query, user, group, tag): result = await query("{ users { id name group { id name tags { id name } } } }") assert not result.errors assert result.data["users"] == [ { "id": str(user.id), "name": "user", "group": { "id": str(group.id), "name": "group", "tags": [ { "id": str(tag.id), "name": "tag", }, ], }, }, ] strawberry-graphql-django-0.62.0/tests/queries/test_fields.py000066400000000000000000000155421502405145400244040ustar00rootroot00000000000000import textwrap from typing import Optional, cast import pytest import strawberry from django.conf import settings from strawberry.types import ExecutionResult import strawberry_django from tests import models, types, utils def generate_query(user_type): @strawberry.type class Query: users: list[user_type] = strawberry_django.field() # type: ignore return utils.generate_query(Query) def test_field_name(user): @strawberry_django.type(models.User) class MyUser: my_name: str = strawberry_django.field(field_name="name") query = generate_query(MyUser) result = query("{ users { myName } }") assert isinstance(result, ExecutionResult) assert not result.errors assert result.data is not None assert result.data["users"] == [{"myName": "user"}] def test_relational_field_name(user, group): @strawberry_django.type(models.User) class MyUser: my_group: types.Group = strawberry_django.field(field_name="group") query = generate_query(MyUser) result = query("{ users { myGroup { name } } }") assert isinstance(result, ExecutionResult) assert not result.errors assert result.data is not None assert result.data["users"] == [{"myGroup": {"name": "group"}}] def test_foreign_key_id_with_auto(group, user): @strawberry_django.type(models.User) class MyUser: group_id: strawberry.auto @strawberry.type class Query: users: list[MyUser] = strawberry_django.field() schema = strawberry.Schema(query=Query) expected = """\ type MyUser { groupId: ID } type Query { users: [MyUser!]! } """ assert textwrap.dedent(str(schema)).strip() == textwrap.dedent(expected).strip() result = schema.execute_sync("{ users { groupId } }") assert isinstance(result, ExecutionResult) assert not result.errors assert result.data is not None assert result.data["users"] == [{"groupId": str(group.id)}] def test_foreign_key_id_with_explicit_type(group, user): @strawberry_django.type(models.User) class MyUser: group_id: Optional[strawberry.ID] @strawberry.type class Query: users: list[MyUser] = strawberry_django.field() schema = strawberry.Schema(query=Query) expected = """\ type MyUser { groupId: ID } type Query { users: [MyUser!]! } """ assert textwrap.dedent(str(schema)).strip() == textwrap.dedent(expected).strip() result = schema.execute_sync("{ users { groupId } }") assert isinstance(result, ExecutionResult) assert not result.errors assert result.data is not None assert result.data["users"] == [{"groupId": str(group.id)}] @pytest.mark.asyncio @pytest.mark.django_db(transaction=True) async def test_sync_resolver(user, group): @strawberry_django.type(models.User) class MyUser: @strawberry_django.field def my_group(self, info) -> types.Group: return cast("types.Group", models.Group.objects.get()) query = generate_query(MyUser) result = await query("{ users { myGroup { name } } }") # type: ignore assert isinstance(result, ExecutionResult) assert not result.errors assert result.data is not None assert result.data["users"] == [{"myGroup": {"name": "group"}}] @pytest.mark.asyncio @pytest.mark.django_db(transaction=True) async def test_async_resolver(user, group): @strawberry_django.type(models.User) class MyUser: @strawberry_django.field async def my_group(self, info) -> types.Group: from asgiref.sync import sync_to_async return cast("types.Group", await sync_to_async(models.Group.objects.get)()) query = generate_query(MyUser) result = await query("{ users { myGroup { name } } }") # type: ignore assert isinstance(result, ExecutionResult) assert not result.errors assert result.data is not None assert result.data["users"] == [{"myGroup": {"name": "group"}}] @pytest.mark.skipif( not settings.GEOS_IMPORTED, reason="Test requires GEOS to be imported and properly configured", ) @pytest.mark.django_db(transaction=True) def test_geo_data(query, geofields): # Test for point result = query("{ geofields { point } }") assert not result.errors assert result.data["geofields"] == [ {"point": (0.0, 0.0)}, {"point": (1.0, 1.0)}, {"point": None}, ] # Test for lineString result = query("{ geofields { lineString } }") assert not result.errors assert result.data["geofields"] == [ {"lineString": ((0.0, 0.0), (1.0, 1.0))}, {"lineString": ((1.0, 1.0), (2.0, 2.0), (3.0, 3.0))}, {"lineString": None}, ] # Test for polygon result = query("{ geofields { polygon } }") assert not result.errors assert result.data["geofields"] == [ { "polygon": ( ((-1.0, -1.0), (-1.0, 1.0), (1.0, 1.0), (1.0, -1.0), (-1.0, -1.0)), ), }, { "polygon": ( ((-1.0, -1.0), (-1.0, 1.0), (1.0, 1.0), (1.0, -1.0), (-1.0, -1.0)), ((-2.0, -2.0), (-2.0, 2.0), (2.0, 2.0), (2.0, -2.0), (-2.0, -2.0)), ), }, {"polygon": None}, ] # Test for multiPoint result = query("{ geofields { multiPoint } }") assert not result.errors assert result.data["geofields"] == [ {"multiPoint": ((0.0, 0.0), (1.0, 1.0))}, {"multiPoint": ((0.0, 0.0), (-1.0, -1.0), (1.0, 1.0))}, {"multiPoint": None}, ] # Test for multiLineString result = query("{ geofields { multiLineString } }") assert not result.errors assert result.data["geofields"] == [ {"multiLineString": (((0.0, 0.0), (1.0, 1.0)), ((1.0, 1.0), (-1.0, -1.0)))}, { "multiLineString": ( ((0.0, 0.0), (1.0, 1.0)), ((1.0, 1.0), (-1.0, -1.0)), ((2.0, 2.0), (-2.0, -2.0)), ), }, {"multiLineString": None}, ] # Test for multiPolygon result = query("{ geofields { multiPolygon } }") assert not result.errors assert result.data["geofields"] == [ { "multiPolygon": ( ((((-1.0, -1.0), (-1.0, 1.0), (1.0, 1.0), (1.0, -1.0), (-1.0, -1.0))),), ((((-1.0, -1.0), (-1.0, 1.0), (1.0, 1.0), (1.0, -1.0), (-1.0, -1.0))),), ), }, { "multiPolygon": ( ( ((-1.0, -1.0), (-1.0, 1.0), (1.0, 1.0), (1.0, -1.0), (-1.0, -1.0)), ((-2.0, -2.0), (-2.0, 2.0), (2.0, 2.0), (2.0, -2.0), (-2.0, -2.0)), ), ( ((-1.0, -1.0), (-1.0, 1.0), (1.0, 1.0), (1.0, -1.0), (-1.0, -1.0)), ((-2.0, -2.0), (-2.0, 2.0), (2.0, 2.0), (2.0, -2.0), (-2.0, -2.0)), ), ), }, {"multiPolygon": None}, ] strawberry-graphql-django-0.62.0/tests/queries/test_files.py000066400000000000000000000026711502405145400242370ustar00rootroot00000000000000import pytest import strawberry from django.db import models from strawberry import auto import strawberry_django from tests import utils class FileModel(models.Model): file = models.FileField() image = models.ImageField() @strawberry_django.type(FileModel) class File: file: auto image: auto @strawberry.type class Query: files: list[File] = strawberry_django.field() @pytest.fixture def query(db): return utils.generate_query(Query) @pytest.fixture def instance(mocker): mocker.patch( "django.core.files.images.ImageFile._get_image_dimensions", ).return_value = [ 800, 600, ] mocker.patch("os.stat")().st_size = 10 return FileModel.objects.create(file="file", image="image") def test_file(query, instance): result = query("{ files { file { name size url } } }") assert not result.errors assert result.data["files"] == [ { "file": { "name": "file", "size": 10, "url": "/file", }, }, ] def test_image(query, instance): result = query("{ files { image { name size url width height } } }") assert not result.errors assert result.data["files"] == [ { "image": { "name": "image", "size": 10, "url": "/image", "width": 800, "height": 600, }, }, ] strawberry-graphql-django-0.62.0/tests/queries/test_m2m_through.py000066400000000000000000000032631502405145400253660ustar00rootroot00000000000000from typing import Optional import strawberry from django.db import models from strawberry import auto import strawberry_django class MemberModel(models.Model): name = models.CharField(max_length=50) class ProjectModel(models.Model): name = models.CharField(max_length=50) members = models.ManyToManyField(MemberModel, through="MembershipModel") class MembershipModel(models.Model): project = models.ForeignKey(ProjectModel, on_delete=models.CASCADE) member = models.ForeignKey(MemberModel, on_delete=models.CASCADE) @strawberry_django.type(ProjectModel) class Project: name: auto membership: list["Membership"] = strawberry_django.field( field_name="membershipmodel_set", ) @strawberry_django.type(MemberModel) class Member: name: auto membership: list["Membership"] = strawberry_django.field( field_name="membershipmodel_set", ) @strawberry_django.type(MembershipModel) class Membership: project: Project member: Member @strawberry.type class Query: projects: Optional[list[Project]] = strawberry_django.field() schema = strawberry.Schema(query=Query) def test_query(db): project = ProjectModel.objects.create(name="my project") member = MemberModel.objects.create(name="my member") MembershipModel.objects.create(project=project, member=member) result = schema.execute_sync("{ projects { membership { member { name } } } }") assert not result.errors assert result.data == { "projects": [ { "membership": [ { "member": {"name": "my member"}, }, ], }, ], } strawberry-graphql-django-0.62.0/tests/queries/test_relations.py000066400000000000000000000036131502405145400251320ustar00rootroot00000000000000def test_foreign_key_relation(query, user, group): result = query("{ users { name group { name } } }") assert not result.errors assert result.data["users"] == [ { "name": "user", "group": { "name": "group", }, }, ] def test_foreign_key_relation_reversed(query, user, group): result = query("{ groups { name users { name } } }") assert not result.errors assert result.data["groups"] == [ { "name": "group", "users": [ { "name": "user", }, ], }, ] def test_one_to_one_relation(query, user, tag): result = query("{ users { name tag { name } } }") assert not result.errors assert result.data["users"] == [ { "name": "user", "tag": { "name": "tag", }, }, ] def test_one_to_one_relation_reversed(query, user, tag): result = query("{ tags { name user { name } } }") assert not result.errors assert result.data["tags"] == [ { "name": "tag", "user": { "name": "user", }, }, ] def test_many_to_many_relation(query, group, tag): result = query("{ groups { name tags { name } } }") assert not result.errors assert result.data["groups"] == [ { "name": "group", "tags": [ { "name": "tag", }, ], }, ] def test_many_to_many_relation_reversed(query, group): result = query("{ tags { name groups { name } } }") assert not result.errors assert result.data["tags"] == [ { "name": "tag", "groups": [ { "name": "group", }, ], }, ] strawberry-graphql-django-0.62.0/tests/queries/test_sync.py000066400000000000000000000010511502405145400241000ustar00rootroot00000000000000def test_query(query, user, group, tag): result = query("{ users { id name group { id name tags { id name } } } }") assert not result.errors assert result.data["users"] == [ { "id": str(user.id), "name": "user", "group": { "id": str(group.id), "name": "group", "tags": [ { "id": str(tag.id), "name": "tag", }, ], }, }, ] strawberry-graphql-django-0.62.0/tests/relay/000077500000000000000000000000001502405145400211555ustar00rootroot00000000000000strawberry-graphql-django-0.62.0/tests/relay/__init__.py000066400000000000000000000000001502405145400232540ustar00rootroot00000000000000strawberry-graphql-django-0.62.0/tests/relay/lazy/000077500000000000000000000000001502405145400221345ustar00rootroot00000000000000strawberry-graphql-django-0.62.0/tests/relay/lazy/__init__.py000066400000000000000000000000001502405145400242330ustar00rootroot00000000000000strawberry-graphql-django-0.62.0/tests/relay/lazy/a.py000066400000000000000000000011001502405145400227160ustar00rootroot00000000000000from typing import TYPE_CHECKING, Annotated import strawberry from strawberry import relay from typing_extensions import TypeAlias import strawberry_django from strawberry_django.relay import DjangoListConnection from .models import RelayAuthor if TYPE_CHECKING: from .b import BookConnection @strawberry_django.type(RelayAuthor) class AuthorType(relay.Node): name: str books: Annotated["BookConnection", strawberry.lazy("tests.relay.lazy.b")] = ( strawberry_django.connection() ) AuthorConnection: TypeAlias = DjangoListConnection[AuthorType] strawberry-graphql-django-0.62.0/tests/relay/lazy/b.py000066400000000000000000000012641502405145400227320ustar00rootroot00000000000000from typing import TYPE_CHECKING, Annotated import strawberry from strawberry import relay from typing_extensions import TypeAlias import strawberry_django from strawberry_django.relay import DjangoListConnection from .models import RelayBook if TYPE_CHECKING: from .a import AuthorType @strawberry_django.filter_type(RelayBook) class BookFilter: name: str @strawberry_django.order(RelayBook) class BookOrder: name: str @strawberry_django.type(RelayBook, filters=BookFilter, order=BookOrder) class BookType(relay.Node): name: str author: Annotated["AuthorType", strawberry.lazy("tests.relay.lazy.a")] BookConnection: TypeAlias = DjangoListConnection[BookType] strawberry-graphql-django-0.62.0/tests/relay/lazy/models.py000066400000000000000000000004651502405145400237760ustar00rootroot00000000000000from django.db import models class RelayAuthor(models.Model): name = models.CharField(max_length=100) class RelayBook(models.Model): title = models.CharField(max_length=100) author = models.ForeignKey( RelayAuthor, on_delete=models.CASCADE, related_name="books", ) strawberry-graphql-django-0.62.0/tests/relay/lazy/snapshots/000077500000000000000000000000001502405145400241565ustar00rootroot00000000000000strawberry-graphql-django-0.62.0/tests/relay/lazy/snapshots/test_lazy_annotations/000077500000000000000000000000001502405145400306115ustar00rootroot00000000000000test_lazy_type_annotations_in_schema/000077500000000000000000000000001502405145400402345ustar00rootroot00000000000000strawberry-graphql-django-0.62.0/tests/relay/lazy/snapshots/test_lazy_annotationsauthors_and_books_schema.gql000066400000000000000000000077041502405145400457750ustar00rootroot00000000000000strawberry-graphql-django-0.62.0/tests/relay/lazy/snapshots/test_lazy_annotations/test_lazy_type_annotations_in_schematype AuthorType implements Node { """The Globally Unique ID of this object""" id: ID! name: String! books( filters: BookFilter order: BookOrder """Returns the items in the list that come before the specified cursor.""" before: String = null """Returns the items in the list that come after the specified cursor.""" after: String = null """Returns the first n items from the list.""" first: Int = null """Returns the items in the list that come after the specified cursor.""" last: Int = null ): BookTypeConnection! } """A connection to a list of items.""" type AuthorTypeConnection { """Pagination data for this connection""" pageInfo: PageInfo! """Contains the nodes in this connection""" edges: [AuthorTypeEdge!]! """Total quantity of existing nodes.""" totalCount: Int } """An edge in a connection.""" type AuthorTypeEdge { """A cursor for use in pagination""" cursor: String! """The item at the end of the edge""" node: AuthorType! } input BookFilter { name: String! AND: BookFilter OR: BookFilter NOT: BookFilter DISTINCT: Boolean } input BookOrder { name: String } type BookType implements Node { """The Globally Unique ID of this object""" id: ID! name: String! author: AuthorType! } """A connection to a list of items.""" type BookTypeConnection { """Pagination data for this connection""" pageInfo: PageInfo! """Contains the nodes in this connection""" edges: [BookTypeEdge!]! """Total quantity of existing nodes.""" totalCount: Int } """An edge in a connection.""" type BookTypeEdge { """A cursor for use in pagination""" cursor: String! """The item at the end of the edge""" node: BookType! } """An object with a Globally Unique ID""" interface Node { """The Globally Unique ID of this object""" id: ID! } """Information to aid in pagination.""" type PageInfo { """When paginating forwards, are there more items?""" hasNextPage: Boolean! """When paginating backwards, are there more items?""" hasPreviousPage: Boolean! """When paginating backwards, the cursor to continue.""" startCursor: String """When paginating forwards, the cursor to continue.""" endCursor: String } type Query { booksConn( filters: BookFilter order: BookOrder """Returns the items in the list that come before the specified cursor.""" before: String = null """Returns the items in the list that come after the specified cursor.""" after: String = null """Returns the first n items from the list.""" first: Int = null """Returns the items in the list that come after the specified cursor.""" last: Int = null ): BookTypeConnection! booksConn2( filters: BookFilter order: BookOrder """Returns the items in the list that come before the specified cursor.""" before: String = null """Returns the items in the list that come after the specified cursor.""" after: String = null """Returns the first n items from the list.""" first: Int = null """Returns the items in the list that come after the specified cursor.""" last: Int = null ): BookTypeConnection! authorsConn( """Returns the items in the list that come before the specified cursor.""" before: String = null """Returns the items in the list that come after the specified cursor.""" after: String = null """Returns the first n items from the list.""" first: Int = null """Returns the items in the list that come after the specified cursor.""" last: Int = null ): AuthorTypeConnection! authorsConn2( """Returns the items in the list that come before the specified cursor.""" before: String = null """Returns the items in the list that come after the specified cursor.""" after: String = null """Returns the first n items from the list.""" first: Int = null """Returns the items in the list that come after the specified cursor.""" last: Int = null ): AuthorTypeConnection! }strawberry-graphql-django-0.62.0/tests/relay/lazy/test_lazy_annotations.py000066400000000000000000000015071502405145400271440ustar00rootroot00000000000000import pathlib import strawberry from pytest_snapshot.plugin import Snapshot import strawberry_django from strawberry_django.relay import DjangoListConnection from .a import AuthorConnection, AuthorType from .b import BookConnection, BookType SNAPSHOTS_DIR = pathlib.Path(__file__).parent / "snapshots" def test_lazy_type_annotations_in_schema(snapshot: Snapshot): @strawberry.type class Query: books_conn: BookConnection = strawberry_django.connection() books_conn2: DjangoListConnection[BookType] = strawberry_django.connection() authors_conn: AuthorConnection = strawberry_django.connection() authors_conn2: DjangoListConnection[AuthorType] = strawberry_django.connection() schema = strawberry.Schema(query=Query) snapshot.assert_match(str(schema), "authors_and_books_schema.gql") strawberry-graphql-django-0.62.0/tests/relay/schema.py000066400000000000000000000045241502405145400227740ustar00rootroot00000000000000from collections.abc import Iterable from typing import ( Annotated, Any, ClassVar, Optional, ) import strawberry from django.db import models from strawberry import relay from strawberry.permission import BasePermission from strawberry.types import Info import strawberry_django from strawberry_django.relay import DjangoListConnection class FruitModel(models.Model): class Meta: ordering: ClassVar[list[str]] = ["id"] name = models.CharField(max_length=255) color = models.CharField(max_length=255) @strawberry_django.filter_type(FruitModel, lookups=True) class FruitFilter: name: strawberry.auto color: strawberry.auto @strawberry_django.order(FruitModel) class FruitOrder: name: strawberry.auto color: strawberry.auto @strawberry_django.type(FruitModel) class Fruit(relay.Node): name: strawberry.auto color: strawberry.auto class DummyPermission(BasePermission): message = "Dummy message" async def has_permission(self, source: Any, info: Info, **kwargs: Any) -> bool: return True @strawberry.type class Query: node: relay.Node = strawberry_django.node() node_with_async_permissions: relay.Node = strawberry_django.node( permission_classes=[DummyPermission], ) nodes: list[relay.Node] = strawberry_django.node() node_optional: Optional[relay.Node] = strawberry_django.node() nodes_optional: list[Optional[relay.Node]] = strawberry_django.node() fruits: DjangoListConnection[Fruit] = strawberry_django.connection() fruits_lazy: DjangoListConnection[ Annotated["Fruit", strawberry.lazy("tests.relay.schema")] ] = strawberry_django.connection() fruits_with_filters_and_order: DjangoListConnection[Fruit] = ( strawberry_django.connection( filters=FruitFilter, order=FruitOrder, ) ) @strawberry_django.connection(DjangoListConnection[Fruit]) def fruits_custom_resolver(self, info: Info) -> Iterable[FruitModel]: return FruitModel.objects.all() @strawberry_django.connection( DjangoListConnection[Fruit], filters=FruitFilter, order=FruitOrder, ) def fruits_custom_resolver_with_filters_and_order( self, info: Info, ) -> Iterable[FruitModel]: return FruitModel.objects.all() schema = strawberry.Schema(query=Query) strawberry-graphql-django-0.62.0/tests/relay/snapshots/000077500000000000000000000000001502405145400231775ustar00rootroot00000000000000strawberry-graphql-django-0.62.0/tests/relay/snapshots/schema.gql000066400000000000000000000121231502405145400251430ustar00rootroot00000000000000type Fruit implements Node { """The Globally Unique ID of this object""" id: ID! name: String! color: String! } """A connection to a list of items.""" type FruitConnection { """Pagination data for this connection""" pageInfo: PageInfo! """Contains the nodes in this connection""" edges: [FruitEdge!]! """Total quantity of existing nodes.""" totalCount: Int } """An edge in a connection.""" type FruitEdge { """A cursor for use in pagination""" cursor: String! """The item at the end of the edge""" node: Fruit! } input FruitFilter { name: StrFilterLookup color: StrFilterLookup AND: FruitFilter OR: FruitFilter NOT: FruitFilter DISTINCT: Boolean } input FruitOrder { name: Ordering color: Ordering } """An object with a Globally Unique ID""" interface Node { """The Globally Unique ID of this object""" id: ID! } enum Ordering { ASC ASC_NULLS_FIRST ASC_NULLS_LAST DESC DESC_NULLS_FIRST DESC_NULLS_LAST } """Information to aid in pagination.""" type PageInfo { """When paginating forwards, are there more items?""" hasNextPage: Boolean! """When paginating backwards, are there more items?""" hasPreviousPage: Boolean! """When paginating backwards, the cursor to continue.""" startCursor: String """When paginating forwards, the cursor to continue.""" endCursor: String } type Query { node( """The ID of the object.""" id: ID! ): Node! nodeWithAsyncPermissions( """The ID of the object.""" id: ID! ): Node! nodes( """The IDs of the objects.""" ids: [ID!]! ): [Node!]! nodeOptional( """The ID of the object.""" id: ID! ): Node nodesOptional( """The IDs of the objects.""" ids: [ID!]! ): [Node]! fruits( """Returns the items in the list that come before the specified cursor.""" before: String = null """Returns the items in the list that come after the specified cursor.""" after: String = null """Returns the first n items from the list.""" first: Int = null """Returns the items in the list that come after the specified cursor.""" last: Int = null ): FruitConnection! fruitsLazy( """Returns the items in the list that come before the specified cursor.""" before: String = null """Returns the items in the list that come after the specified cursor.""" after: String = null """Returns the first n items from the list.""" first: Int = null """Returns the items in the list that come after the specified cursor.""" last: Int = null ): FruitConnection! fruitsWithFiltersAndOrder( filters: FruitFilter order: FruitOrder """Returns the items in the list that come before the specified cursor.""" before: String = null """Returns the items in the list that come after the specified cursor.""" after: String = null """Returns the first n items from the list.""" first: Int = null """Returns the items in the list that come after the specified cursor.""" last: Int = null ): FruitConnection! fruitsCustomResolver( """Returns the items in the list that come before the specified cursor.""" before: String = null """Returns the items in the list that come after the specified cursor.""" after: String = null """Returns the first n items from the list.""" first: Int = null """Returns the items in the list that come after the specified cursor.""" last: Int = null ): FruitConnection! fruitsCustomResolverWithFiltersAndOrder( filters: FruitFilter order: FruitOrder """Returns the items in the list that come before the specified cursor.""" before: String = null """Returns the items in the list that come after the specified cursor.""" after: String = null """Returns the first n items from the list.""" first: Int = null """Returns the items in the list that come after the specified cursor.""" last: Int = null ): FruitConnection! } input StrFilterLookup { """Exact match. Filter will be skipped on `null` value""" exact: String """Assignment test. Filter will be skipped on `null` value""" isNull: Boolean """ Exact match of items in a given list. Filter will be skipped on `null` value """ inList: [String!] """Case-insensitive exact match. Filter will be skipped on `null` value""" iExact: String """ Case-sensitive containment test. Filter will be skipped on `null` value """ contains: String """ Case-insensitive containment test. Filter will be skipped on `null` value """ iContains: String """Case-sensitive starts-with. Filter will be skipped on `null` value""" startsWith: String """Case-insensitive starts-with. Filter will be skipped on `null` value""" iStartsWith: String """Case-sensitive ends-with. Filter will be skipped on `null` value""" endsWith: String """Case-insensitive ends-with. Filter will be skipped on `null` value""" iEndsWith: String """ Case-sensitive regular expression match. Filter will be skipped on `null` value """ regex: String """ Case-insensitive regular expression match. Filter will be skipped on `null` value """ iRegex: String }strawberry-graphql-django-0.62.0/tests/relay/test_cursor_pagination.py000066400000000000000000001375411502405145400263270ustar00rootroot00000000000000import datetime from typing import Optional, cast import pytest import strawberry from django.db.models import F, OrderBy, QuerySet, Value from django.db.models.aggregates import Count from pytest_mock import MockFixture from strawberry.relay import GlobalID, Node, to_base64 import strawberry_django from strawberry_django.optimizer import DjangoOptimizerExtension from strawberry_django.relay import ( DjangoCursorConnection, DjangoCursorEdge, ) from tests.projects.models import Milestone, Project from tests.utils import assert_num_queries @strawberry_django.order(Project) class ProjectOrder: id: strawberry.auto name: strawberry.auto due_date: strawberry.auto @strawberry_django.order_field() def milestone_count( self, queryset: QuerySet, value: strawberry_django.Ordering, prefix: str ) -> "tuple[QuerySet, list[OrderBy]]": queryset = queryset.annotate(_milestone_count=Count(f"{prefix}milestone")) return queryset, [value.resolve("_milestone_count")] @strawberry_django.order(Milestone) class MilestoneOrder: due_date: strawberry.auto project: ProjectOrder @strawberry_django.order_field() def days_left( self, queryset: QuerySet, value: strawberry_django.Ordering, prefix: str ) -> "tuple[QuerySet, list[OrderBy]]": queryset = queryset.alias( _days_left=Value(datetime.date(2025, 12, 31)) - F(f"{prefix}due_date") ) return queryset, [value.resolve("_days_left")] @strawberry_django.type(Milestone, order=MilestoneOrder) class MilestoneType(Node): due_date: strawberry.auto project: "ProjectType" @classmethod def get_queryset(cls, qs: QuerySet, info): if not qs.ordered: qs = qs.order_by("project__name", "pk") return qs @strawberry_django.type(Project, order=ProjectOrder) class ProjectType(Node): name: str due_date: datetime.date milestones: DjangoCursorConnection[MilestoneType] = strawberry_django.connection() @classmethod def get_queryset(cls, qs: QuerySet, info): if not qs.ordered: qs = qs.order_by("name", "pk") return qs @strawberry.type() class Query: project: Optional[ProjectType] = strawberry_django.node() projects: DjangoCursorConnection[ProjectType] = strawberry_django.connection() milestones: DjangoCursorConnection[MilestoneType] = strawberry_django.connection() @strawberry_django.connection( DjangoCursorConnection[ProjectType], disable_optimization=True ) @staticmethod def deferred_projects() -> list[ProjectType]: result = Project.objects.all().order_by("name").defer("name") return cast("list[ProjectType]", result) @strawberry_django.connection(DjangoCursorConnection[ProjectType]) @staticmethod def projects_with_resolver() -> list[ProjectType]: return cast("list[ProjectType]", Project.objects.all().order_by("-pk")) schema = strawberry.Schema(query=Query, extensions=[DjangoOptimizerExtension()]) @pytest.fixture def test_objects(): pa = Project.objects.create(id=1, name="Project A") pc1 = Project.objects.create(id=2, name="Project C") Project.objects.create(id=5, name="Project C") pb = Project.objects.create(id=3, name="Project B") Project.objects.create(id=6, name="Project D") Project.objects.create(id=4, name="Project E") Milestone.objects.create(id=1, project=pb, due_date=datetime.date(2025, 6, 1)) Milestone.objects.create(id=2, project=pb, due_date=datetime.date(2025, 6, 2)) Milestone.objects.create(id=3, project=pc1, due_date=datetime.date(2025, 6, 1)) Milestone.objects.create(id=4, project=pa, due_date=datetime.date(2025, 6, 5)) @pytest.mark.django_db(transaction=True) def test_cursor_pagination(test_objects): query = """ query TestQuery { projects { edges { cursor node { id name } } } } """ with assert_num_queries(1): result = schema.execute_sync(query) assert result.data == { "projects": { "edges": [ { "cursor": to_base64( DjangoCursorEdge.CURSOR_PREFIX, '["Project A","1"]' ), "node": { "id": str(GlobalID("ProjectType", "1")), "name": "Project A", }, }, { "cursor": to_base64( DjangoCursorEdge.CURSOR_PREFIX, '["Project B","3"]' ), "node": { "id": str(GlobalID("ProjectType", "3")), "name": "Project B", }, }, { "cursor": to_base64( DjangoCursorEdge.CURSOR_PREFIX, '["Project C","2"]' ), "node": { "id": str(GlobalID("ProjectType", "2")), "name": "Project C", }, }, { "cursor": to_base64( DjangoCursorEdge.CURSOR_PREFIX, '["Project C","5"]' ), "node": { "id": str(GlobalID("ProjectType", "5")), "name": "Project C", }, }, { "cursor": to_base64( DjangoCursorEdge.CURSOR_PREFIX, '["Project D","6"]' ), "node": { "id": str(GlobalID("ProjectType", "6")), "name": "Project D", }, }, { "cursor": to_base64( DjangoCursorEdge.CURSOR_PREFIX, '["Project E","4"]' ), "node": { "id": str(GlobalID("ProjectType", "4")), "name": "Project E", }, }, ] } } @pytest.mark.django_db(transaction=True) def test_cursor_pagination_custom_resolver(test_objects): query = """ query TestQuery($after: String, $first: Int) { projectsWithResolver(after: $after, first: $first) { edges { cursor node { id name } } } } """ with assert_num_queries(1): result = schema.execute_sync( query, { "after": to_base64(DjangoCursorEdge.CURSOR_PREFIX, '["6"]'), "first": 2, }, ) assert result.data == { "projectsWithResolver": { "edges": [ { "cursor": to_base64(DjangoCursorEdge.CURSOR_PREFIX, '["5"]'), "node": { "id": str(GlobalID("ProjectType", "5")), "name": "Project C", }, }, { "cursor": to_base64(DjangoCursorEdge.CURSOR_PREFIX, '["4"]'), "node": { "id": str(GlobalID("ProjectType", "4")), "name": "Project E", }, }, ] } } @pytest.mark.django_db(transaction=True) def test_forward_pagination(test_objects): query = """ query TestQuery($first: Int, $after: String) { projects(first: $first, after: $after) { edges { cursor node { id name } } pageInfo { startCursor endCursor hasNextPage } } } """ with assert_num_queries(1): result = schema.execute_sync( query, { "first": 3, "after": to_base64(DjangoCursorEdge.CURSOR_PREFIX, '["Project B","3"]'), }, ) assert result.data == { "projects": { "pageInfo": { "startCursor": to_base64( DjangoCursorEdge.CURSOR_PREFIX, '["Project C","2"]' ), "endCursor": to_base64( DjangoCursorEdge.CURSOR_PREFIX, '["Project D","6"]' ), "hasNextPage": True, }, "edges": [ { "cursor": to_base64( DjangoCursorEdge.CURSOR_PREFIX, '["Project C","2"]' ), "node": { "id": str(GlobalID("ProjectType", "2")), "name": "Project C", }, }, { "cursor": to_base64( DjangoCursorEdge.CURSOR_PREFIX, '["Project C","5"]' ), "node": { "id": str(GlobalID("ProjectType", "5")), "name": "Project C", }, }, { "cursor": to_base64( DjangoCursorEdge.CURSOR_PREFIX, '["Project D","6"]' ), "node": { "id": str(GlobalID("ProjectType", "6")), "name": "Project D", }, }, ], } } @pytest.mark.django_db(transaction=True) def test_forward_pagination_first_page(test_objects): query = """ query TestQuery($first: Int, $after: String) { projects(first: $first, after: $after) { edges { cursor node { id name } } pageInfo { startCursor endCursor hasPreviousPage hasNextPage } } } """ with assert_num_queries(1): result = schema.execute_sync( query, { "first": 1, "after": None, }, ) assert result.data == { "projects": { "pageInfo": { "startCursor": to_base64( DjangoCursorEdge.CURSOR_PREFIX, '["Project A","1"]' ), "endCursor": to_base64( DjangoCursorEdge.CURSOR_PREFIX, '["Project A","1"]' ), "hasPreviousPage": False, "hasNextPage": True, }, "edges": [ { "cursor": to_base64( DjangoCursorEdge.CURSOR_PREFIX, '["Project A","1"]' ), "node": { "id": str(GlobalID("ProjectType", "1")), "name": "Project A", }, }, ], } } @pytest.mark.django_db(transaction=True) def test_forward_pagination_last_page(test_objects): query = """ query TestQuery($first: Int, $after: String) { projects(first: $first, after: $after) { edges { cursor node { id name } } pageInfo { startCursor endCursor hasNextPage } } } """ with assert_num_queries(1): result = schema.execute_sync( query, { "first": 10, "after": to_base64(DjangoCursorEdge.CURSOR_PREFIX, '["Project D","6"]'), }, ) assert result.data == { "projects": { "pageInfo": { "startCursor": to_base64( DjangoCursorEdge.CURSOR_PREFIX, '["Project E","4"]' ), "endCursor": to_base64( DjangoCursorEdge.CURSOR_PREFIX, '["Project E","4"]' ), "hasNextPage": False, }, "edges": [ { "cursor": to_base64( DjangoCursorEdge.CURSOR_PREFIX, '["Project E","4"]' ), "node": { "id": str(GlobalID("ProjectType", "4")), "name": "Project E", }, }, ], } } @pytest.mark.django_db(transaction=True) def test_backward_pagination(test_objects): query = """ query TestQuery($last: Int, $before: String) { projects(last: $last, before: $before) { edges { cursor node { id name } } pageInfo { startCursor endCursor hasPreviousPage } } } """ with assert_num_queries(1): result = schema.execute_sync( query, { "last": 2, "before": to_base64( DjangoCursorEdge.CURSOR_PREFIX, '["Project C","5"]' ), }, ) assert result.data == { "projects": { "pageInfo": { "startCursor": to_base64( DjangoCursorEdge.CURSOR_PREFIX, '["Project B","3"]' ), "endCursor": to_base64( DjangoCursorEdge.CURSOR_PREFIX, '["Project C","2"]' ), "hasPreviousPage": True, }, "edges": [ { "cursor": to_base64( DjangoCursorEdge.CURSOR_PREFIX, '["Project B","3"]' ), "node": { "id": str(GlobalID("ProjectType", "3")), "name": "Project B", }, }, { "cursor": to_base64( DjangoCursorEdge.CURSOR_PREFIX, '["Project C","2"]' ), "node": { "id": str(GlobalID("ProjectType", "2")), "name": "Project C", }, }, ], } } @pytest.mark.django_db(transaction=True) def test_backward_pagination_first_page(test_objects): query = """ query TestQuery($last: Int, $before: String) { projects(last: $last, before: $before) { edges { cursor node { id name } } pageInfo { startCursor endCursor hasNextPage hasPreviousPage } } } """ with assert_num_queries(1): result = schema.execute_sync( query, { "last": 2, "before": None, }, ) assert result.data == { "projects": { "pageInfo": { "startCursor": to_base64( DjangoCursorEdge.CURSOR_PREFIX, '["Project D","6"]' ), "endCursor": to_base64( DjangoCursorEdge.CURSOR_PREFIX, '["Project E","4"]' ), "hasPreviousPage": True, "hasNextPage": False, }, "edges": [ { "cursor": to_base64( DjangoCursorEdge.CURSOR_PREFIX, '["Project D","6"]' ), "node": { "id": str(GlobalID("ProjectType", "6")), "name": "Project D", }, }, { "cursor": to_base64( DjangoCursorEdge.CURSOR_PREFIX, '["Project E","4"]' ), "node": { "id": str(GlobalID("ProjectType", "4")), "name": "Project E", }, }, ], } } @pytest.mark.django_db(transaction=True) def test_backward_pagination_last_page(test_objects): query = """ query TestQuery($last: Int, $before: String) { projects(last: $last, before: $before) { edges { cursor node { id name } } pageInfo { startCursor endCursor hasPreviousPage } } } """ with assert_num_queries(1): result = schema.execute_sync( query, { "last": 2, "before": to_base64( DjangoCursorEdge.CURSOR_PREFIX, '["Project C","2"]' ), }, ) assert result.data == { "projects": { "pageInfo": { "startCursor": to_base64( DjangoCursorEdge.CURSOR_PREFIX, '["Project A","1"]' ), "endCursor": to_base64( DjangoCursorEdge.CURSOR_PREFIX, '["Project B","3"]' ), "hasPreviousPage": False, }, "edges": [ { "cursor": to_base64( DjangoCursorEdge.CURSOR_PREFIX, '["Project A","1"]' ), "node": { "id": str(GlobalID("ProjectType", "1")), "name": "Project A", }, }, { "cursor": to_base64( DjangoCursorEdge.CURSOR_PREFIX, '["Project B","3"]' ), "node": { "id": str(GlobalID("ProjectType", "3")), "name": "Project B", }, }, ], } } @pytest.mark.parametrize( ("first", "last", "pks", "has_next", "has_previous"), [ (4, 2, [3, 4], True, True), (6, 2, [5, 6], False, True), (4, 4, [1, 2, 3, 4], True, False), (6, 6, [1, 2, 3, 4, 5, 6], False, False), (8, 4, [3, 4, 5, 6], False, True), (4, 8, [1, 2, 3, 4], True, False), ], ) @pytest.mark.django_db(transaction=True) def test_first_and_last_pagination( first, last, pks, has_next, has_previous, test_objects ): query = """ query TestQuery($first: Int, $last: Int) { projects(first: $first, last: $last, order: { id: ASC }) { edges { cursor node { id } } pageInfo { startCursor endCursor hasNextPage hasPreviousPage } } } """ with assert_num_queries(1): result = schema.execute_sync( query, { "first": first, "last": last, }, ) assert result.data == { "projects": { "pageInfo": { "startCursor": to_base64( DjangoCursorEdge.CURSOR_PREFIX, f'["{pks[0]}"]' ), "endCursor": to_base64( DjangoCursorEdge.CURSOR_PREFIX, f'["{pks[-1]}"]' ), "hasPreviousPage": has_previous, "hasNextPage": has_next, }, "edges": [ { "cursor": to_base64( DjangoCursorEdge.CURSOR_PREFIX, f'["{pk}"]' ), "node": { "id": str(GlobalID("ProjectType", str(pk))), }, } for pk in pks ], } } @pytest.mark.django_db(transaction=True) def test_empty_connection(): query = """ query TestQuery { projects { edges { cursor node { id name } } pageInfo { startCursor endCursor hasNextPage hasPreviousPage } } } """ with assert_num_queries(1): result = schema.execute_sync( query, ) assert result.data == { "projects": { "pageInfo": { "startCursor": None, "endCursor": None, "hasNextPage": False, "hasPreviousPage": False, }, "edges": [], } } @pytest.mark.django_db(transaction=True) def test_cursor_pagination_custom_order(test_objects): query = """ query TestQuery($first: Int, $after: String) { projects(first: $first, after: $after, order: { name: DESC id: ASC }) { edges { cursor node { id name } } } } """ with assert_num_queries(1): result = schema.execute_sync( query, { "first": 2, "after": to_base64(DjangoCursorEdge.CURSOR_PREFIX, '["Project E","4"]'), }, ) assert result.data == { "projects": { "edges": [ { "cursor": to_base64( DjangoCursorEdge.CURSOR_PREFIX, '["Project D","6"]' ), "node": { "id": str(GlobalID("ProjectType", "6")), "name": "Project D", }, }, { "cursor": to_base64( DjangoCursorEdge.CURSOR_PREFIX, '["Project C","2"]' ), "node": { "id": str(GlobalID("ProjectType", "2")), "name": "Project C", }, }, ] } } @pytest.mark.django_db(transaction=True) def test_cursor_pagination_joined_field_order(test_objects): query = """ query TestQuery { milestones(order: { dueDate: DESC, project: { name: ASC } }) { edges { cursor node { id dueDate project { id name } } } } } """ with assert_num_queries(2): result = schema.execute_sync(query) assert result.data == { "milestones": { "edges": [ { "cursor": to_base64( DjangoCursorEdge.CURSOR_PREFIX, '["2025-06-05","Project A","4"]', ), "node": { "id": str(GlobalID("MilestoneType", "4")), "dueDate": "2025-06-05", "project": { "id": str(GlobalID("ProjectType", "1")), "name": "Project A", }, }, }, { "cursor": to_base64( DjangoCursorEdge.CURSOR_PREFIX, '["2025-06-02","Project B","2"]', ), "node": { "id": str(GlobalID("MilestoneType", "2")), "dueDate": "2025-06-02", "project": { "id": str(GlobalID("ProjectType", "3")), "name": "Project B", }, }, }, { "cursor": to_base64( DjangoCursorEdge.CURSOR_PREFIX, '["2025-06-01","Project B","1"]', ), "node": { "id": str(GlobalID("MilestoneType", "1")), "dueDate": "2025-06-01", "project": { "id": str(GlobalID("ProjectType", "3")), "name": "Project B", }, }, }, { "cursor": to_base64( DjangoCursorEdge.CURSOR_PREFIX, '["2025-06-01","Project C","3"]', ), "node": { "id": str(GlobalID("MilestoneType", "3")), "dueDate": "2025-06-01", "project": { "id": str(GlobalID("ProjectType", "2")), "name": "Project C", }, }, }, ] } } @pytest.mark.django_db(transaction=True) def test_cursor_pagination_expression_order(test_objects): query = """ query TestQuery($after: String) { milestones(after: $after, order: { daysLeft: ASC }) { edges { cursor node { id } } } } """ with assert_num_queries(1): result = schema.execute_sync( query, { "after": to_base64( DjangoCursorEdge.CURSOR_PREFIX, '["209 00:00:00","4"]' ) }, ) assert result.data == { "milestones": { "edges": [ { "cursor": to_base64( DjangoCursorEdge.CURSOR_PREFIX, '["212 00:00:00","2"]' ), "node": { "id": str(GlobalID("MilestoneType", "2")), }, }, { "cursor": to_base64( DjangoCursorEdge.CURSOR_PREFIX, '["213 00:00:00","1"]' ), "node": { "id": str(GlobalID("MilestoneType", "1")), }, }, { "cursor": to_base64( DjangoCursorEdge.CURSOR_PREFIX, '["213 00:00:00","3"]' ), "node": { "id": str(GlobalID("MilestoneType", "3")), }, }, ] } } @pytest.mark.django_db(transaction=True) def test_cursor_pagination_agg_expression_order(test_objects): query = """ query TestQuery($after: String, $first: Int) { projects(after: $after, first: $first, order: { milestoneCount: DESC }) { edges { cursor node { id } } } } """ with assert_num_queries(1): result = schema.execute_sync( query, { "after": None, "first": 4, }, ) assert result.data == { "projects": { "edges": [ { "cursor": to_base64( DjangoCursorEdge.CURSOR_PREFIX, '["2","3"]' ), "node": { "id": str(GlobalID("ProjectType", "3")), }, }, { "cursor": to_base64( DjangoCursorEdge.CURSOR_PREFIX, '["1","1"]' ), "node": { "id": str(GlobalID("ProjectType", "1")), }, }, { "cursor": to_base64( DjangoCursorEdge.CURSOR_PREFIX, '["1","2"]' ), "node": { "id": str(GlobalID("ProjectType", "2")), }, }, { "cursor": to_base64( DjangoCursorEdge.CURSOR_PREFIX, '["0","4"]' ), "node": { "id": str(GlobalID("ProjectType", "4")), }, }, ] } } @pytest.mark.django_db(transaction=True) def test_cursor_pagination_order_field_deferred(test_objects): query = """ query TestQuery { deferredProjects(first: 2) { edges { cursor node { id } } } } """ with assert_num_queries(1): result = schema.execute_sync(query) assert result.data == { "deferredProjects": { "edges": [ { "cursor": to_base64( DjangoCursorEdge.CURSOR_PREFIX, '["Project A","1"]' ), "node": { "id": str(GlobalID("ProjectType", "1")), }, }, { "cursor": to_base64( DjangoCursorEdge.CURSOR_PREFIX, '["Project B","3"]' ), "node": { "id": str(GlobalID("ProjectType", "3")), }, }, ] } } @pytest.mark.django_db(transaction=True) @pytest.mark.parametrize( ("order", "pks"), [ ("DESC_NULLS_FIRST", [1, 4, 3, 2]), ("DESC_NULLS_LAST", [3, 2, 1, 4]), ("ASC_NULLS_FIRST", [1, 4, 2, 3]), ("ASC_NULLS_LAST", [2, 3, 1, 4]), ], ) @pytest.mark.parametrize("offset", [0, 1, 2, 3]) def test_cursor_pagination_order_with_nulls(order, pks, offset): pa = Project.objects.create(id=1, name="Project A", due_date=None) pc = Project.objects.create( id=2, name="Project C", due_date=datetime.date(2025, 6, 2) ) pb = Project.objects.create( id=3, name="Project B", due_date=datetime.date(2025, 6, 5) ) pd = Project.objects.create(id=4, name="Project D", due_date=None) projects_lookup = {p.pk: p for p in (pa, pb, pc, pd)} projects = [projects_lookup[pk] for pk in pks] query = """ query TestQuery($after: String, $first: Int, $order: Ordering!) { projects(after: $after, first: $first, order: { dueDate: $order }) { edges { cursor node { id name } } } } """ def make_cursor(project: Project) -> str: due_date_part = ( f'"{project.due_date.isoformat()}"' if project.due_date else "null" ) return to_base64( DjangoCursorEdge.CURSOR_PREFIX, f'[{due_date_part},"{project.pk}"]' ) with assert_num_queries(1): result = schema.execute_sync( query, { "order": order, "after": make_cursor(projects[offset]), "first": 2, }, ) assert result.data == { "projects": { "edges": [ { "cursor": make_cursor(project), "node": { "id": str(GlobalID("ProjectType", str(project.pk))), "name": project.name, }, } for project in projects[offset + 1 : offset + 3] ] } } @pytest.mark.django_db(transaction=True) async def test_cursor_pagination_async(test_objects): query = """ query TestQuery { projects { edges { cursor node { id name } } } } """ result = await schema.execute(query) assert result.data == { "projects": { "edges": [ { "cursor": to_base64( DjangoCursorEdge.CURSOR_PREFIX, '["Project A","1"]' ), "node": { "id": str(GlobalID("ProjectType", "1")), "name": "Project A", }, }, { "cursor": to_base64( DjangoCursorEdge.CURSOR_PREFIX, '["Project B","3"]' ), "node": { "id": str(GlobalID("ProjectType", "3")), "name": "Project B", }, }, { "cursor": to_base64( DjangoCursorEdge.CURSOR_PREFIX, '["Project C","2"]' ), "node": { "id": str(GlobalID("ProjectType", "2")), "name": "Project C", }, }, { "cursor": to_base64( DjangoCursorEdge.CURSOR_PREFIX, '["Project C","5"]' ), "node": { "id": str(GlobalID("ProjectType", "5")), "name": "Project C", }, }, { "cursor": to_base64( DjangoCursorEdge.CURSOR_PREFIX, '["Project D","6"]' ), "node": { "id": str(GlobalID("ProjectType", "6")), "name": "Project D", }, }, { "cursor": to_base64( DjangoCursorEdge.CURSOR_PREFIX, '["Project E","4"]' ), "node": { "id": str(GlobalID("ProjectType", "4")), "name": "Project E", }, }, ] } } @pytest.mark.django_db(transaction=True) def test_nested_cursor_pagination_in_single(): pa = Project.objects.create(id=1, name="Project A") pb = Project.objects.create(id=2, name="Project B") Milestone.objects.create(id=1, project=pb, due_date=datetime.date(2025, 6, 1)) Milestone.objects.create(id=2, project=pb, due_date=datetime.date(2025, 6, 2)) Milestone.objects.create(id=3, project=pb, due_date=datetime.date(2025, 6, 1)) Milestone.objects.create(id=4, project=pa, due_date=datetime.date(2025, 6, 5)) Milestone.objects.create(id=5, project=pa, due_date=datetime.date(2025, 6, 1)) query = """ query TestQuery($id: ID!) { project(id: $id) { id milestones(first: 2, order: { dueDate: ASC }) { edges { cursor node { id dueDate } } } } } """ with assert_num_queries(2): result = schema.execute_sync(query, {"id": str(GlobalID("ProjectType", "2"))}) assert result.data == { "project": { "id": str(GlobalID("ProjectType", "2")), "milestones": { "edges": [ { "cursor": to_base64( DjangoCursorEdge.CURSOR_PREFIX, '["2025-06-01","1"]', ), "node": { "id": str(GlobalID("MilestoneType", "1")), "dueDate": "2025-06-01", }, }, { "cursor": to_base64( DjangoCursorEdge.CURSOR_PREFIX, '["2025-06-01","3"]', ), "node": { "id": str(GlobalID("MilestoneType", "3")), "dueDate": "2025-06-01", }, }, ] }, }, } @pytest.mark.django_db(transaction=True) def test_nested_cursor_pagination(): pa = Project.objects.create(id=1, name="Project A") pb = Project.objects.create(id=2, name="Project B") Milestone.objects.create(id=1, project=pb, due_date=datetime.date(2025, 6, 1)) Milestone.objects.create(id=2, project=pb, due_date=datetime.date(2025, 6, 2)) Milestone.objects.create(id=3, project=pb, due_date=datetime.date(2025, 6, 1)) Milestone.objects.create(id=4, project=pa, due_date=datetime.date(2025, 6, 5)) Milestone.objects.create(id=5, project=pa, due_date=datetime.date(2025, 6, 1)) query = """ query TestQuery { projects { edges { cursor node { id milestones(first: 2, order: { dueDate: ASC }) { pageInfo { hasNextPage } edges { cursor node { id dueDate } } } } } } } """ with assert_num_queries(2): result = schema.execute_sync(query) assert result.data == { "projects": { "edges": [ { "cursor": to_base64( DjangoCursorEdge.CURSOR_PREFIX, '["Project A","1"]' ), "node": { "id": str(GlobalID("ProjectType", "1")), "milestones": { "pageInfo": {"hasNextPage": False}, "edges": [ { "cursor": to_base64( DjangoCursorEdge.CURSOR_PREFIX, '["2025-06-01","5"]', ), "node": { "id": str(GlobalID("MilestoneType", "5")), "dueDate": "2025-06-01", }, }, { "cursor": to_base64( DjangoCursorEdge.CURSOR_PREFIX, '["2025-06-05","4"]', ), "node": { "id": str(GlobalID("MilestoneType", "4")), "dueDate": "2025-06-05", }, }, ], }, }, }, { "cursor": to_base64( DjangoCursorEdge.CURSOR_PREFIX, '["Project B","2"]' ), "node": { "id": str(GlobalID("ProjectType", "2")), "milestones": { "pageInfo": {"hasNextPage": True}, "edges": [ { "cursor": to_base64( DjangoCursorEdge.CURSOR_PREFIX, '["2025-06-01","1"]', ), "node": { "id": str(GlobalID("MilestoneType", "1")), "dueDate": "2025-06-01", }, }, { "cursor": to_base64( DjangoCursorEdge.CURSOR_PREFIX, '["2025-06-01","3"]', ), "node": { "id": str(GlobalID("MilestoneType", "3")), "dueDate": "2025-06-01", }, }, ], }, }, }, ] } } @pytest.mark.django_db(transaction=True) @pytest.mark.parametrize("first", [None, 3]) @pytest.mark.parametrize( "after", [None, to_base64(DjangoCursorEdge.CURSOR_PREFIX, '["2"]')] ) @pytest.mark.parametrize("last", [None, 3]) @pytest.mark.parametrize( "before", [None, to_base64(DjangoCursorEdge.CURSOR_PREFIX, '["2"]')] ) def test_total_count_ignores_pagination(test_objects, first, after, before, last): query = """ query TestQuery($first: Int, $after: String, $last: Int, $before: String) { projects(first: $first, after: $after, last: $last, before: $before, order: { id: ASC }) { totalCount } } """ with assert_num_queries(1): result = schema.execute_sync( query, {"first": first, "after": after, "last": last, "before": before} ) assert result.data == {"projects": {"totalCount": 6}} @pytest.mark.django_db(transaction=True) def test_total_count_works_with_edges(test_objects): query = """ query TestQuery($first: Int, $after: String, $last: Int, $before: String) { projects(first: $first, after: $after, last: $last, before: $before, order: { id: ASC }) { totalCount edges { node { id } } } } """ with assert_num_queries(2): result = schema.execute_sync( query, {"first": 3, "after": to_base64(DjangoCursorEdge.CURSOR_PREFIX, '["2"]')}, ) assert result.data == { "projects": { "totalCount": 6, "edges": [ {"node": {"id": str(GlobalID("ProjectType", "3"))}}, {"node": {"id": str(GlobalID("ProjectType", "4"))}}, {"node": {"id": str(GlobalID("ProjectType", "5"))}}, ], } } @pytest.mark.django_db(transaction=True) def test_nested_total_count(): p1 = Project.objects.create() p2 = Project.objects.create() p1m = [Milestone.objects.create(project=p1) for _ in range(3)] p2m = [Milestone.objects.create(project=p2) for _ in range(2)] query = """ query TestQuery { projects(first: 2, order: { id: ASC }) { edges { node { id milestones { totalCount edges { node { id } } } } } } } """ with assert_num_queries(2): result = schema.execute_sync(query) assert result.data == { "projects": { "edges": [ { "node": { "id": str(GlobalID("ProjectType", str(p1.pk))), "milestones": { "totalCount": 3, "edges": [ { "node": { "id": str( GlobalID("MilestoneType", str(m.pk)) ) } } for m in p1m ], }, } }, { "node": { "id": str(GlobalID("ProjectType", str(p2.pk))), "milestones": { "totalCount": 2, "edges": [ { "node": { "id": str( GlobalID("MilestoneType", str(m.pk)) ) } } for m in p2m ], }, } }, ], } } @pytest.mark.django_db(transaction=True) @pytest.mark.parametrize( "cursor", [ *( to_base64(DjangoCursorEdge.CURSOR_PREFIX, c) for c in ("", "[]", "[1]", "{}", "foo", '["foo"]') ), to_base64("foo", "bar"), to_base64("foo", '["1"]'), ], ) def test_invalid_cursor(cursor, test_objects): query = """ query TestQuery($after: String) { projects(after: $after, order: { id: ASC }) { edges { cursor node { id } } } } """ result = schema.execute_sync(query, {"after": cursor}) assert result.data is None assert result.errors assert result.errors[0].message == "Invalid cursor" @pytest.mark.django_db(transaction=True) @pytest.mark.parametrize( ("first", "last", "error_message"), [ (-1, None, "Argument 'first' must be a non-negative integer."), (None, -1, "Argument 'last' must be a non-negative integer."), (150, None, "Argument 'first' cannot be higher than 100."), (None, 150, "Argument 'last' cannot be higher than 100."), (30, 150, "Argument 'last' cannot be higher than 100."), ], ) def test_invalid_offsets(first, last, error_message, test_objects): query = """ query TestQuery($first: Int, $last: Int) { projects(first: $first, last: $last, order: { id: ASC }) { edges { cursor node { id } } } } """ result = schema.execute_sync(query, {"first": first, "last": last}) assert result.data is None assert result.errors assert result.errors[0].message == error_message @pytest.mark.django_db(transaction=True) def test_cursor_connection_rejects_non_querysets(mocker: MockFixture): with pytest.raises(TypeError): DjangoCursorConnection.resolve_connection( list(Project.objects.all()), info=mocker.Mock() ) strawberry-graphql-django-0.62.0/tests/relay/test_fields.py000066400000000000000000001030331502405145400240340ustar00rootroot00000000000000import pytest from pytest_django import DjangoAssertNumQueries from strawberry.relay.utils import to_base64 from .schema import FruitModel, schema @pytest.fixture(autouse=True) def _fixtures(transactional_db): for pk, name, color in [ (1, "Banana", "yellow"), (2, "Apple", "red"), (3, "Pineapple", "yellow"), (4, "Grape", "purple"), (5, "Orange", "orange"), ]: FruitModel.objects.create( id=pk, name=name, color=color, ) def test_query_node(): result = schema.execute_sync( """ query TestQuery ($id: ID!) { node (id: $id) { ... on Node { id } ... on Fruit { name color } } } """, variable_values={ "id": to_base64("Fruit", 2), }, ) assert result.errors is None assert result.data == { "node": { "id": to_base64("Fruit", 2), "color": "red", "name": "Apple", }, } async def test_query_node_with_async_permissions(): result = await schema.execute( """ query TestQuery ($id: ID!) { nodeWithAsyncPermissions (id: $id) { ... on Node { id } ... on Fruit { name color } } } """, variable_values={ "id": to_base64("Fruit", 2), }, ) assert result.errors is None assert result.data == { "nodeWithAsyncPermissions": { "id": to_base64("Fruit", 2), "color": "red", "name": "Apple", }, } def test_query_node_optional(): result = schema.execute_sync( """ query TestQuery ($id: ID!) { nodeOptional (id: $id) { ... on Node { id } ... on Fruit { name color } } } """, variable_values={ "id": to_base64("Fruit", 999), }, ) assert result.errors is None assert result.data == {"nodeOptional": None} async def test_query_node_async(): result = await schema.execute( """ query TestQuery ($id: ID!) { node (id: $id) { ... on Node { id } ... on Fruit { name color } } } """, variable_values={ "id": to_base64("Fruit", 2), }, ) assert result.errors is None assert result.data == { "node": { "id": to_base64("Fruit", 2), "color": "red", "name": "Apple", }, } async def test_query_node_optional_async(): result = await schema.execute( """ query TestQuery ($id: ID!) { nodeOptional (id: $id) { ... on Node { id } ... on Fruit { name color } } } """, variable_values={ "id": to_base64("Fruit", 999), }, ) assert result.errors is None assert result.data == {"nodeOptional": None} def test_query_nodes(): result = schema.execute_sync( """ query TestQuery ($ids: [ID!]!) { nodes (ids: $ids) { ... on Node { id } ... on Fruit { name color } } } """, variable_values={ "ids": [to_base64("Fruit", 2), to_base64("Fruit", 4)], }, ) assert result.errors is None assert result.data == { "nodes": [ { "id": to_base64("Fruit", 2), "name": "Apple", "color": "red", }, { "id": to_base64("Fruit", 4), "name": "Grape", "color": "purple", }, ], } def test_query_nodes_optional(): result = schema.execute_sync( """ query TestQuery ($ids: [ID!]!) { nodesOptional (ids: $ids) { ... on Node { id } ... on Fruit { name color } } } """, variable_values={ "ids": [ to_base64("Fruit", 2), to_base64("Fruit", 999), to_base64("Fruit", 4), ], }, ) assert result.errors is None assert result.data == { "nodesOptional": [ { "id": to_base64("Fruit", 2), "name": "Apple", "color": "red", }, None, { "id": to_base64("Fruit", 4), "name": "Grape", "color": "purple", }, ], } async def test_query_nodes_async(): result = await schema.execute( """ query TestQuery ($ids: [ID!]!) { nodes (ids: $ids) { ... on Node { id } ... on Fruit { name color } } } """, variable_values={ "ids": [ to_base64("Fruit", 2), to_base64("Fruit", 4), ], }, ) assert result.errors is None assert result.data == { "nodes": [ { "id": to_base64("Fruit", 2), "name": "Apple", "color": "red", }, { "id": to_base64("Fruit", 4), "name": "Grape", "color": "purple", }, ], } async def test_query_nodes_optional_async(): result = await schema.execute( """ query TestQuery ($ids: [ID!]!) { nodesOptional (ids: $ids) { ... on Node { id } ... on Fruit { name color } } } """, variable_values={ "ids": [ to_base64("Fruit", 2), to_base64("Fruit", 998), to_base64("Fruit", 4), to_base64("Fruit", 999), ], }, ) assert result.errors is None assert result.data == { "nodesOptional": [ { "id": to_base64("Fruit", 2), "name": "Apple", "color": "red", }, None, { "id": to_base64("Fruit", 4), "name": "Grape", "color": "purple", }, None, ], } fruits_query = """ query TestQuery ( $first: Int = null $last: Int = null $before: String = null, $after: String = null, ) {{ {} ( first: $first last: $last before: $before after: $after ) {{ pageInfo {{ hasNextPage hasPreviousPage startCursor endCursor }} edges {{ cursor node {{ id name color }} }} }} }} """ attrs = [ "fruits", "fruitsLazy", "fruitsWithFiltersAndOrder", "fruitsCustomResolver", "fruitsCustomResolverWithFiltersAndOrder", ] @pytest.mark.parametrize("query_attr", attrs) def test_query_connection(query_attr: str): result = schema.execute_sync( fruits_query.format(query_attr), variable_values={}, ) assert result.errors is None assert result.data == { query_attr: { "edges": [ { "cursor": "YXJyYXljb25uZWN0aW9uOjA=", "node": { "id": to_base64("Fruit", 1), "color": "yellow", "name": "Banana", }, }, { "cursor": "YXJyYXljb25uZWN0aW9uOjE=", "node": { "id": to_base64("Fruit", 2), "color": "red", "name": "Apple", }, }, { "cursor": "YXJyYXljb25uZWN0aW9uOjI=", "node": { "id": to_base64("Fruit", 3), "color": "yellow", "name": "Pineapple", }, }, { "cursor": "YXJyYXljb25uZWN0aW9uOjM=", "node": { "id": to_base64("Fruit", 4), "color": "purple", "name": "Grape", }, }, { "cursor": "YXJyYXljb25uZWN0aW9uOjQ=", "node": { "id": to_base64("Fruit", 5), "color": "orange", "name": "Orange", }, }, ], "pageInfo": { "hasNextPage": False, "hasPreviousPage": False, "startCursor": to_base64("arrayconnection", "0"), "endCursor": to_base64("arrayconnection", "4"), }, }, } @pytest.mark.parametrize("query_attr", attrs) async def test_query_connection_async(query_attr: str): result = await schema.execute( fruits_query.format(query_attr), variable_values={}, ) assert result.errors is None assert result.data == { query_attr: { "edges": [ { "cursor": "YXJyYXljb25uZWN0aW9uOjA=", "node": { "id": to_base64("Fruit", 1), "color": "yellow", "name": "Banana", }, }, { "cursor": "YXJyYXljb25uZWN0aW9uOjE=", "node": { "id": to_base64("Fruit", 2), "color": "red", "name": "Apple", }, }, { "cursor": "YXJyYXljb25uZWN0aW9uOjI=", "node": { "id": to_base64("Fruit", 3), "color": "yellow", "name": "Pineapple", }, }, { "cursor": "YXJyYXljb25uZWN0aW9uOjM=", "node": { "id": to_base64("Fruit", 4), "color": "purple", "name": "Grape", }, }, { "cursor": "YXJyYXljb25uZWN0aW9uOjQ=", "node": { "id": to_base64("Fruit", 5), "color": "orange", "name": "Orange", }, }, ], "pageInfo": { "hasNextPage": False, "hasPreviousPage": False, "startCursor": to_base64("arrayconnection", "0"), "endCursor": to_base64("arrayconnection", "4"), }, }, } @pytest.mark.parametrize("query_attr", attrs) def test_query_connection_filtering_first(query_attr: str): result = schema.execute_sync( fruits_query.format(query_attr), variable_values={"first": 2}, ) assert result.errors is None assert result.data == { query_attr: { "edges": [ { "cursor": "YXJyYXljb25uZWN0aW9uOjA=", "node": { "id": to_base64("Fruit", 1), "color": "yellow", "name": "Banana", }, }, { "cursor": "YXJyYXljb25uZWN0aW9uOjE=", "node": { "id": to_base64("Fruit", 2), "color": "red", "name": "Apple", }, }, ], "pageInfo": { "hasNextPage": True, "hasPreviousPage": False, "startCursor": to_base64("arrayconnection", "0"), "endCursor": to_base64("arrayconnection", "1"), }, }, } @pytest.mark.parametrize("query_attr", attrs) async def test_query_connection_filtering_first_async(query_attr: str): result = await schema.execute( fruits_query.format(query_attr), variable_values={"first": 2}, ) assert result.errors is None assert result.data == { query_attr: { "edges": [ { "cursor": "YXJyYXljb25uZWN0aW9uOjA=", "node": { "id": to_base64("Fruit", 1), "color": "yellow", "name": "Banana", }, }, { "cursor": "YXJyYXljb25uZWN0aW9uOjE=", "node": { "id": to_base64("Fruit", 2), "color": "red", "name": "Apple", }, }, ], "pageInfo": { "hasNextPage": True, "hasPreviousPage": False, "startCursor": to_base64("arrayconnection", "0"), "endCursor": to_base64("arrayconnection", "1"), }, }, } @pytest.mark.parametrize("query_attr", attrs) def test_query_connection_filtering_first_with_after(query_attr: str): result = schema.execute_sync( fruits_query.format(query_attr), variable_values={"first": 2, "after": to_base64("arrayconnection", "1")}, ) assert result.errors is None assert result.data == { query_attr: { "edges": [ { "cursor": "YXJyYXljb25uZWN0aW9uOjI=", "node": { "id": to_base64("Fruit", 3), "color": "yellow", "name": "Pineapple", }, }, { "cursor": "YXJyYXljb25uZWN0aW9uOjM=", "node": { "id": to_base64("Fruit", 4), "color": "purple", "name": "Grape", }, }, ], "pageInfo": { "hasNextPage": True, "hasPreviousPage": True, "startCursor": to_base64("arrayconnection", "2"), "endCursor": to_base64("arrayconnection", "3"), }, }, } @pytest.mark.parametrize("query_attr", attrs) async def test_query_connection_filtering_first_with_after_async(query_attr: str): result = await schema.execute( fruits_query.format(query_attr), variable_values={"first": 2, "after": to_base64("arrayconnection", "1")}, ) assert result.errors is None assert result.data == { query_attr: { "edges": [ { "cursor": "YXJyYXljb25uZWN0aW9uOjI=", "node": { "id": to_base64("Fruit", 3), "color": "yellow", "name": "Pineapple", }, }, { "cursor": "YXJyYXljb25uZWN0aW9uOjM=", "node": { "id": to_base64("Fruit", 4), "color": "purple", "name": "Grape", }, }, ], "pageInfo": { "hasNextPage": True, "hasPreviousPage": True, "startCursor": to_base64("arrayconnection", "2"), "endCursor": to_base64("arrayconnection", "3"), }, }, } @pytest.mark.parametrize("query_attr", attrs) def test_query_connection_filtering_last(query_attr: str): result = schema.execute_sync( fruits_query.format(query_attr), variable_values={"last": 2}, ) assert result.errors is None assert result.data == { query_attr: { "edges": [ { "cursor": "YXJyYXljb25uZWN0aW9uOjM=", "node": { "id": to_base64("Fruit", 4), "color": "purple", "name": "Grape", }, }, { "cursor": "YXJyYXljb25uZWN0aW9uOjQ=", "node": { "id": to_base64("Fruit", 5), "color": "orange", "name": "Orange", }, }, ], "pageInfo": { "hasNextPage": False, "hasPreviousPage": True, "startCursor": to_base64("arrayconnection", "3"), "endCursor": to_base64("arrayconnection", "4"), }, }, } @pytest.mark.parametrize("query_attr", attrs) async def test_query_connection_filtering_last_async(query_attr: str): result = await schema.execute( fruits_query.format(query_attr), variable_values={"last": 2}, ) assert result.errors is None assert result.data == { query_attr: { "edges": [ { "cursor": "YXJyYXljb25uZWN0aW9uOjM=", "node": { "id": to_base64("Fruit", 4), "color": "purple", "name": "Grape", }, }, { "cursor": "YXJyYXljb25uZWN0aW9uOjQ=", "node": { "id": to_base64("Fruit", 5), "color": "orange", "name": "Orange", }, }, ], "pageInfo": { "hasNextPage": False, "hasPreviousPage": True, "startCursor": to_base64("arrayconnection", "3"), "endCursor": to_base64("arrayconnection", "4"), }, }, } @pytest.mark.parametrize("query_attr", attrs) def test_query_connection_filtering_first_with_before(query_attr: str): result = schema.execute_sync( fruits_query.format(query_attr), variable_values={"first": 1, "before": to_base64("arrayconnection", "3")}, ) assert result.errors is None assert result.data == { query_attr: { "edges": [ { "cursor": "YXJyYXljb25uZWN0aW9uOjI=", "node": { "id": to_base64("Fruit", 3), "color": "yellow", "name": "Pineapple", }, }, ], "pageInfo": { "hasNextPage": True, "hasPreviousPage": True, "startCursor": to_base64("arrayconnection", "2"), "endCursor": to_base64("arrayconnection", "2"), }, }, } @pytest.mark.parametrize("query_attr", attrs) async def test_query_connection_filtering_first_with_before_async(query_attr: str): result = await schema.execute( fruits_query.format(query_attr), variable_values={"first": 1, "before": to_base64("arrayconnection", "3")}, ) assert result.errors is None assert result.data == { query_attr: { "edges": [ { "cursor": "YXJyYXljb25uZWN0aW9uOjI=", "node": { "id": to_base64("Fruit", 3), "color": "yellow", "name": "Pineapple", }, }, ], "pageInfo": { "hasNextPage": True, "hasPreviousPage": True, "startCursor": to_base64("arrayconnection", "2"), "endCursor": to_base64("arrayconnection", "2"), }, }, } @pytest.mark.parametrize("query_attr", attrs) def test_query_connection_filtering_last_with_before(query_attr: str): result = schema.execute_sync( fruits_query.format(query_attr), variable_values={"last": 2, "before": to_base64("arrayconnection", "4")}, ) assert result.errors is None assert result.data == { query_attr: { "edges": [ { "cursor": "YXJyYXljb25uZWN0aW9uOjI=", "node": { "id": to_base64("Fruit", 3), "color": "yellow", "name": "Pineapple", }, }, { "cursor": "YXJyYXljb25uZWN0aW9uOjM=", "node": { "id": to_base64("Fruit", 4), "color": "purple", "name": "Grape", }, }, ], "pageInfo": { "hasNextPage": True, "hasPreviousPage": True, "startCursor": to_base64("arrayconnection", "2"), "endCursor": to_base64("arrayconnection", "3"), }, }, } @pytest.mark.parametrize("query_attr", attrs) async def test_query_connection_filtering_last_with_before_async(query_attr: str): result = await schema.execute( fruits_query.format(query_attr), variable_values={"last": 2, "before": to_base64("arrayconnection", "4")}, ) assert result.errors is None assert result.data == { query_attr: { "edges": [ { "cursor": "YXJyYXljb25uZWN0aW9uOjI=", "node": { "id": to_base64("Fruit", 3), "color": "yellow", "name": "Pineapple", }, }, { "cursor": "YXJyYXljb25uZWN0aW9uOjM=", "node": { "id": to_base64("Fruit", 4), "color": "purple", "name": "Grape", }, }, ], "pageInfo": { "hasNextPage": True, "hasPreviousPage": True, "startCursor": to_base64("arrayconnection", "2"), "endCursor": to_base64("arrayconnection", "3"), }, }, } fruits_query_filters_order = """ query TestQuery ( $first: Int = null $last: Int = null $before: String = null, $after: String = null, $filters: FruitFilter $order: FruitOrder ) {{ {} ( first: $first last: $last before: $before after: $after filters: $filters order: $order ) {{ pageInfo {{ hasNextPage hasPreviousPage startCursor endCursor }} edges {{ cursor node {{ id name color }} }} }} }} """ custom_attrs = [ "fruitsWithFiltersAndOrder", "fruitsCustomResolverWithFiltersAndOrder", ] @pytest.mark.parametrize("query_attr", custom_attrs) def test_query_connection_with_filters(query_attr: str): result = schema.execute_sync( fruits_query_filters_order.format(query_attr), variable_values={"filters": {"name": {"endsWith": "e"}}}, ) assert result.errors is None assert result.data == { query_attr: { "edges": [ { "cursor": "YXJyYXljb25uZWN0aW9uOjA=", "node": { "id": to_base64("Fruit", 2), "color": "red", "name": "Apple", }, }, { "cursor": "YXJyYXljb25uZWN0aW9uOjE=", "node": { "id": to_base64("Fruit", 3), "color": "yellow", "name": "Pineapple", }, }, { "cursor": "YXJyYXljb25uZWN0aW9uOjI=", "node": { "id": to_base64("Fruit", 4), "color": "purple", "name": "Grape", }, }, { "cursor": "YXJyYXljb25uZWN0aW9uOjM=", "node": { "id": to_base64("Fruit", 5), "color": "orange", "name": "Orange", }, }, ], "pageInfo": { "hasNextPage": False, "hasPreviousPage": False, "startCursor": to_base64("arrayconnection", "0"), "endCursor": to_base64("arrayconnection", "3"), }, }, } @pytest.mark.parametrize("query_attr", custom_attrs) def test_query_connection_with_filters_and_order(query_attr: str): result = schema.execute_sync( fruits_query_filters_order.format(query_attr), variable_values={ "filters": {"name": {"endsWith": "e"}}, "order": {"name": "DESC"}, }, ) assert result.errors is None assert result.data == { query_attr: { "edges": [ { "cursor": "YXJyYXljb25uZWN0aW9uOjA=", "node": { "id": to_base64("Fruit", 3), "color": "yellow", "name": "Pineapple", }, }, { "cursor": "YXJyYXljb25uZWN0aW9uOjE=", "node": { "id": to_base64("Fruit", 5), "color": "orange", "name": "Orange", }, }, { "cursor": "YXJyYXljb25uZWN0aW9uOjI=", "node": { "id": to_base64("Fruit", 4), "color": "purple", "name": "Grape", }, }, { "cursor": "YXJyYXljb25uZWN0aW9uOjM=", "node": { "id": to_base64("Fruit", 2), "color": "red", "name": "Apple", }, }, ], "pageInfo": { "hasNextPage": False, "hasPreviousPage": False, "startCursor": to_base64("arrayconnection", "0"), "endCursor": to_base64("arrayconnection", "3"), }, }, } @pytest.mark.parametrize("query_attr", custom_attrs) def test_query_connection_custom_resolver_filtering_first(query_attr: str): result = schema.execute_sync( fruits_query_filters_order.format(query_attr), variable_values={"first": 2, "filters": {"name": {"endsWith": "e"}}}, ) assert result.errors is None assert result.data == { query_attr: { "edges": [ { "cursor": "YXJyYXljb25uZWN0aW9uOjA=", "node": { "id": to_base64("Fruit", 2), "color": "red", "name": "Apple", }, }, { "cursor": "YXJyYXljb25uZWN0aW9uOjE=", "node": { "id": to_base64("Fruit", 3), "color": "yellow", "name": "Pineapple", }, }, ], "pageInfo": { "hasNextPage": True, "hasPreviousPage": False, "startCursor": to_base64("arrayconnection", "0"), "endCursor": to_base64("arrayconnection", "1"), }, }, } @pytest.mark.parametrize("query_attr", custom_attrs) def test_query_connection_custom_resolver_filtering_first_with_after(query_attr: str): result = schema.execute_sync( fruits_query_filters_order.format(query_attr), variable_values={ "first": 2, "after": to_base64("arrayconnection", "1"), "filters": {"name": {"endsWith": "e"}}, }, ) assert result.errors is None assert result.data == { query_attr: { "edges": [ { "cursor": "YXJyYXljb25uZWN0aW9uOjI=", "node": { "id": to_base64("Fruit", 4), "color": "purple", "name": "Grape", }, }, { "cursor": "YXJyYXljb25uZWN0aW9uOjM=", "node": { "id": to_base64("Fruit", 5), "color": "orange", "name": "Orange", }, }, ], "pageInfo": { "hasNextPage": False, "hasPreviousPage": True, "startCursor": to_base64("arrayconnection", "2"), "endCursor": to_base64("arrayconnection", "3"), }, }, } @pytest.mark.parametrize("query_attr", custom_attrs) def test_query_connection_custom_resolver_filtering_last(query_attr: str): result = schema.execute_sync( fruits_query_filters_order.format(query_attr), variable_values={"last": 2, "filters": {"name": {"endsWith": "e"}}}, ) assert result.errors is None assert result.data == { query_attr: { "edges": [ { "cursor": "YXJyYXljb25uZWN0aW9uOjI=", "node": { "id": to_base64("Fruit", 4), "color": "purple", "name": "Grape", }, }, { "cursor": "YXJyYXljb25uZWN0aW9uOjM=", "node": { "id": to_base64("Fruit", 5), "color": "orange", "name": "Orange", }, }, ], "pageInfo": { "hasNextPage": False, "hasPreviousPage": True, "startCursor": to_base64("arrayconnection", "2"), "endCursor": to_base64("arrayconnection", "3"), }, }, } @pytest.mark.parametrize("query_attr", custom_attrs) def test_query_connection_custom_resolver_filtering_last_with_before(query_attr: str): result = schema.execute_sync( fruits_query_filters_order.format(query_attr), variable_values={ "last": 2, "before": to_base64("arrayconnection", "3"), "filters": {"name": {"endsWith": "e"}}, }, ) assert result.errors is None assert result.data == { query_attr: { "edges": [ { "cursor": "YXJyYXljb25uZWN0aW9uOjE=", "node": { "id": to_base64("Fruit", 3), "color": "yellow", "name": "Pineapple", }, }, { "cursor": "YXJyYXljb25uZWN0aW9uOjI=", "node": { "id": to_base64("Fruit", 4), "color": "purple", "name": "Grape", }, }, ], "pageInfo": { "hasNextPage": True, "hasPreviousPage": True, "startCursor": to_base64("arrayconnection", "1"), "endCursor": to_base64("arrayconnection", "2"), }, }, } fruits_query_total_count = """ query TestQuery ( $first: Int = null $last: Int = null $before: String = null, $after: String = null, ) {{ {} ( first: $first last: $last before: $before after: $after ) {{ totalCount }} }} """ attrs = [ "fruits", "fruitsLazy", "fruitsWithFiltersAndOrder", "fruitsCustomResolver", "fruitsCustomResolverWithFiltersAndOrder", ] @pytest.mark.parametrize("query_attr", custom_attrs) def test_query_connection_total_count_sql_queries( django_assert_num_queries: DjangoAssertNumQueries, query_attr: str ): with django_assert_num_queries(1): result = schema.execute_sync( fruits_query_total_count.format(query_attr), variable_values={}, ) assert result.errors is None assert result.data == { query_attr: {"totalCount": 5}, } strawberry-graphql-django-0.62.0/tests/relay/test_nested_pagination.py000066400000000000000000000041001502405145400262540ustar00rootroot00000000000000import pytest from strawberry.relay import to_base64 from strawberry.relay.types import PREFIX from strawberry_django.optimizer import DjangoOptimizerExtension from tests import utils from tests.projects.faker import IssueFactory, MilestoneFactory @pytest.mark.django_db(transaction=True) def test_nested_pagination(gql_client: utils.GraphQLTestClient): # Nested pagination with the same arguments for the parent and child connections query = """ query testNestedConnectionPagination($first: Int, $after: String) { milestoneConn(first: $first, after: $after) { edges { node { id issuesWithFilters(first: $first, after: $after) { edges { node { id } } } } } } } """ # Create 4 milestones, each with 4 issues nested_data = { milestone: IssueFactory.create_batch(4, milestone=milestone) for milestone in MilestoneFactory.create_batch(4) } # Run the nested pagination query # We expect only 2 database queries if the optimizer is enabled, otherwise 3 (N+1) with utils.assert_num_queries(2 if DjangoOptimizerExtension.enabled.get() else 3): result = gql_client.query(query, {"first": 2, "after": to_base64(PREFIX, 0)}) # We expect the 2nd and 3rd milestones each with their 2nd and 3rd issues assert not result.errors assert result.data == { "milestoneConn": { "edges": [ { "node": { "id": to_base64("MilestoneType", milestone.id), "issuesWithFilters": { "edges": [ {"node": {"id": to_base64("IssueType", issue.id)}} for issue in issues[1:3] ] }, } } for milestone, issues in list(nested_data.items())[1:3] ] } } strawberry-graphql-django-0.62.0/tests/relay/test_query.py000066400000000000000000000022061502405145400237330ustar00rootroot00000000000000import pytest import strawberry from strawberry import relay import strawberry_django from tests.projects.models import Project @pytest.mark.parametrize("type_name", ["ProjectType", "PublicProjectObject"]) @pytest.mark.django_db(transaction=True) def test_correct_model_returned(type_name: str): @strawberry_django.type(Project) class ProjectType(relay.Node): name: relay.NodeID[str] due_date: strawberry.auto @strawberry_django.type(Project) class PublicProjectObject(relay.Node): name: relay.NodeID[str] due_date: strawberry.auto @strawberry.type class Query: node: relay.Node = relay.node() schema = strawberry.Schema(query=Query, types=[ProjectType, PublicProjectObject]) Project.objects.create(name="test") node_id = relay.to_base64(type_name, "test") result = schema.execute_sync( """ query NodeQuery($id: ID!) { node(id: $id) { __typename id } } """, {"id": node_id}, ) assert result.errors is None assert result.data == {"node": {"__typename": type_name, "id": node_id}} strawberry-graphql-django-0.62.0/tests/relay/test_schema.py000066400000000000000000000004331502405145400240260ustar00rootroot00000000000000import pathlib from pytest_snapshot.plugin import Snapshot from .schema import schema SNAPSHOTS_DIR = pathlib.Path(__file__).parent / "snapshots" def test_schema(snapshot: Snapshot): snapshot.snapshot_dir = SNAPSHOTS_DIR snapshot.assert_match(str(schema), "schema.gql") strawberry-graphql-django-0.62.0/tests/relay/test_types.py000066400000000000000000000115671502405145400237440ustar00rootroot00000000000000from typing import Any, Optional, Union, cast import pytest from strawberry import relay from strawberry.types.info import Info from typing_extensions import assert_type from .schema import Fruit, FruitModel, schema @pytest.fixture(autouse=True) def _fixtures(transactional_db): for pk, name, color in [ (1, "Banana", "yellow"), (2, "Apple", "red"), (3, "Pineapple", "yellow"), (4, "Grape", "purple"), (5, "Orange", "orange"), ]: FruitModel.objects.create( id=pk, name=name, color=color, ) class FakeInfo: schema = schema # We only need that info contains the schema for the tests fake_info = cast("Info", FakeInfo()) @pytest.mark.parametrize("type_name", [None, 1, 1.1]) def test_global_id_wrong_type_name(type_name: Any): with pytest.raises(relay.GlobalIDValueError): relay.GlobalID(type_name=type_name, node_id="foobar") @pytest.mark.parametrize("node_id", [None, 1, 1.1]) def test_global_id_wrong_type_node_id(node_id: Any): with pytest.raises(relay.GlobalIDValueError): relay.GlobalID(type_name="foobar", node_id=node_id) def test_global_id_from_id(): gid = relay.GlobalID.from_id("Zm9vYmFyOjE=") assert gid.type_name == "foobar" assert gid.node_id == "1" @pytest.mark.parametrize("value", ["foobar", ["Zm9vYmFy"], 123]) def test_global_id_from_id_error(value: Any): with pytest.raises(relay.GlobalIDValueError): relay.GlobalID.from_id(value) def test_global_id_resolve_type(): gid = relay.GlobalID(type_name="Fruit", node_id="1") type_ = gid.resolve_type(fake_info) assert type_ is Fruit def test_global_id_resolve_node_sync(): gid = relay.GlobalID(type_name="Fruit", node_id="1") fruit = gid.resolve_node_sync(fake_info) assert isinstance(fruit, FruitModel) assert fruit.pk == 1 assert fruit.name == "Banana" def test_global_id_resolve_node_sync_non_existing(): gid = relay.GlobalID(type_name="Fruit", node_id="999") fruit = gid.resolve_node_sync(fake_info) assert_type(fruit, Optional[relay.Node]) assert fruit is None def test_global_id_resolve_node_sync_non_existing_but_required(): gid = relay.GlobalID(type_name="Fruit", node_id="999") with pytest.raises(FruitModel.DoesNotExist): gid.resolve_node_sync(fake_info, required=True) def test_global_id_resolve_node_sync_ensure_type(): gid = relay.GlobalID(type_name="Fruit", node_id="1") fruit = gid.resolve_node_sync(fake_info, ensure_type=FruitModel) assert_type(fruit, FruitModel) assert isinstance(fruit, FruitModel) assert fruit.pk == 1 assert fruit.name == "Banana" def test_global_id_resolve_node_sync_ensure_type_with_union(): class Foo: ... gid = relay.GlobalID(type_name="Fruit", node_id="1") fruit = gid.resolve_node_sync(fake_info, ensure_type=Union[FruitModel, Foo]) assert_type(fruit, Union[FruitModel, Foo]) assert isinstance(fruit, FruitModel) assert fruit.pk == 1 assert fruit.name == "Banana" def test_global_id_resolve_node_sync_ensure_type_wrong_type(): class Foo: ... gid = relay.GlobalID(type_name="Fruit", node_id="1") with pytest.raises(TypeError): gid.resolve_node_sync(fake_info, ensure_type=Foo) async def test_global_id_resolve_node(): gid = relay.GlobalID(type_name="Fruit", node_id="1") fruit = await gid.resolve_node(fake_info) assert_type(fruit, Optional[relay.Node]) assert isinstance(fruit, FruitModel) assert fruit.pk == 1 assert fruit.name == "Banana" async def test_global_id_resolve_node_non_existing(): gid = relay.GlobalID(type_name="Fruit", node_id="999") fruit = await gid.resolve_node(fake_info) assert_type(fruit, Optional[relay.Node]) assert fruit is None async def test_global_id_resolve_node_non_existing_but_required(): gid = relay.GlobalID(type_name="Fruit", node_id="999") with pytest.raises(FruitModel.DoesNotExist): await gid.resolve_node(fake_info, required=True) async def test_global_id_resolve_node_ensure_type(): gid = relay.GlobalID(type_name="Fruit", node_id="1") fruit = await gid.resolve_node(fake_info, ensure_type=FruitModel) assert_type(fruit, FruitModel) assert isinstance(fruit, FruitModel) assert fruit.pk == 1 assert fruit.name == "Banana" async def test_global_id_resolve_node_ensure_type_with_union(): class Foo: ... gid = relay.GlobalID(type_name="Fruit", node_id="1") fruit = await gid.resolve_node(fake_info, ensure_type=Union[FruitModel, Foo]) assert_type(fruit, Union[FruitModel, Foo]) assert isinstance(fruit, FruitModel) assert fruit.pk == 1 assert fruit.name == "Banana" async def test_global_id_resolve_node_ensure_type_wrong_type(): class Foo: ... gid = relay.GlobalID(type_name="Fruit", node_id="1") with pytest.raises(TypeError): await gid.resolve_node(fake_info, ensure_type=Foo) strawberry-graphql-django-0.62.0/tests/relay/treenode/000077500000000000000000000000001502405145400227625ustar00rootroot00000000000000strawberry-graphql-django-0.62.0/tests/relay/treenode/__init__.py000066400000000000000000000000001502405145400250610ustar00rootroot00000000000000strawberry-graphql-django-0.62.0/tests/relay/treenode/a.py000066400000000000000000000012721502405145400235560ustar00rootroot00000000000000from typing import TYPE_CHECKING, Annotated import strawberry from strawberry import relay from typing_extensions import TypeAlias import strawberry_django from strawberry_django.relay import DjangoListConnection from .models import TreeNodeAuthor if TYPE_CHECKING: from .b import TreeNodeBookConnection @strawberry_django.type(TreeNodeAuthor) class TreeNodeAuthorType(relay.Node): name: str books: Annotated[ "TreeNodeBookConnection", strawberry.lazy("tests.relay.treenode.b") ] = strawberry_django.connection() children: "TreeNodeAuthorConnection" = strawberry_django.connection() TreeNodeAuthorConnection: TypeAlias = DjangoListConnection[TreeNodeAuthorType] strawberry-graphql-django-0.62.0/tests/relay/treenode/b.py000066400000000000000000000014221502405145400235540ustar00rootroot00000000000000from typing import TYPE_CHECKING, Annotated import strawberry from strawberry import relay from typing_extensions import TypeAlias import strawberry_django from strawberry_django.relay import DjangoListConnection from .models import TreeNodeBook if TYPE_CHECKING: from .a import TreeNodeAuthorType @strawberry_django.filter_type(TreeNodeBook) class TreeNodeBookFilter: name: str @strawberry_django.order(TreeNodeBook) class TreeNodeBookOrder: name: str @strawberry_django.type( TreeNodeBook, filters=TreeNodeBookFilter, order=TreeNodeBookOrder ) class TreeNodeBookType(relay.Node): name: str author: Annotated["TreeNodeAuthorType", strawberry.lazy("tests.relay.treenode.a")] TreeNodeBookConnection: TypeAlias = DjangoListConnection[TreeNodeBookType] strawberry-graphql-django-0.62.0/tests/relay/treenode/models.py000066400000000000000000000010721502405145400246170ustar00rootroot00000000000000from django.db import models from tree_queries.fields import TreeNodeForeignKey from tree_queries.models import TreeNode class TreeNodeAuthor(TreeNode): name = models.CharField(max_length=100) parent = TreeNodeForeignKey( to="self", on_delete=models.CASCADE, null=True, blank=True, related_name="children", ) class TreeNodeBook(models.Model): title = models.CharField(max_length=100) author = models.ForeignKey( TreeNodeAuthor, on_delete=models.CASCADE, related_name="books", ) strawberry-graphql-django-0.62.0/tests/relay/treenode/snapshots/000077500000000000000000000000001502405145400250045ustar00rootroot00000000000000strawberry-graphql-django-0.62.0/tests/relay/treenode/snapshots/test_lazy_annotations/000077500000000000000000000000001502405145400314375ustar00rootroot00000000000000test_lazy_type_annotations_in_schema/000077500000000000000000000000001502405145400410625ustar00rootroot00000000000000strawberry-graphql-django-0.62.0/tests/relay/treenode/snapshots/test_lazy_annotationsauthors_and_books_schema.gql000066400000000000000000000111121502405145400466070ustar00rootroot00000000000000strawberry-graphql-django-0.62.0/tests/relay/treenode/snapshots/test_lazy_annotations/test_lazy_type_annotations_in_schema"""An object with a Globally Unique ID""" interface Node { """The Globally Unique ID of this object""" id: ID! } """Information to aid in pagination.""" type PageInfo { """When paginating forwards, are there more items?""" hasNextPage: Boolean! """When paginating backwards, are there more items?""" hasPreviousPage: Boolean! """When paginating backwards, the cursor to continue.""" startCursor: String """When paginating forwards, the cursor to continue.""" endCursor: String } type Query { booksConn( filters: TreeNodeBookFilter order: TreeNodeBookOrder """Returns the items in the list that come before the specified cursor.""" before: String = null """Returns the items in the list that come after the specified cursor.""" after: String = null """Returns the first n items from the list.""" first: Int = null """Returns the items in the list that come after the specified cursor.""" last: Int = null ): TreeNodeBookTypeConnection! booksConn2( filters: TreeNodeBookFilter order: TreeNodeBookOrder """Returns the items in the list that come before the specified cursor.""" before: String = null """Returns the items in the list that come after the specified cursor.""" after: String = null """Returns the first n items from the list.""" first: Int = null """Returns the items in the list that come after the specified cursor.""" last: Int = null ): TreeNodeBookTypeConnection! authorsConn( """Returns the items in the list that come before the specified cursor.""" before: String = null """Returns the items in the list that come after the specified cursor.""" after: String = null """Returns the first n items from the list.""" first: Int = null """Returns the items in the list that come after the specified cursor.""" last: Int = null ): TreeNodeAuthorTypeConnection! authorsConn2( """Returns the items in the list that come before the specified cursor.""" before: String = null """Returns the items in the list that come after the specified cursor.""" after: String = null """Returns the first n items from the list.""" first: Int = null """Returns the items in the list that come after the specified cursor.""" last: Int = null ): TreeNodeAuthorTypeConnection! } type TreeNodeAuthorType implements Node { """The Globally Unique ID of this object""" id: ID! name: String! books( filters: TreeNodeBookFilter order: TreeNodeBookOrder """Returns the items in the list that come before the specified cursor.""" before: String = null """Returns the items in the list that come after the specified cursor.""" after: String = null """Returns the first n items from the list.""" first: Int = null """Returns the items in the list that come after the specified cursor.""" last: Int = null ): TreeNodeBookTypeConnection! children( """Returns the items in the list that come before the specified cursor.""" before: String = null """Returns the items in the list that come after the specified cursor.""" after: String = null """Returns the first n items from the list.""" first: Int = null """Returns the items in the list that come after the specified cursor.""" last: Int = null ): TreeNodeAuthorTypeConnection! } """A connection to a list of items.""" type TreeNodeAuthorTypeConnection { """Pagination data for this connection""" pageInfo: PageInfo! """Contains the nodes in this connection""" edges: [TreeNodeAuthorTypeEdge!]! """Total quantity of existing nodes.""" totalCount: Int } """An edge in a connection.""" type TreeNodeAuthorTypeEdge { """A cursor for use in pagination""" cursor: String! """The item at the end of the edge""" node: TreeNodeAuthorType! } input TreeNodeBookFilter { name: String! AND: TreeNodeBookFilter OR: TreeNodeBookFilter NOT: TreeNodeBookFilter DISTINCT: Boolean } input TreeNodeBookOrder { name: String } type TreeNodeBookType implements Node { """The Globally Unique ID of this object""" id: ID! name: String! author: TreeNodeAuthorType! } """A connection to a list of items.""" type TreeNodeBookTypeConnection { """Pagination data for this connection""" pageInfo: PageInfo! """Contains the nodes in this connection""" edges: [TreeNodeBookTypeEdge!]! """Total quantity of existing nodes.""" totalCount: Int } """An edge in a connection.""" type TreeNodeBookTypeEdge { """A cursor for use in pagination""" cursor: String! """The item at the end of the edge""" node: TreeNodeBookType! }strawberry-graphql-django-0.62.0/tests/relay/treenode/test_lazy_annotations.py000066400000000000000000000016671502405145400300010ustar00rootroot00000000000000import pathlib import strawberry from pytest_snapshot.plugin import Snapshot import strawberry_django from strawberry_django.relay import DjangoListConnection from .a import TreeNodeAuthorConnection, TreeNodeAuthorType from .b import TreeNodeBookConnection, TreeNodeBookType SNAPSHOTS_DIR = pathlib.Path(__file__).parent / "snapshots" def test_lazy_type_annotations_in_schema(snapshot: Snapshot): @strawberry.type class Query: books_conn: TreeNodeBookConnection = strawberry_django.connection() books_conn2: DjangoListConnection[TreeNodeBookType] = ( strawberry_django.connection() ) authors_conn: TreeNodeAuthorConnection = strawberry_django.connection() authors_conn2: DjangoListConnection[TreeNodeAuthorType] = ( strawberry_django.connection() ) schema = strawberry.Schema(query=Query) snapshot.assert_match(str(schema), "authors_and_books_schema.gql") strawberry-graphql-django-0.62.0/tests/relay/treenode/test_nested_children.py000066400000000000000000000066671502405145400275440ustar00rootroot00000000000000import pytest import strawberry from strawberry.relay.utils import to_base64 import strawberry_django from strawberry_django.optimizer import DjangoOptimizerExtension from .a import TreeNodeAuthorConnection from .models import TreeNodeAuthor @strawberry.type class Query: authors: TreeNodeAuthorConnection = strawberry_django.connection() schema = strawberry.Schema(query=Query, extensions=[DjangoOptimizerExtension]) @pytest.mark.django_db(transaction=True) def test_nested_children_total_count(): parent = TreeNodeAuthor.objects.create(name="Parent") child1 = TreeNodeAuthor.objects.create(name="Child1", parent=parent) child2 = TreeNodeAuthor.objects.create(name="Child2", parent=parent) query = """\ query { authors(first: 1) { totalCount edges { node { id name children { totalCount edges { node { id name } } } } } } } """ result = schema.execute_sync(query) assert not result.errors assert result.data == { "authors": { "totalCount": 3, "edges": [ { "node": { "id": to_base64("TreeNodeAuthorType", parent.pk), "name": "Parent", "children": { "totalCount": 2, "edges": [ { "node": { "id": to_base64( "TreeNodeAuthorType", child1.pk ), "name": "Child1", } }, { "node": { "id": to_base64( "TreeNodeAuthorType", child2.pk ), "name": "Child2", } }, ], }, } } ], } } @pytest.mark.django_db(transaction=True) def test_nested_children_total_count_no_children(): parent = TreeNodeAuthor.objects.create(name="Parent") query = """\ query { authors { totalCount edges { node { id name children { totalCount edges { node { id name } } } } } } } """ result = schema.execute_sync(query) assert not result.errors assert result.data == { "authors": { "totalCount": 1, "edges": [ { "node": { "id": to_base64("TreeNodeAuthorType", parent.pk), "name": "Parent", "children": { "totalCount": 0, "edges": [], }, } } ], } } strawberry-graphql-django-0.62.0/tests/schema.py000066400000000000000000000003441502405145400216540ustar00rootroot00000000000000import typing import strawberry @strawberry.type class Query: @strawberry.field def hello(self, name: typing.Optional[str] = None) -> str: return f"Hello {name or 'world'}" schema = strawberry.Schema(Query) strawberry-graphql-django-0.62.0/tests/test_apps.py000066400000000000000000000004001502405145400224070ustar00rootroot00000000000000from strawberry_django.apps import StrawberryDjangoConfig def test_app_name() -> None: assert StrawberryDjangoConfig.name == "strawberry_django" def test_verbose_name() -> None: assert StrawberryDjangoConfig.verbose_name == "Strawberry django" strawberry-graphql-django-0.62.0/tests/test_commands.py000066400000000000000000000021601502405145400232520ustar00rootroot00000000000000import textwrap from io import StringIO from unittest.mock import patch import pytest from django.core.management import call_command from django.core.management.base import CommandError class _FakeSchema: pass def test_django_export_schema(): out = StringIO() call_command("export_schema", "tests.schema", stdout=out) output = out.getvalue() assert output expected = """\ type Query { hello(name: String = null): String! } """ assert output == textwrap.dedent(expected) def test_django_export_schema_exception_handle(): with pytest.raises( CommandError, match=r"No module named 'tests.fake_schema'", ): call_command("export_schema", "tests.fake_schema") mock_import_module = patch( "strawberry_django.management.commands.export_schema.import_module_symbol", return_value=_FakeSchema(), ) with ( mock_import_module, pytest.raises( CommandError, match=r"The `schema` must be an instance of strawberry.Schema", ), ): call_command("export_schema", "tests.schema") strawberry-graphql-django-0.62.0/tests/test_descriptors.py000066400000000000000000000043621502405145400240200ustar00rootroot00000000000000import textwrap import strawberry from asgiref.sync import sync_to_async import strawberry_django from tests import models def test_model_property(transactional_db): @strawberry_django.type(models.Fruit) class Fruit: name: strawberry.auto name_length: strawberry.auto @strawberry.type class Query: fruit: Fruit = strawberry_django.field() schema = strawberry.Schema(query=Query) assert ( textwrap.dedent(str(schema)) == textwrap.dedent( """\ type Fruit { name: String! nameLength: Int! } type Query { fruit(pk: ID!): Fruit! } """, ).strip() ) fruit1 = models.Fruit.objects.create(name="Banana") fruit2 = models.Fruit.objects.create(name="Apple") query = """ query Fruit($pk: ID!) { fruit(pk: $pk) { name nameLength } } """ for pk, name, length in [(fruit1.pk, "Banana", 6), (fruit2.pk, "Apple", 5)]: result = schema.execute_sync(query, variable_values={"pk": pk}) assert result.errors is None assert result.data == {"fruit": {"name": name, "nameLength": length}} async def test_model_property_async(transactional_db): @strawberry_django.type(models.Fruit) class Fruit: name: strawberry.auto name_length: strawberry.auto @strawberry.type class Query: fruit: Fruit = strawberry_django.field() schema = strawberry.Schema(query=Query) assert ( textwrap.dedent(str(schema)) == textwrap.dedent( """\ type Fruit { name: String! nameLength: Int! } type Query { fruit(pk: ID!): Fruit! } """, ).strip() ) fruit1 = await sync_to_async(models.Fruit.objects.create)(name="Banana") fruit2 = await sync_to_async(models.Fruit.objects.create)(name="Apple") query = """ query Fruit($pk: ID!) { fruit(pk: $pk) { name nameLength } } """ for pk, name, length in [(fruit1.pk, "Banana", 6), (fruit2.pk, "Apple", 5)]: result = await schema.execute(query, variable_values={"pk": pk}) assert result.errors is None assert result.data == {"fruit": {"name": name, "nameLength": length}} strawberry-graphql-django-0.62.0/tests/test_deterministic_ordering.py000066400000000000000000000251731502405145400262160ustar00rootroot00000000000000import pytest from django.db import connection from django.test.utils import CaptureQueriesContext from strawberry.relay.utils import to_base64 from tests import utils from tests.projects.faker import ( FavoriteFactory, IssueFactory, MilestoneFactory, ProjectFactory, QuizFactory, UserFactory, ) from tests.projects.models import Favorite, Milestone, Project, Quiz @pytest.mark.django_db(transaction=True) def test_required(gql_client: utils.GraphQLTestClient): # Query a required field, and a nested required field with no ordering # We expect the queries to **not** have an `ORDER BY` clause query = """ query testRequired($id: ID!) { projectMandatory(id: $id) { name firstMilestoneRequired { name } } } """ # Sanity check the models assert Project._meta.ordering == [] assert Milestone._meta.ordering == [] # Create a project and a milestone project = ProjectFactory() milestone = MilestoneFactory(project=project) # Run the query # Capture the SQL queries that are executed with CaptureQueriesContext(connection) as ctx: result = gql_client.query( query, variables={"id": to_base64("ProjectType", project.pk)}, ) # Sanity check the results assert not result.errors assert result.data == { "projectMandatory": { "name": project.name, "firstMilestoneRequired": {"name": milestone.name}, } } # Assert that the queries do **not** have an `ORDER BY` clause for query in ctx.captured_queries: assert "ORDER BY" not in query["sql"] @pytest.mark.django_db(transaction=True) def test_optional(gql_client: utils.GraphQLTestClient): # Query an optional field, and a nested optional field with no ordering # We expect the queries to have an `ORDER BY` clause query = """ query testOptional($id: ID!) { project(id: $id) { name firstMilestone { name } } } """ # Sanity check the models assert Project._meta.ordering == [] assert Milestone._meta.ordering == [] # Create a project and a milestone project = ProjectFactory() milestone = MilestoneFactory(project=project) # Run the query # Capture the SQL queries that are executed with CaptureQueriesContext(connection) as ctx: result = gql_client.query( query, variables={"id": to_base64("ProjectType", project.pk)}, ) # Sanity check the results assert not result.errors assert result.data == { "project": { "name": project.name, "firstMilestone": {"name": milestone.name}, } } # Assert that the queries do have an `ORDER BY` clause for query in ctx.captured_queries: assert "ORDER BY" in query["sql"] @pytest.mark.django_db(transaction=True) def test_list(gql_client: utils.GraphQLTestClient): # Query a list field, and a nested list field with no ordering # We expect the queries to have an `ORDER BY` clause query = """ query testList{ projectList { name milestones { name } } } """ # Sanity check the models assert Project._meta.ordering == [] assert Milestone._meta.ordering == [] # Create some projects and milestones projects = ProjectFactory.create_batch(3) milestones = [] for project in projects: milestones.extend(MilestoneFactory.create_batch(3, project=project)) # Run the query # Capture the SQL queries that are executed with CaptureQueriesContext(connection) as ctx: result = gql_client.query(query) # Sanity check the results assert not result.errors assert result.data == { "projectList": [ { "name": project.name, "milestones": [ {"name": milestone.name} for milestone in project.milestones.order_by("pk") ], } for project in Project.objects.order_by("pk") ] } # Assert that the queries do have an `ORDER BY` clause for query in ctx.captured_queries: assert "ORDER BY" in query["sql"] @pytest.mark.django_db(transaction=True) def test_connection(gql_client: utils.GraphQLTestClient): # Query a connection field, and a nested connection field with no ordering # We expect the queries to have an `ORDER BY` clause query = """ query testConnection{ projectConn { edges { node { name milestoneConn { edges { node { name } } } } } } } """ # Sanity check the models assert Project._meta.ordering == [] assert Milestone._meta.ordering == [] # Create some projects and milestones projects = ProjectFactory.create_batch(3) milestones = [] for project in projects: milestones.extend(MilestoneFactory.create_batch(3, project=project)) # Run the query # Capture the SQL queries that are executed with CaptureQueriesContext(connection) as ctx: result = gql_client.query(query) # Sanity check the results assert not result.errors assert result.data == { "projectConn": { "edges": [ { "node": { "name": project.name, "milestoneConn": { "edges": [ {"node": {"name": milestone.name}} for milestone in project.milestones.order_by("pk") ] }, } } for project in Project.objects.order_by("pk") ] } } # Assert that the queries do have an `ORDER BY` clause for query in ctx.captured_queries: assert "ORDER BY" in query["sql"] @pytest.mark.django_db(transaction=True) def test_paginated(gql_client: utils.GraphQLTestClient): # Query a paginated field, and a nested paginated field with no ordering # We expect the queries to have an `ORDER BY` clause query = """ query testPaginated{ projectsPaginated { results { name milestonesPaginated { results { name } } } } } """ # Sanity check the models assert Project._meta.ordering == [] assert Milestone._meta.ordering == [] # Create some projects and milestones projects = ProjectFactory.create_batch(3) milestones = [] for project in projects: milestones.extend(MilestoneFactory.create_batch(3, project=project)) # Run the query # Capture the SQL queries that are executed with CaptureQueriesContext(connection) as ctx: result = gql_client.query(query) # Sanity check the results assert not result.errors assert result.data == { "projectsPaginated": { "results": [ { "name": project.name, "milestonesPaginated": { "results": [ {"name": milestone.name} for milestone in project.milestones.order_by("pk") ] }, } for project in Project.objects.order_by("pk") ] } } # Assert that the queries do have an `ORDER BY` clause for query in ctx.captured_queries: assert "ORDER BY" in query["sql"] @pytest.mark.django_db(transaction=True) def test_default_ordering(gql_client: utils.GraphQLTestClient): # Query a field for a model with default ordering # We expect the default ordering to be respected query = """ query testDefaultOrdering{ favoriteConn { edges { node { name } } } } """ # Sanity check the model assert Favorite._meta.ordering == ("name",) # Create some favorites # Ensure the names are in reverse order to the primary keys user = UserFactory() issue = IssueFactory() favorites = [ FavoriteFactory(name=name, user=user, issue=issue) for name in ["c", "b", "a"] ] # Run the query # Note that we need to login to access the favorites with gql_client.login(user): result = gql_client.query(query) # Sanity check the results # We expect the favorites to be ordered by name assert not result.errors assert result.data == { "favoriteConn": { "edges": [ {"node": {"name": favorite.name}} for favorite in reversed(favorites) ] } } @pytest.mark.django_db(transaction=True) def test_get_queryset_ordering(gql_client: utils.GraphQLTestClient): # Query a field for a type with a `get_queryset` method that applies ordering # We expect the ordering to be respected query = """ query testGetQuerySetOrdering{ quizList { title } } """ # Sanity check the model assert Quiz._meta.ordering == [] # Create some quizzes # Ensure the titles are in reverse order to the primary keys quizzes = [QuizFactory(title=title) for title in ["c", "b", "a"]] # Run the query result = gql_client.query(query) # Sanity check the results # We expect the quizzes to be ordered by title assert not result.errors assert result.data == { "quizList": [{"title": quiz.title} for quiz in reversed(quizzes)] } @pytest.mark.django_db(transaction=True) def test_graphql_ordering(gql_client: utils.GraphQLTestClient): # Query a field for a type that allows ordering via GraphQL # We expect the ordering to be respected query = """ query testGraphQLOrdering{ milestoneList(order: { name: ASC }) { name } } """ # Sanity check the model assert Milestone._meta.ordering == [] # Create some milestones # Ensure the names are in reverse order to the primary keys milestones = [MilestoneFactory(name=name) for name in ["c", "b", "a"]] # Run the query result = gql_client.query(query) # Sanity check the results # We expect the milestones to be ordered by name assert not result.errors assert result.data == { "milestoneList": [ {"name": milestone.name} for milestone in reversed(milestones) ] } strawberry-graphql-django-0.62.0/tests/test_enums.py000066400000000000000000000300361502405145400226030ustar00rootroot00000000000000import textwrap from typing import cast import pytest import strawberry from django.db import models from django.test import override_settings from django.utils.translation import gettext_lazy from django_choices_field import IntegerChoicesField, TextChoicesField from pytest_mock import MockerFixture import strawberry_django from strawberry_django import mutations from strawberry_django.fields import types from strawberry_django.fields.types import field_type_map from strawberry_django.settings import strawberry_django_settings class Choice(models.TextChoices): """Choice description.""" A = "a", "A description" B = "b", "B description" C = "c", gettext_lazy("C description") D = "12d-d'éléphant_🐘", "D description" E = "_2d_d__l_phant__", "E description" __empty__ = "Empty" class IntegerChoice(models.IntegerChoices): """IntegerChoice description.""" X = 1, "1 description" Y = 2, "2 description" Z = 3, gettext_lazy("3 description") class ChoicesModel(models.Model): attr1 = TextChoicesField(choices_enum=Choice) attr2 = IntegerChoicesField(choices_enum=IntegerChoice) attr3 = models.CharField( max_length=255, choices=[ ("c", "C description"), ("d", gettext_lazy("D description")), ], ) attr4 = models.IntegerField( choices=[ (4, "4 description"), (5, gettext_lazy("5 description")), ], ) attr5 = models.CharField( max_length=255, choices=Choice.choices, ) attr6 = models.IntegerField( choices=IntegerChoice.choices, ) class ChoicesWithExtraFieldsModel(models.Model): attr1 = TextChoicesField(choices_enum=Choice) attr2 = IntegerChoicesField(choices_enum=IntegerChoice) attr3 = models.CharField( max_length=255, choices=[ ("c", "C description"), ("d", gettext_lazy("D description")), ], ) attr4 = models.IntegerField( choices=[ (4, "4 description"), (5, gettext_lazy("5 description")), ], ) attr5 = models.CharField( max_length=255, choices=Choice.choices, ) attr6 = models.IntegerField( choices=IntegerChoice.choices, ) extra1 = models.CharField(max_length=255) extra2 = models.PositiveIntegerField() def test_choices_field(): @strawberry_django.type(ChoicesModel) class ChoicesType: attr1: strawberry.auto attr2: strawberry.auto attr3: strawberry.auto attr4: strawberry.auto attr5: strawberry.auto attr6: strawberry.auto @strawberry.type class Query: @strawberry_django.field def obj(self) -> ChoicesType: return cast( "ChoicesType", ChoicesModel( attr1=Choice.A, attr2=IntegerChoice.X, attr3="c", attr4=4, attr5=Choice.A, attr6=IntegerChoice.X, ), ) expected = """\ enum Choice { A B C D E } type ChoicesType { attr1: Choice! attr2: IntegerChoice! attr3: String! attr4: Int! attr5: String! attr6: Int! } enum IntegerChoice { X Y Z } type Query { obj: ChoicesType! } """ schema = strawberry.Schema(query=Query) assert textwrap.dedent(str(schema)) == textwrap.dedent(expected).strip() result = schema.execute_sync( "query { obj { attr1, attr2, attr3, attr4, attr5, attr6 }}", ) assert result.errors is None assert result.data == { "obj": { "attr1": "A", "attr2": "X", "attr3": "c", "attr4": 4, "attr5": "a", "attr6": 1, }, } def test_no_choices_enum(mocker: MockerFixture): # We can't use patch with the module name directly as it tries to resolve # strawberry.fields as a function instead of the module for python versions # before 3.11 mocker.patch.object(types, "TextChoicesField", None) mocker.patch.dict(field_type_map, {TextChoicesField: str}) mocker.patch.object(types, "IntegerChoicesField", None) mocker.patch.dict(field_type_map, {IntegerChoicesField: str}) @strawberry_django.type(ChoicesModel) class ChoicesType: attr1: strawberry.auto attr2: strawberry.auto attr3: strawberry.auto attr4: strawberry.auto attr5: strawberry.auto attr6: strawberry.auto @strawberry.type class Query: @strawberry_django.field def obj(self) -> ChoicesType: return cast( "ChoicesType", ChoicesModel( attr1=Choice.A, attr2=IntegerChoice.X, attr3="c", attr4=4, attr5=Choice.A, attr6=IntegerChoice.X, ), ) expected = """\ type ChoicesType { attr1: String! attr2: String! attr3: String! attr4: Int! attr5: String! attr6: Int! } type Query { obj: ChoicesType! } """ schema = strawberry.Schema(query=Query) assert textwrap.dedent(str(schema)) == textwrap.dedent(expected).strip() result = schema.execute_sync( "query { obj { attr1, attr2, attr3, attr4, attr5, attr6 }}", ) assert result.errors is None assert result.data == { "obj": { "attr1": "a", "attr2": "1", "attr3": "c", "attr4": 4, "attr5": "a", "attr6": 1, }, } @override_settings( STRAWBERRY_DJANGO={ **strawberry_django_settings(), "GENERATE_ENUMS_FROM_CHOICES": True, }, ) def test_generate_choices_from_enum(): @strawberry_django.type(ChoicesModel) class ChoicesType: attr1: strawberry.auto attr2: strawberry.auto attr3: strawberry.auto attr4: strawberry.auto attr5: strawberry.auto attr6: strawberry.auto @strawberry.type class Query: @strawberry_django.field def obj(self) -> ChoicesType: return cast( "ChoicesType", ChoicesModel( attr1=Choice.A, attr2=IntegerChoice.X, attr3="c", attr4=4, attr5=Choice.A, attr6=IntegerChoice.X, ), ) expected = '''\ enum Choice { A B C D E } type ChoicesType { attr1: Choice! attr2: IntegerChoice! attr3: TestsChoicesModelAttr3Enum! attr4: Int! attr5: TestsChoicesModelAttr5Enum! attr6: Int! } enum IntegerChoice { X Y Z } type Query { obj: ChoicesType! } enum TestsChoicesModelAttr3Enum { """C description""" c """D description""" d } enum TestsChoicesModelAttr5Enum { """A description""" a """B description""" b """C description""" c """D description""" _2d_d__l_phant__ """E description""" _2d_d__l_phant___ } ''' schema = strawberry.Schema(query=Query) assert textwrap.dedent(str(schema)) == textwrap.dedent(expected).strip() result = schema.execute_sync( "query { obj { attr1, attr2, attr3, attr4, attr5, attr6 }}", ) assert result.errors is None assert result.data == { "obj": { "attr1": "A", "attr2": "X", "attr3": "c", "attr4": 4, "attr5": "a", "attr6": 1, }, } @override_settings( STRAWBERRY_DJANGO={ **strawberry_django_settings(), "GENERATE_ENUMS_FROM_CHOICES": True, }, ) def test_generate_choices_from_enum_with_extra_fields(): @strawberry_django.type(ChoicesWithExtraFieldsModel) class ChoicesWithExtraFieldsType: attr1: strawberry.auto attr2: strawberry.auto attr3: strawberry.auto attr4: strawberry.auto attr5: strawberry.auto attr6: strawberry.auto extra1: strawberry.auto extra2: strawberry.auto @strawberry.type class Query: @strawberry_django.field def obj(self) -> ChoicesWithExtraFieldsType: return cast( "ChoicesWithExtraFieldsType", ChoicesWithExtraFieldsModel( attr1=Choice.A, attr2=IntegerChoice.X, attr3="c", attr4=4, attr5=Choice.A, attr6=IntegerChoice.X, extra1="str1", extra2=99, ), ) expected = '''\ enum Choice { A B C D E } type ChoicesWithExtraFieldsType { attr1: Choice! attr2: IntegerChoice! attr3: TestsChoicesWithExtraFieldsModelAttr3Enum! attr4: Int! attr5: TestsChoicesWithExtraFieldsModelAttr5Enum! attr6: Int! extra1: String! extra2: Int! } enum IntegerChoice { X Y Z } type Query { obj: ChoicesWithExtraFieldsType! } enum TestsChoicesWithExtraFieldsModelAttr3Enum { """C description""" c """D description""" d } enum TestsChoicesWithExtraFieldsModelAttr5Enum { """A description""" a """B description""" b """C description""" c """D description""" _2d_d__l_phant__ """E description""" _2d_d__l_phant___ } ''' schema = strawberry.Schema(query=Query) assert textwrap.dedent(str(schema)) == textwrap.dedent(expected).strip() result = schema.execute_sync( "query { obj { attr1, attr2, attr3, attr4, attr5, attr6, extra1, extra2 }}", ) assert result.errors is None assert result.data == { "obj": { "attr1": "A", "attr2": "X", "attr3": "c", "attr4": 4, "attr5": "a", "attr6": 1, "extra1": "str1", "extra2": 99, }, } @override_settings( STRAWBERRY_DJANGO={ **strawberry_django_settings(), "GENERATE_ENUMS_FROM_CHOICES": True, }, ) @pytest.mark.django_db(transaction=True) def test_create_mutation_with_generated_enum_input(db): @strawberry_django.type(ChoicesModel) class ChoicesType: attr1: strawberry.auto attr2: strawberry.auto attr3: strawberry.auto attr4: strawberry.auto attr5: strawberry.auto attr6: strawberry.auto @strawberry_django.input(ChoicesModel) class ChoicesInput: attr1: strawberry.auto attr2: strawberry.auto attr3: strawberry.auto attr4: strawberry.auto attr5: strawberry.auto attr6: strawberry.auto @strawberry.type class Query: choice: ChoicesType = strawberry_django.field() @strawberry.type class Mutation: create_choice: ChoicesType = mutations.create( ChoicesInput, handle_django_errors=True, argument_name="input" ) schema = strawberry.Schema(query=Query, mutation=Mutation) variables = { "input": { "attr1": "A", "attr2": "X", "attr3": Choice.C, "attr4": 4, "attr5": "a", "attr6": 1, } } result = schema.execute_sync( """ mutation CreateChoice($input: ChoicesInput!) { createChoice(input: $input) { ... on OperationInfo { messages { kind field message } } ... on ChoicesType { attr3 } } } """, variables, ) assert result.data == {"createChoice": {"attr3": "c"}} strawberry-graphql-django-0.62.0/tests/test_field_permissions.py000066400000000000000000000156671502405145400252070ustar00rootroot00000000000000from collections.abc import Awaitable from typing import Any, Union import pytest import strawberry from strawberry import BasePermission, Info import strawberry_django from strawberry_django.optimizer import DjangoOptimizerExtension from tests import models @pytest.mark.django_db(transaction=True) async def test_with_async_permission(db): class AsyncPermission(BasePermission): async def has_permission( # type: ignore self, source: Any, info: Info, **kwargs: Any, ) -> Union[bool, Awaitable[bool]]: return True @strawberry_django.type(models.Fruit) class Fruit: name: strawberry.auto @strawberry_django.type(models.Color) class Color: name: strawberry.auto fruits: list[Fruit] = strawberry_django.field( permission_classes=[AsyncPermission] ) @strawberry.type(name="Query") class Query: colors: list[Color] = strawberry_django.field() red = await models.Color.objects.acreate(name="Red") yellow = await models.Color.objects.acreate(name="Yellow") await models.Fruit.objects.acreate(name="Apple", color=red) await models.Fruit.objects.acreate(name="Banana", color=yellow) await models.Fruit.objects.acreate(name="Strawberry", color=red) schema = strawberry.Schema(query=Query) query = """ query { colors { name fruits { name } } } """ result = await schema.execute(query) assert result.errors is None assert result.data == { "colors": [ { "name": "Red", "fruits": [ {"name": "Apple"}, {"name": "Strawberry"}, ], }, { "name": "Yellow", "fruits": [{"name": "Banana"}], }, ] } @pytest.mark.django_db(transaction=True) async def test_with_async_permission_and_optimizer(db): class AsyncPermission(BasePermission): async def has_permission( # type: ignore self, source: Any, info: Info, **kwargs: Any, ) -> Union[bool, Awaitable[bool]]: return True @strawberry_django.type(models.Fruit) class Fruit: name: strawberry.auto @strawberry_django.type(models.Color) class Color: name: strawberry.auto fruits: list[Fruit] = strawberry_django.field( permission_classes=[AsyncPermission] ) @strawberry.type(name="Query") class Query: colors: list[Color] = strawberry_django.field() red = await models.Color.objects.acreate(name="Red") yellow = await models.Color.objects.acreate(name="Yellow") await models.Fruit.objects.acreate(name="Apple", color=red) await models.Fruit.objects.acreate(name="Banana", color=yellow) await models.Fruit.objects.acreate(name="Strawberry", color=red) schema = strawberry.Schema( query=Query, extensions=[DjangoOptimizerExtension()], ) query = """ query { colors { name fruits { name } } } """ result = await schema.execute(query) assert result.errors is None assert result.data == { "colors": [ { "name": "Red", "fruits": [ {"name": "Apple"}, {"name": "Strawberry"}, ], }, { "name": "Yellow", "fruits": [{"name": "Banana"}], }, ] } @pytest.mark.django_db(transaction=True) def test_with_sync_permission(db): class AsyncPermission(BasePermission): def has_permission( self, source: Any, info: Info, **kwargs: Any, ) -> Union[bool, Awaitable[bool]]: return True @strawberry_django.type(models.Fruit) class Fruit: name: strawberry.auto @strawberry_django.type(models.Color) class Color: name: strawberry.auto fruits: list[Fruit] = strawberry_django.field( permission_classes=[AsyncPermission] ) @strawberry.type(name="Query") class Query: colors: list[Color] = strawberry_django.field() red = models.Color.objects.create(name="Red") yellow = models.Color.objects.create(name="Yellow") models.Fruit.objects.create(name="Apple", color=red) models.Fruit.objects.create(name="Banana", color=yellow) models.Fruit.objects.create(name="Strawberry", color=red) schema = strawberry.Schema(query=Query) query = """ query { colors { name fruits { name } } } """ result = schema.execute_sync(query) assert result.errors is None assert result.data == { "colors": [ { "name": "Red", "fruits": [ {"name": "Apple"}, {"name": "Strawberry"}, ], }, { "name": "Yellow", "fruits": [{"name": "Banana"}], }, ] } @pytest.mark.django_db(transaction=True) def test_with_sync_permission_and_optimizer(db): class AsyncPermission(BasePermission): def has_permission( self, source: Any, info: Info, **kwargs: Any, ) -> Union[bool, Awaitable[bool]]: return True @strawberry_django.type(models.Fruit) class Fruit: name: strawberry.auto @strawberry_django.type(models.Color) class Color: name: strawberry.auto fruits: list[Fruit] = strawberry_django.field( permission_classes=[AsyncPermission] ) @strawberry.type(name="Query") class Query: colors: list[Color] = strawberry_django.field() red = models.Color.objects.create(name="Red") yellow = models.Color.objects.create(name="Yellow") models.Fruit.objects.create(name="Apple", color=red) models.Fruit.objects.create(name="Banana", color=yellow) models.Fruit.objects.create(name="Strawberry", color=red) schema = strawberry.Schema( query=Query, extensions=[DjangoOptimizerExtension()], ) query = """ query { colors { name fruits { name } } } """ result = schema.execute_sync(query) assert result.errors is None assert result.data == { "colors": [ { "name": "Red", "fruits": [ {"name": "Apple"}, {"name": "Strawberry"}, ], }, { "name": "Yellow", "fruits": [{"name": "Banana"}], }, ] } strawberry-graphql-django-0.62.0/tests/test_input_mutations.py000066400000000000000000001350261502405145400247230ustar00rootroot00000000000000from unittest.mock import patch import pytest from django.core.exceptions import ValidationError from strawberry.relay import from_base64, to_base64 from strawberry_django.optimizer import DjangoOptimizerExtension from tests.utils import GraphQLTestClient, assert_num_queries from .projects.faker import ( IssueFactory, MilestoneFactory, ProjectFactory, TagFactory, UserFactory, ) from .projects.models import Issue, Milestone, Project, Tag @pytest.mark.django_db(transaction=True) def test_input_mutation(db, gql_client: GraphQLTestClient): query = """ mutation CreateProject ($input: CreateProjectInput!) { createProject (input: $input) { ... on ProjectType { name cost dueDate } } } """ with assert_num_queries(1): res = gql_client.query( query, { "input": { "name": "Some Project", "cost": "12.50", "dueDate": "2030-01-01", }, }, ) assert res.data == { "createProject": { "name": "Some Project", # The cost is properly set, but this user doesn't have # permission to see it "cost": None, "dueDate": "2030-01-01", }, } @pytest.mark.django_db(transaction=True) def test_input_mutation_with_internal_error_code(db, gql_client: GraphQLTestClient): query = """ mutation CreateProject ($input: CreateProjectInput!) { createProject (input: $input) { ... on ProjectType { name cost } ... on OperationInfo { messages { field message kind code } } } } """ with assert_num_queries(0): res = gql_client.query( query, {"input": {"name": 100 * "way to long", "cost": "10.40"}}, ) assert res.data == { "createProject": { "messages": [ { "field": "name", "kind": "VALIDATION", "message": ( "Ensure this value has at most 255 characters (it has" " 1100)." ), "code": "max_length", }, ], }, } @pytest.mark.django_db(transaction=True) def test_input_mutation_with_explicit_error_code(db, gql_client: GraphQLTestClient): query = """ mutation CreateProject ($input: CreateProjectInput!) { createProject (input: $input) { ... on ProjectType { name cost } ... on OperationInfo { messages { field message kind code } } } } """ with assert_num_queries(0): res = gql_client.query( query, {"input": {"name": "Some Project", "cost": "-1"}}, ) assert res.data == { "createProject": { "messages": [ { "field": "cost", "kind": "VALIDATION", "message": "Cost cannot be lower than zero", "code": "min_cost", }, ], }, } @pytest.mark.django_db(transaction=True) def test_input_mutation_with_errors(db, gql_client: GraphQLTestClient): query = """ mutation CreateProject ($input: CreateProjectInput!) { createProject (input: $input) { ... on ProjectType { name cost } ... on OperationInfo { messages { field message kind code } } } } """ with assert_num_queries(0): res = gql_client.query( query, {"input": {"name": "Some Project", "cost": "501.50"}}, ) assert res.data == { "createProject": { "messages": [ { "field": "cost", "kind": "VALIDATION", "message": "Cost cannot be higher than 500", "code": None, }, ], }, } @pytest.mark.django_db(transaction=True) def test_input_create_mutation(db, gql_client: GraphQLTestClient): query = """ mutation CreateIssue ($input: IssueInput!) { createIssue (input: $input) { __typename ... on OperationInfo { messages { kind field message } } ... on IssueType { id name milestone { id name } priority kind tags { id name } } } } """ milestone = MilestoneFactory.create() tags = TagFactory.create_batch(4) res = gql_client.query( query, { "input": { "name": "Some Issue", "milestone": {"id": to_base64("MilestoneType", milestone.pk)}, "priority": 5, "kind": Issue.Kind.FEATURE.value, "tags": [{"id": to_base64("TagType", t.pk)} for t in tags], }, }, ) assert res.data assert isinstance(res.data["createIssue"], dict) typename, pk = from_base64(res.data["createIssue"].pop("id")) assert typename == "IssueType" assert {frozenset(t.items()) for t in res.data["createIssue"].pop("tags")} == { frozenset({"id": to_base64("TagType", t.pk), "name": t.name}.items()) for t in tags } assert res.data == { "createIssue": { "__typename": "IssueType", "name": "Some Issue", "milestone": { "id": to_base64("MilestoneType", milestone.pk), "name": milestone.name, }, "priority": 5, "kind": "f", }, } issue = Issue.objects.get(pk=pk) assert issue.name == "Some Issue" assert issue.priority == 5 assert issue.kind == Issue.Kind.FEATURE assert issue.milestone == milestone assert set(issue.tags.all()) == set(tags) @pytest.mark.django_db(transaction=True) def test_input_create_mutation_nested_creation(db, gql_client: GraphQLTestClient): query = """ mutation CreateIssue ($input: IssueInput!) { createIssue (input: $input) { __typename ... on OperationInfo { messages { kind field message } } ... on IssueType { id name milestone { id name project { id name } } } } } """ assert not Project.objects.filter(name="New Project").exists() assert not Milestone.objects.filter(name="New Milestone").exists() assert not Issue.objects.filter(name="New Issue").exists() res = gql_client.query( query, { "input": { "name": "New Issue", "milestone": { "name": "New Milestone", "project": { "name": "New Project", }, }, }, }, ) assert res.data assert isinstance(res.data["createIssue"], dict) typename, pk = from_base64(res.data["createIssue"]["id"]) assert typename == "IssueType" issue = Issue.objects.get(pk=pk) assert issue.name == "New Issue" milestone = Milestone.objects.get(name="New Milestone") assert milestone.name == "New Milestone" project = Project.objects.get(name="New Project") assert project.name == "New Project" assert milestone.project_id == project.pk assert issue.milestone_id == milestone.pk assert res.data == { "createIssue": { "__typename": "IssueType", "id": to_base64("IssueType", issue.pk), "name": "New Issue", "milestone": { "id": to_base64("MilestoneType", milestone.pk), "name": "New Milestone", "project": { "id": to_base64("ProjectType", project.pk), "name": "New Project", }, }, }, } @pytest.mark.django_db(transaction=True) def test_input_create_with_m2m_mutation(db, gql_client: GraphQLTestClient): query = """ mutation CreateMilestone ($input: MilestoneInput!) { createMilestone (input: $input) { __typename ... on OperationInfo { messages { kind field message } } ... on MilestoneType { id name project { id name } issues { id name } } } } """ project = ProjectFactory.create() res = gql_client.query( query, { "input": { "name": "Some Milestone", "project": { "id": to_base64("ProjectType", project.pk), }, "issues": [ { "name": "Milestone Issue 1", }, { "name": "Milestone Issue 2", }, ], }, }, ) assert res.data assert isinstance(res.data["createMilestone"], dict) typename, pk = from_base64(res.data["createMilestone"].pop("id")) assert typename == "MilestoneType" issues = res.data["createMilestone"].pop("issues") assert {i["name"] for i in issues} == {"Milestone Issue 1", "Milestone Issue 2"} assert res.data == { "createMilestone": { "__typename": "MilestoneType", "name": "Some Milestone", "project": { "id": to_base64("ProjectType", project.pk), "name": project.name, }, }, } milestone = Milestone.objects.get(pk=pk) assert milestone.name == "Some Milestone" assert milestone.project == project assert {i.name for i in milestone.issues.all()} == { "Milestone Issue 1", "Milestone Issue 2", } @pytest.mark.django_db(transaction=True) def test_input_create_mutation_with_multiple_level_nested_creation( db, gql_client: GraphQLTestClient ): query = """ mutation createProjectWithMilestones ($input: ProjectInputPartial!) { createProjectWithMilestones (input: $input) { __typename ... on OperationInfo { messages { kind field message } } ... on ProjectType { id name milestones { id name issues { id name tags { name } } } } } } """ shared_tag = TagFactory.create(name="Shared Tag") shared_tag_id = to_base64("TagType", shared_tag.pk) res = gql_client.query( query, { "input": { "name": "Some Project", "milestones": [ { "name": "Some Milestone", "issues": [ { "name": "Some Issue", "tags": [ {"name": "Tag 1"}, {"name": "Tag 2"}, {"name": "Tag 3"}, {"id": shared_tag_id}, ], } ], }, { "name": "Another Milestone", "issues": [ { "name": "Some Issue", "tags": [ {"name": "Tag 4"}, {"id": shared_tag_id}, ], }, { "name": "Another Issue", "tags": [ {"name": "Tag 5"}, {"id": shared_tag_id}, ], }, { "name": "Third issue", "tags": [ {"name": "Tag 6"}, {"id": shared_tag_id}, ], }, ], }, ], }, }, ) assert res.data assert isinstance(res.data["createProjectWithMilestones"], dict) projects = Project.objects.all() project_typename, project_pk = from_base64( res.data["createProjectWithMilestones"].pop("id") ) assert project_typename == "ProjectType" assert projects[0] == Project.objects.get(pk=project_pk) milestones = Milestone.objects.all() assert len(milestones) == 2 assert len(res.data["createProjectWithMilestones"]["milestones"]) == 2 some_milestone = res.data["createProjectWithMilestones"]["milestones"][0] milestone_typename, milestone_pk = from_base64(some_milestone.pop("id")) assert milestone_typename == "MilestoneType" assert milestones[0] == Milestone.objects.get(pk=milestone_pk) another_milestone = res.data["createProjectWithMilestones"]["milestones"][1] milestone_typename, milestone_pk = from_base64(another_milestone.pop("id")) assert milestone_typename == "MilestoneType" assert milestones[1] == Milestone.objects.get(pk=milestone_pk) issues = Issue.objects.all() assert len(issues) == 4 assert len(some_milestone["issues"]) == 1 assert len(another_milestone["issues"]) == 3 # Issues for first milestone fetched_issue = some_milestone["issues"][0] issue_typename, issue_pk = from_base64(fetched_issue.pop("id")) assert issue_typename == "IssueType" assert issues[0] == Issue.objects.get(pk=issue_pk) # Issues for second milestone for i in range(3): fetched_issue = another_milestone["issues"][i] issue_typename, issue_pk = from_base64(fetched_issue.pop("id")) assert issue_typename == "IssueType" assert issues[i + 1] == Issue.objects.get(pk=issue_pk) tags = Tag.objects.all() assert len(tags) == 7 assert len(issues[0].tags.all()) == 4 # 3 new tags + shared tag assert len(issues[1].tags.all()) == 2 # 1 new tag + shared tag assert len(issues[2].tags.all()) == 2 # 1 new tag + shared tag assert len(issues[3].tags.all()) == 2 # 1 new tag + shared tag assert res.data == { "createProjectWithMilestones": { "__typename": "ProjectType", "name": "Some Project", "milestones": [ { "name": "Some Milestone", "issues": [ { "name": "Some Issue", "tags": [ {"name": "Shared Tag"}, {"name": "Tag 1"}, {"name": "Tag 2"}, {"name": "Tag 3"}, ], } ], }, { "name": "Another Milestone", "issues": [ { "name": "Some Issue", "tags": [ {"name": "Shared Tag"}, {"name": "Tag 4"}, ], }, { "name": "Another Issue", "tags": [ {"name": "Shared Tag"}, {"name": "Tag 5"}, ], }, { "name": "Third issue", "tags": [ {"name": "Shared Tag"}, {"name": "Tag 6"}, ], }, ], }, ], }, } @pytest.mark.django_db(transaction=True) def test_input_update_mutation_with_multiple_level_nested_creation( db, gql_client: GraphQLTestClient ): query = """ mutation UpdateProject ($input: ProjectInputPartial!) { updateProject (input: $input) { __typename ... on OperationInfo { messages { kind field message } } ... on ProjectType { id name milestones { id name issues { id name tags { name } } } } } } """ project = ProjectFactory.create(name="Some Project") shared_tag = TagFactory.create(name="Shared Tag") shared_tag_id = to_base64("TagType", shared_tag.pk) res = gql_client.query( query, { "input": { "id": to_base64("ProjectType", project.pk), "milestones": [ { "name": "Some Milestone", "issues": [ { "name": "Some Issue", "tags": [ {"name": "Tag 1"}, {"name": "Tag 2"}, {"name": "Tag 3"}, {"id": shared_tag_id}, ], } ], }, { "name": "Another Milestone", "issues": [ { "name": "Some Issue", "tags": [ {"name": "Tag 4"}, {"id": shared_tag_id}, ], }, { "name": "Another Issue", "tags": [ {"name": "Tag 5"}, {"id": shared_tag_id}, ], }, { "name": "Third issue", "tags": [ {"name": "Tag 6"}, {"id": shared_tag_id}, ], }, ], }, ], }, }, ) assert res.data assert isinstance(res.data["updateProject"], dict) project_typename, project_pk = from_base64(res.data["updateProject"].pop("id")) assert project_typename == "ProjectType" assert project.pk == int(project_pk) milestones = Milestone.objects.all() assert len(milestones) == 2 assert len(res.data["updateProject"]["milestones"]) == 2 some_milestone = res.data["updateProject"]["milestones"][0] milestone_typename, milestone_pk = from_base64(some_milestone.pop("id")) assert milestone_typename == "MilestoneType" assert milestones[0] == Milestone.objects.get(pk=milestone_pk) another_milestone = res.data["updateProject"]["milestones"][1] milestone_typename, milestone_pk = from_base64(another_milestone.pop("id")) assert milestone_typename == "MilestoneType" assert milestones[1] == Milestone.objects.get(pk=milestone_pk) issues = Issue.objects.all() assert len(issues) == 4 assert len(some_milestone["issues"]) == 1 assert len(another_milestone["issues"]) == 3 # Issues for first milestone fetched_issue = some_milestone["issues"][0] issue_typename, issue_pk = from_base64(fetched_issue.pop("id")) assert issue_typename == "IssueType" assert issues[0] == Issue.objects.get(pk=issue_pk) # Issues for second milestone for i in range(3): fetched_issue = another_milestone["issues"][i] issue_typename, issue_pk = from_base64(fetched_issue.pop("id")) assert issue_typename == "IssueType" assert issues[i + 1] == Issue.objects.get(pk=issue_pk) tags = Tag.objects.all() assert len(tags) == 7 assert len(issues[0].tags.all()) == 4 # 3 new tags + shared tag assert len(issues[1].tags.all()) == 2 # 1 new tag + shared tag assert len(issues[2].tags.all()) == 2 # 1 new tag + shared tag assert len(issues[3].tags.all()) == 2 # 1 new tag + shared tag assert res.data == { "updateProject": { "__typename": "ProjectType", "name": "Some Project", "milestones": [ { "name": "Some Milestone", "issues": [ { "name": "Some Issue", "tags": [ {"name": "Shared Tag"}, {"name": "Tag 1"}, {"name": "Tag 2"}, {"name": "Tag 3"}, ], } ], }, { "name": "Another Milestone", "issues": [ { "name": "Some Issue", "tags": [ {"name": "Shared Tag"}, {"name": "Tag 4"}, ], }, { "name": "Another Issue", "tags": [ {"name": "Shared Tag"}, {"name": "Tag 5"}, ], }, { "name": "Third issue", "tags": [ {"name": "Shared Tag"}, {"name": "Tag 6"}, ], }, ], }, ], }, } @pytest.mark.parametrize("mock_model", ["Milestone", "Issue", "Tag"]) @pytest.mark.django_db(transaction=True) def test_input_create_mutation_with_nested_calls_nested_full_clean( db, gql_client: GraphQLTestClient, mock_model: str ): query = """ mutation createProjectWithMilestones ($input: ProjectInputPartial!) { createProjectWithMilestones (input: $input) { __typename ... on OperationInfo { messages { kind field message } } ... on ProjectType { id name milestones { id name issues { id name tags { name } } } } } } """ shared_tag = TagFactory.create(name="Shared Tag") shared_tag_id = to_base64("TagType", shared_tag.pk) with patch( f"tests.projects.models.{mock_model}.clean", side_effect=ValidationError({"name": ValidationError("Invalid name")}), ) as mocked_full_clean: res = gql_client.query( query, { "input": { "name": "Some Project", "milestones": [ { "name": "Some Milestone", "issues": [ { "name": "Some Issue", "tags": [ {"name": "Tag 1"}, {"name": "Tag 2"}, {"name": "Tag 3"}, {"id": shared_tag_id}, ], } ], }, { "name": "Another Milestone", "issues": [ { "name": "Some Issue", "tags": [ {"name": "Tag 4"}, {"id": shared_tag_id}, ], }, { "name": "Another Issue", "tags": [ {"name": "Tag 5"}, {"id": shared_tag_id}, ], }, { "name": "Third issue", "tags": [ {"name": "Tag 6"}, {"id": shared_tag_id}, ], }, ], }, ], }, }, ) assert res.data assert isinstance(res.data["createProjectWithMilestones"], dict) assert res.data["createProjectWithMilestones"]["__typename"] == "OperationInfo" assert mocked_full_clean.call_count == 1 assert res.data["createProjectWithMilestones"]["messages"] == [ {"field": "name", "kind": "VALIDATION", "message": "Invalid name"} ] @pytest.mark.django_db(transaction=True) def test_input_update_mutation(db, gql_client: GraphQLTestClient): query = """ mutation UpdateIssue ($input: IssueInputPartial!) { updateIssue (input: $input) { __typename ... on OperationInfo { messages { kind field message } } ... on IssueType { id name milestone { id name } priority kind tags { id name } } } } """ issue = IssueFactory.create( name="Old name", milestone=MilestoneFactory.create(), priority=0, kind=Issue.Kind.BUG, ) tags = TagFactory.create_batch(4) issue.tags.set(tags) milestone = MilestoneFactory.create() add_tags = TagFactory.create_batch(2) remove_tags = tags[:2] res = gql_client.query( query, { "input": { "id": to_base64("IssueType", issue.pk), "name": "New name", "milestone": {"id": to_base64("MilestoneType", milestone.pk)}, "priority": 5, "kind": Issue.Kind.FEATURE.value, "tags": { "add": [{"id": to_base64("TagType", t.pk)} for t in add_tags], "remove": [{"id": to_base64("TagType", t.pk)} for t in remove_tags], }, }, }, ) assert res.data assert isinstance(res.data["updateIssue"], dict) expected_tags = tags + add_tags for removed in remove_tags: expected_tags.remove(removed) assert {frozenset(t.items()) for t in res.data["updateIssue"].pop("tags")} == { frozenset({"id": to_base64("TagType", t.pk), "name": t.name}.items()) for t in expected_tags } assert res.data == { "updateIssue": { "__typename": "IssueType", "id": to_base64("IssueType", issue.pk), "name": "New name", "milestone": { "id": to_base64("MilestoneType", milestone.pk), "name": milestone.name, }, "priority": 5, "kind": "f", }, } issue.refresh_from_db() assert issue.name == "New name" assert issue.priority == 5 assert issue.kind == Issue.Kind.FEATURE assert issue.milestone == milestone assert set(issue.tags.all()) == set(expected_tags) @pytest.mark.django_db(transaction=True) def test_input_nested_update_mutation(db, gql_client: GraphQLTestClient): query = """ mutation UpdateIssue ($input: IssueInputPartial!) { updateIssue (input: $input) { __typename ... on OperationInfo { messages { kind field message } } ... on IssueType { id name milestone { id name } priority kind } } } """ issue = IssueFactory.create( name="Old name", milestone=MilestoneFactory.create(), priority=0, kind=Issue.Kind.BUG, ) milestone = MilestoneFactory.create(name="Something") res = gql_client.query( query, { "input": { "id": to_base64("IssueType", issue.pk), "name": "New name", "milestone": { "id": to_base64("MilestoneType", milestone.pk), "name": "foo", }, }, }, ) assert res.data assert isinstance(res.data["updateIssue"], dict) assert res.data["updateIssue"]["milestone"]["name"] == "foo" milestone.refresh_from_db() assert milestone.name == "foo" @pytest.mark.django_db(transaction=True) def test_input_update_m2m_set_not_null_mutation(db, gql_client: GraphQLTestClient): query = """ mutation UpdateProject ($input: ProjectInputPartial!, $optimizerEnabled: Boolean!) { updateProject (input: $input) { __typename ... on OperationInfo { messages { kind field message } } ... on ProjectType { id name dueDate isDelayed @include(if: $optimizerEnabled) milestones { id name } cost } } } """ project = ProjectFactory.create( name="Project Name", ) milestone_1 = MilestoneFactory.create(project=project) milestone_1_id = to_base64("MilestoneType", milestone_1.pk) MilestoneFactory.create(project=project) # For mutations, having the optimizer enabled is expected to generate one extra # query for the refetch of the object with assert_num_queries(14 if DjangoOptimizerExtension.enabled.get() else 13): res = gql_client.query( query, { "input": { "id": to_base64("ProjectType", project.pk), "milestones": [{"id": milestone_1_id}], }, "optimizerEnabled": DjangoOptimizerExtension.enabled.get(), }, ) assert res.data assert isinstance(res.data["updateProject"], dict) assert len(res.data["updateProject"]["milestones"]) == 1 assert res.data["updateProject"]["milestones"][0]["id"] == milestone_1_id @pytest.mark.django_db(transaction=True) def test_input_update_m2m_set_mutation(db, gql_client: GraphQLTestClient): query = """ mutation UpdateIssue ($input: IssueInputPartial!) { updateIssue (input: $input) { __typename ... on OperationInfo { messages { kind field message } } ... on IssueType { id name milestone { id name } priority kind tags { id name } issueAssignees { owner user { id } } } } } """ issue = IssueFactory.create( name="Old name", milestone=MilestoneFactory.create(), priority=0, kind=Issue.Kind.BUG, ) tags = TagFactory.create_batch(4) issue.tags.set(tags) milestone = MilestoneFactory.create() user_1 = UserFactory.create() user_2 = UserFactory.create() user_3 = UserFactory.create() assignee = issue.issue_assignees.create( user=user_3, owner=False, ) res = gql_client.query( query, { "input": { "id": to_base64("IssueType", issue.pk), "name": "New name", "milestone": {"id": to_base64("MilestoneType", milestone.pk)}, "priority": 5, "kind": Issue.Kind.FEATURE.value, "tags": { "set": [ {"id": None, "name": "Foobar"}, {"name": "Foobin"}, ], }, "issueAssignees": { "set": [ { "user": {"id": to_base64("UserType", user_1.username)}, }, { "user": {"id": to_base64("UserType", user_2.username)}, "owner": True, }, { "id": to_base64("AssigneeType", assignee.pk), "owner": True, }, ], }, }, }, ) assert res.data assert isinstance(res.data["updateIssue"], dict) tags = res.data["updateIssue"].pop("tags") assert len(tags) == 2 assert {t["name"] for t in tags} == {"Foobar", "Foobin"} assert { (r["user"]["id"], r["owner"]) for r in res.data["updateIssue"].pop("issueAssignees") } == { (to_base64("UserType", user_1.username), False), (to_base64("UserType", user_2.username), True), (to_base64("UserType", user_3.username), True), } assert res.data == { "updateIssue": { "__typename": "IssueType", "id": to_base64("IssueType", issue.pk), "name": "New name", "milestone": { "id": to_base64("MilestoneType", milestone.pk), "name": milestone.name, }, "priority": 5, "kind": "f", }, } issue.refresh_from_db() assert issue.name == "New name" assert issue.priority == 5 assert issue.kind == Issue.Kind.FEATURE assert issue.milestone == milestone @pytest.mark.django_db(transaction=True) def test_input_update_m2m_set_through_mutation(db, gql_client: GraphQLTestClient): query = """ mutation UpdateIssue ($input: IssueInputPartial!) { updateIssue (input: $input) { __typename ... on OperationInfo { messages { kind field message } } ... on IssueType { id name milestone { id name } priority kind tags { id name } issueAssignees { owner user { id } } } } } """ issue = IssueFactory.create( name="Old name", milestone=MilestoneFactory.create(), priority=0, kind=Issue.Kind.BUG, ) tags = TagFactory.create_batch(4) issue.tags.set(tags) milestone = MilestoneFactory.create() user_1 = UserFactory.create() user_2 = UserFactory.create() user_3 = UserFactory.create() issue.issue_assignees.create( user=user_3, owner=False, ) res = gql_client.query( query, { "input": { "id": to_base64("IssueType", issue.pk), "name": "New name", "milestone": {"id": to_base64("MilestoneType", milestone.pk)}, "priority": 5, "kind": Issue.Kind.FEATURE.value, "tags": { "set": [ {"id": None, "name": "Foobar"}, {"name": "Foobin"}, ], }, "assignees": { "set": [ { "id": to_base64("UserType", user_1.username), }, { "id": to_base64("UserType", user_2.username), "throughDefaults": { "owner": True, }, }, { "id": to_base64("UserType", user_3.username), "throughDefaults": { "owner": True, }, }, ], }, }, }, ) assert res.data assert isinstance(res.data["updateIssue"], dict) tags = res.data["updateIssue"].pop("tags") assert len(tags) == 2 assert {t["name"] for t in tags} == {"Foobar", "Foobin"} assert { (r["user"]["id"], r["owner"]) for r in res.data["updateIssue"].pop("issueAssignees") } == { (to_base64("UserType", user_1.username), False), (to_base64("UserType", user_2.username), True), (to_base64("UserType", user_3.username), True), } assert res.data == { "updateIssue": { "__typename": "IssueType", "id": to_base64("IssueType", issue.pk), "name": "New name", "milestone": { "id": to_base64("MilestoneType", milestone.pk), "name": milestone.name, }, "priority": 5, "kind": "f", }, } issue.refresh_from_db() assert issue.name == "New name" assert issue.priority == 5 assert issue.kind == Issue.Kind.FEATURE assert issue.milestone == milestone @pytest.mark.django_db(transaction=True) def test_input_update_mutation_with_key_attr(db, gql_client: GraphQLTestClient): query = """ mutation UpdateIssueWithKeyAttr ($input: IssueInputPartialWithoutId!) { updateIssueWithKeyAttr (input: $input) { __typename ... on OperationInfo { messages { kind field message } } ... on IssueType { name milestone { id name } priority kind tags { id name } } } } """ issue = IssueFactory.create( name="Unique name", milestone=MilestoneFactory.create(), priority=0, kind=Issue.Kind.BUG, ) tags = TagFactory.create_batch(4) issue.tags.set(tags) milestone = MilestoneFactory.create() add_tags = TagFactory.create_batch(2) remove_tags = tags[:2] res = gql_client.query( query, { "input": { "name": "Unique name", "milestone": {"id": to_base64("MilestoneType", milestone.pk)}, "priority": 5, "kind": Issue.Kind.FEATURE.value, "tags": { "add": [{"id": to_base64("TagType", t.pk)} for t in add_tags], "remove": [{"id": to_base64("TagType", t.pk)} for t in remove_tags], }, }, }, ) assert res.data assert isinstance(res.data["updateIssueWithKeyAttr"], dict) expected_tags = tags + add_tags for removed in remove_tags: expected_tags.remove(removed) assert { frozenset(t.items()) for t in res.data["updateIssueWithKeyAttr"].pop("tags") } == { frozenset({"id": to_base64("TagType", t.pk), "name": t.name}.items()) for t in expected_tags } assert res.data == { "updateIssueWithKeyAttr": { "__typename": "IssueType", "name": "Unique name", "milestone": { "id": to_base64("MilestoneType", milestone.pk), "name": milestone.name, }, "priority": 5, "kind": "f", }, } issue.refresh_from_db() assert issue.priority == 5 assert issue.kind == Issue.Kind.FEATURE assert issue.milestone == milestone assert set(issue.tags.all()) == set(expected_tags) @pytest.mark.django_db(transaction=True) def test_input_delete_mutation(db, gql_client: GraphQLTestClient): query = """ mutation DeleteIssue ($input: NodeInput!) { deleteIssue (input: $input) { __typename ... on OperationInfo { messages { kind field message } } ... on IssueType { id name milestone { id name } priority kind } } } """ issue = IssueFactory.create() assert issue.milestone assert issue.kind res = gql_client.query( query, { "input": { "id": to_base64("IssueType", issue.pk), }, }, ) assert res.data assert isinstance(res.data["deleteIssue"], dict) assert res.data == { "deleteIssue": { "__typename": "IssueType", "id": to_base64("IssueType", issue.pk), "name": issue.name, "milestone": { "id": to_base64("MilestoneType", issue.milestone.pk), "name": issue.milestone.name, }, "priority": issue.priority, "kind": issue.kind.value, # type: ignore }, } with pytest.raises(Issue.DoesNotExist): Issue.objects.get(pk=issue.pk) @pytest.mark.django_db(transaction=True) def test_input_delete_mutation_with_key_attr(db, gql_client: GraphQLTestClient): query = """ mutation DeleteIssue ($input: MilestoneIssueInput!) { deleteIssueWithKeyAttr (input: $input) { __typename ... on OperationInfo { messages { kind field message } } ... on IssueType { id name milestone { id name } priority kind } } } """ issue = IssueFactory.create() assert issue.milestone assert issue.kind res = gql_client.query( query, { "input": { "name": issue.name, }, }, ) assert res.data assert isinstance(res.data["deleteIssueWithKeyAttr"], dict) assert res.data == { "deleteIssueWithKeyAttr": { "__typename": "IssueType", "id": to_base64("IssueType", issue.pk), "name": issue.name, "milestone": { "id": to_base64("MilestoneType", issue.milestone.pk), "name": issue.milestone.name, }, "priority": issue.priority, "kind": issue.kind.value, # type: ignore }, } with pytest.raises(Issue.DoesNotExist): Issue.objects.get(pk=issue.pk) @pytest.mark.django_db(transaction=True) def test_mutation_full_clean_without_kwargs(db, gql_client: GraphQLTestClient): query = """ mutation CreateQuiz ($input: CreateQuizInput!) { createQuiz (input: $input) { __typename ... on OperationInfo { messages { kind field message } } ... on QuizType { title sequence } } } """ res = gql_client.query( query, { "input": { "title": "ABC", }, }, ) expected = {"createQuiz": {"__typename": "QuizType", "sequence": 1, "title": "ABC"}} assert res.data == expected res = gql_client.query( query, { "input": { "title": "ABC", }, }, ) expected = { "createQuiz": { "__typename": "OperationInfo", "messages": [ { "field": "sequence", "kind": "VALIDATION", "message": "Quiz with this Sequence already exists.", }, ], }, } assert res.data == expected @pytest.mark.django_db(transaction=True) def test_mutation_full_clean_with_kwargs(db, gql_client: GraphQLTestClient): query = """ mutation CreateQuiz ($input: CreateQuizInput!) { createQuiz (input: $input) { __typename ... on OperationInfo { messages { kind field message } } ... on QuizType { title sequence } } } """ res = gql_client.query( query, {"input": {"title": "ABC", "fullCleanOptions": True}}, ) expected = {"createQuiz": {"__typename": "QuizType", "sequence": 1, "title": "ABC"}} assert res.data == expected res = gql_client.query( query, {"input": {"title": "ABC", "fullCleanOptions": True}}, ) expected = {"createQuiz": {"__typename": "QuizType", "sequence": 2, "title": "ABC"}} assert res.data == expected res = gql_client.query( query, {"input": {"title": "ABC", "fullCleanOptions": True}}, ) expected = {"createQuiz": {"__typename": "QuizType", "sequence": 3, "title": "ABC"}} assert res.data == expected res = gql_client.query( query, {"input": {"title": "ABC", "fullCleanOptions": True}}, ) expected = {"createQuiz": {"__typename": "QuizType", "sequence": 4, "title": "ABC"}} assert res.data == expected strawberry-graphql-django-0.62.0/tests/test_legacy_order.py000066400000000000000000000375461502405145400241300ustar00rootroot00000000000000# ruff: noqa: TRY002, B904, BLE001, F811, PT012 import warnings from typing import Any, Optional, cast from unittest import mock import pytest import strawberry from django.db.models import Case, Count, Value, When from pytest_mock import MockFixture from strawberry import auto from strawberry.annotation import StrawberryAnnotation from strawberry.exceptions import MissingArgumentsAnnotationsError from strawberry.types import get_object_definition from strawberry.types.base import ( StrawberryOptional, WithStrawberryObjectDefinition, get_object_definition, ) from strawberry.types.field import StrawberryField import strawberry_django from strawberry_django.exceptions import ( ForbiddenFieldArgumentError, MissingFieldArgumentError, ) from strawberry_django.fields.field import StrawberryDjangoField from strawberry_django.fields.filter_order import ( FilterOrderField, FilterOrderFieldResolver, ) from strawberry_django.ordering import Ordering, OrderSequence, process_order from tests import models, utils from tests.types import Fruit @strawberry_django.ordering.order(models.Color) class ColorOrder: pk: auto @strawberry_django.order_field def name(self, prefix, value: auto): return [value.resolve(f"{prefix}name")] @strawberry_django.order_type(models.Fruit) class FruitOrdering: name: auto @strawberry_django.ordering.order(models.Fruit) class FruitOrder: color_id: auto name: auto sweetness: auto color: Optional[ColorOrder] @strawberry_django.order_field def types_number(self, queryset, prefix, value: auto): return queryset.annotate( count=Count(f"{prefix}types__id"), count_nulls=Case( When(count=0, then=Value(None)), default="count", ), ), [value.resolve("count_nulls")] @strawberry_django.type(models.Fruit, order=FruitOrder) class FruitWithOrder: id: auto name: auto @strawberry.type class Query: fruits: list[Fruit] = strawberry_django.field(order=FruitOrder) fruits_with_ordering: list[Fruit] = strawberry_django.field( order=FruitOrder, ordering=FruitOrdering ) @pytest.fixture def query(): return utils.generate_query(Query) def test_legacy_order_argument_is_deprecated(): with warnings.catch_warnings(record=True) as w: warnings.simplefilter("always", DeprecationWarning) strawberry_django.field(order=FruitOrder) assert len(w) == 1 assert issubclass(w[-1].category, DeprecationWarning) assert ( str(w[-1].message) == "strawberry_django.order is deprecated in favor of strawberry_django.ordering." ) def test_legacy_order_type_is_deprecated(): with warnings.catch_warnings(record=True) as w: warnings.simplefilter("always", DeprecationWarning) @strawberry_django.ordering.order(models.Fruit) class TestOrder: name: auto assert len(w) == 1 assert issubclass(w[-1].category, DeprecationWarning) assert ( str(w[-1].message) == "strawberry_django.order is deprecated in favor of strawberry_django.ordering." ) def test_legacy_order_works_when_ordering_is_present(query, fruits): result = query("{ fruitsWithOrdering(order: { name: ASC }) { id name } }") assert not result.errors assert result.data["fruitsWithOrdering"] == [ {"id": "3", "name": "banana"}, {"id": "2", "name": "raspberry"}, {"id": "1", "name": "strawberry"}, ] def test_ordering_works_when_legacy_order_is_present(query, fruits): result = query("{ fruitsWithOrdering(ordering: [{ name: ASC }]) { id name } }") assert not result.errors assert result.data["fruitsWithOrdering"] == [ {"id": "3", "name": "banana"}, {"id": "2", "name": "raspberry"}, {"id": "1", "name": "strawberry"}, ] def test_error_when_ordering_and_order_given(query, fruits): result = query( "{ fruitsWithOrdering(ordering: [{ name: ASC }], order: { name: ASC }) { id name } }" ) assert result.errors is not None assert len(result.errors) == 1 assert ( result.errors[0].message == "Only one of `ordering` or `order` must be given." ) def test_field_order_definition(): field = StrawberryDjangoField(type_annotation=StrawberryAnnotation(FruitWithOrder)) assert field.get_order() == FruitOrder field = StrawberryDjangoField( type_annotation=StrawberryAnnotation(FruitWithOrder), filters=None, ) assert field.get_filters() is None def test_asc(query, fruits): result = query("{ fruits(order: { name: ASC }) { id name } }") assert not result.errors assert result.data["fruits"] == [ {"id": "3", "name": "banana"}, {"id": "2", "name": "raspberry"}, {"id": "1", "name": "strawberry"}, ] def test_desc(query, fruits): result = query("{ fruits(order: { name: DESC }) { id name } }") assert not result.errors assert result.data["fruits"] == [ {"id": "1", "name": "strawberry"}, {"id": "2", "name": "raspberry"}, {"id": "3", "name": "banana"}, ] def test_relationship(query, fruits): def add_color(fruit, color_name): fruit.color = models.Color.objects.create(name=color_name) fruit.save() color_names = ["red", "dark red", "yellow"] for fruit, color_name in zip(fruits, color_names): add_color(fruit, color_name) result = query( "{ fruits(order: { color: { name: DESC } }) { id name color { name } } }", ) assert not result.errors assert result.data["fruits"] == [ {"id": "3", "name": "banana", "color": {"name": "yellow"}}, {"id": "1", "name": "strawberry", "color": {"name": "red"}}, {"id": "2", "name": "raspberry", "color": {"name": "dark red"}}, ] def test_arguments_order_respected(query, db): yellow = models.Color.objects.create(name="yellow") red = models.Color.objects.create(name="red") f1 = models.Fruit.objects.create( name="strawberry", sweetness=1, color=red, ) f2 = models.Fruit.objects.create( name="banana", sweetness=2, color=yellow, ) f3 = models.Fruit.objects.create( name="apple", sweetness=0, color=red, ) result = query("{ fruits(order: { name: ASC, sweetness: ASC }) { id } }") assert not result.errors assert result.data["fruits"] == [{"id": str(f.pk)} for f in [f3, f2, f1]] result = query("{ fruits(order: { sweetness: DESC, name: ASC }) { id } }") assert not result.errors assert result.data["fruits"] == [{"id": str(f.pk)} for f in [f2, f1, f3]] result = query("{ fruits(order: { color: {name: ASC}, name: ASC }) { id } }") assert not result.errors assert result.data["fruits"] == [{"id": str(f.pk)} for f in [f3, f1, f2]] result = query("{ fruits(order: { color: {pk: ASC}, name: ASC }) { id } }") assert not result.errors assert result.data["fruits"] == [{"id": str(f.pk)} for f in [f2, f3, f1]] result = query("{ fruits(order: { colorId: ASC, name: ASC }) { id } }") assert not result.errors assert result.data["fruits"] == [{"id": str(f.pk)} for f in [f2, f3, f1]] result = query("{ fruits(order: { name: ASC, colorId: ASC }) { id } }") assert not result.errors assert result.data["fruits"] == [{"id": str(f.pk)} for f in [f3, f2, f1]] def test_order_sequence(): f1 = StrawberryField(graphql_name="sOmEnAmE", python_name="some_name") f2 = StrawberryField(python_name="some_name") assert OrderSequence.get_graphql_name(None, f1) == "sOmEnAmE" assert OrderSequence.get_graphql_name(None, f2) == "someName" assert OrderSequence.sorted(None, None, fields=[f1, f2]) == [f1, f2] sequence = {"someName": OrderSequence(0, None), "sOmEnAmE": OrderSequence(1, None)} assert OrderSequence.sorted(None, sequence, fields=[f1, f2]) == [f1, f2] def test_order_type(): @strawberry_django.ordering.order(models.Fruit) class FruitOrder: color_id: auto name: auto sweetness: auto @strawberry_django.order_field def custom_order(self, value: auto, prefix: str): pass annotated_type = StrawberryOptional(Ordering._enum_definition) # type: ignore assert [ ( f.name, f.__class__, f.type, f.base_resolver.__class__ if f.base_resolver else None, ) for f in get_object_definition(FruitOrder, strict=True).fields ] == [ ("color_id", StrawberryField, annotated_type, None), ("name", StrawberryField, annotated_type, None), ("sweetness", StrawberryField, annotated_type, None), ( "custom_order", FilterOrderField, annotated_type, FilterOrderFieldResolver, ), ] def test_order_field_missing_prefix(): with pytest.raises( MissingFieldArgumentError, match=r".*\"prefix\".*\"field_method\".*" ): @strawberry_django.order_field def field_method(): pass def test_order_field_missing_value(): with pytest.raises( MissingFieldArgumentError, match=r".*\"value\".*\"field_method\".*" ): @strawberry_django.order_field def field_method(prefix): pass def test_order_field_missing_value_annotation(): with pytest.raises( MissingArgumentsAnnotationsError, match=r"Missing annotation.*\"value\".*\"field_method\".*", ): @strawberry_django.order_field def field_method(prefix, value): pass def test_order_field(): try: @strawberry_django.order_field def field_method(self, root, info, prefix, value: auto, sequence, queryset): pass except Exception as exc: raise pytest.fail(f"DID RAISE {exc}") def test_order_field_forbidden_param_annotation(): with pytest.raises( MissingArgumentsAnnotationsError, match=r".*\"forbidden_param\".*\"field_method\".*", ): @strawberry_django.order_field def field_method(prefix, value: auto, sequence, queryset, forbidden_param): pass def test_order_field_forbidden_param(): with pytest.raises( ForbiddenFieldArgumentError, match=r".*\"forbidden_param\".*\"field_method\".*", ): @strawberry_django.order_field def field_method(prefix, value: auto, sequence, queryset, forbidden_param: str): pass def test_order_field_missing_queryset(): with pytest.raises(MissingFieldArgumentError, match=r".*\"queryset\".*\"order\".*"): @strawberry_django.order_field def order(prefix): pass def test_order_field_value_forbidden_on_object(): with pytest.raises(ForbiddenFieldArgumentError, match=r".*\"value\".*\"order\".*"): @strawberry_django.order_field def field_method(prefix, queryset, value: auto): pass @strawberry_django.order_field def order(prefix, queryset, value: auto): pass def test_order_field_on_object(): try: @strawberry_django.order_field def order(self, root, info, prefix, sequence, queryset): pass except Exception as exc: raise pytest.fail(f"DID RAISE {exc}") def test_order_field_method(): @strawberry_django.ordering.order(models.Fruit) class Order: @strawberry_django.order_field def custom_order(self, root, info, prefix, value: auto, sequence, queryset): assert self == order, "Unexpected self passed" assert root == order, "Unexpected root passed" assert info == fake_info, "Unexpected info passed" assert prefix == "ROOT", "Unexpected prefix passed" assert value == Ordering.ASC, "Unexpected value passed" assert sequence == sequence_inner, "Unexpected sequence passed" assert queryset == qs, "Unexpected queryset passed" raise Exception("WAS CALLED") order = cast("WithStrawberryObjectDefinition", Order(custom_order=Ordering.ASC)) # type: ignore schema = strawberry.Schema(query=Query) fake_info: Any = type("FakeInfo", (), {"schema": schema}) qs: Any = object() sequence_inner: Any = object() sequence = {"customOrder": OrderSequence(0, children=sequence_inner)} with pytest.raises(Exception, match="WAS CALLED"): process_order(order, fake_info, qs, prefix="ROOT", sequence=sequence) def test_order_method_not_called_when_not_decorated(mocker: MockFixture): @strawberry_django.ordering.order(models.Fruit) class Order: def order(self, root, info, prefix, value: auto, sequence, queryset): pytest.fail("Should not have been called") mock_order_method = mocker.spy(Order, "order") process_order( cast("WithStrawberryObjectDefinition", Order()), mock.Mock(), mock.Mock() ) mock_order_method.assert_not_called() def test_order_field_not_called(mocker: MockFixture): @strawberry_django.ordering.order(models.Fruit) class Order: order: Ordering = Ordering.ASC # Calling this and no error being raised is the test, as the wrong behavior would # be for the field to be called like a method process_order( cast("WithStrawberryObjectDefinition", Order()), mock.Mock(), mock.Mock() ) def test_order_object_method(): @strawberry_django.ordering.order(models.Fruit) class Order: @strawberry_django.order_field def order(self, root, info, prefix, sequence, queryset): assert self == order_, "Unexpected self passed" assert root == order_, "Unexpected root passed" assert info == fake_info, "Unexpected info passed" assert prefix == "ROOT", "Unexpected prefix passed" assert sequence == sequence_, "Unexpected sequence passed" assert queryset == qs, "Unexpected queryset passed" return queryset, ["name"] order_ = cast("WithStrawberryObjectDefinition", Order()) schema = strawberry.Schema(query=Query) fake_info: Any = type("FakeInfo", (), {"schema": schema}) qs: Any = object() sequence_: Any = {"customOrder": OrderSequence(0)} order = process_order(order_, fake_info, qs, prefix="ROOT", sequence=sequence_)[1] assert "name" in order, "order was not called" def test_order_nulls(query, db, fruits): t1 = models.FruitType.objects.create(name="Type1") t2 = models.FruitType.objects.create(name="Type2") f1, f2, f3 = models.Fruit.objects.all() f2.types.add(t1) f3.types.add(t1, t2) result = query("{ fruits(order: { typesNumber: ASC }) { id } }") assert not result.errors assert result.data["fruits"] == [ {"id": str(f1.id)}, {"id": str(f2.id)}, {"id": str(f3.id)}, ] result = query("{ fruits(order: { typesNumber: DESC }) { id } }") assert not result.errors assert result.data["fruits"] == [ {"id": str(f3.id)}, {"id": str(f2.id)}, {"id": str(f1.id)}, ] result = query("{ fruits(order: { typesNumber: ASC_NULLS_FIRST }) { id } }") assert not result.errors assert result.data["fruits"] == [ {"id": str(f1.id)}, {"id": str(f2.id)}, {"id": str(f3.id)}, ] result = query("{ fruits(order: { typesNumber: ASC_NULLS_LAST }) { id } }") assert not result.errors assert result.data["fruits"] == [ {"id": str(f2.id)}, {"id": str(f3.id)}, {"id": str(f1.id)}, ] result = query("{ fruits(order: { typesNumber: DESC_NULLS_LAST }) { id } }") assert not result.errors assert result.data["fruits"] == [ {"id": str(f3.id)}, {"id": str(f2.id)}, {"id": str(f1.id)}, ] result = query("{ fruits(order: { typesNumber: DESC_NULLS_FIRST }) { id } }") assert not result.errors assert result.data["fruits"] == [ {"id": str(f1.id)}, {"id": str(f3.id)}, {"id": str(f2.id)}, ] strawberry-graphql-django-0.62.0/tests/test_optimizer.py000066400000000000000000001723731502405145400235110ustar00rootroot00000000000000import datetime from typing import Any, cast import pytest import strawberry from django.db import DEFAULT_DB_ALIAS, connections from django.db.models import Prefetch from django.test.utils import CaptureQueriesContext from django.utils import timezone from pytest_mock import MockerFixture from strawberry.relay import GlobalID, to_base64 from strawberry.types import ExecutionResult, get_object_definition import strawberry_django from strawberry_django.optimizer import DjangoOptimizerExtension from tests.projects.schema import IssueType, MilestoneType, ProjectType, StaffType from . import utils from .projects.faker import ( IssueFactory, MilestoneFactory, ProjectFactory, StaffUserFactory, TagFactory, UserFactory, ) from .projects.models import Assignee, Issue, Milestone, Project from .utils import GraphQLTestClient, assert_num_queries @pytest.mark.django_db(transaction=True) def test_user_query(db, gql_client: GraphQLTestClient): query = """ query TestQuery { me { id email fullName } } """ with assert_num_queries(0): res = gql_client.query(query) assert res.data == { "me": None, } user = UserFactory.create(first_name="John", last_name="Snow") with gql_client.login(user): with assert_num_queries(2): res = gql_client.query(query) assert res.data == { "me": { "id": to_base64("UserType", user.username), "email": user.email, "fullName": "John Snow", }, } @pytest.mark.django_db(transaction=True) def test_staff_query(db, gql_client: GraphQLTestClient, mocker: MockerFixture): staff_type_get_queryset = StaffType.get_queryset mock_staff_type_get_queryset = mocker.patch( "tests.projects.schema.StaffType.get_queryset", autospec=True, side_effect=staff_type_get_queryset, ) query = """ query TestQuery { staffConn { edges { node { email } } } } """ UserFactory.create_batch(5) staff1, staff2 = StaffUserFactory.create_batch(2) res = gql_client.query(query) assert res.data == { "staffConn": { "edges": [ {"node": {"email": staff1.email}}, {"node": {"email": staff2.email}}, ], }, } mock_staff_type_get_queryset.assert_called_once() @pytest.mark.django_db(transaction=True) def test_interface_query(db, gql_client: GraphQLTestClient): query = """ query TestQuery ($id: ID!) { node (id: $id) { __typename id ... on IssueType { name milestone { id name project { id name } } tags { id name } } } } """ issue = IssueFactory.create() assert issue.milestone tags = TagFactory.create_batch(4) issue.tags.set(tags) with assert_num_queries(2 if DjangoOptimizerExtension.enabled.get() else 4): res = gql_client.query(query, {"id": to_base64("IssueType", issue.pk)}) assert isinstance(res.data, dict) assert isinstance(res.data["node"], dict) assert { frozenset(d.items()) for d in cast("list", res.data["node"].pop("tags")) } == frozenset( { frozenset( { "id": to_base64("TagType", t.pk), "name": t.name, }.items(), ) for t in tags }, ) assert res.data == { "node": { "__typename": "IssueType", "id": to_base64("IssueType", issue.pk), "name": issue.name, "milestone": { "id": to_base64("MilestoneType", issue.milestone.pk), "name": issue.milestone.name, "project": { "id": to_base64("ProjectType", issue.milestone.project.pk), "name": issue.milestone.project.name, }, }, }, } @pytest.mark.django_db(transaction=True) def test_query_forward(db, gql_client: GraphQLTestClient): query = """ query TestQuery ($isAsync: Boolean!) { issueConn { totalCount edges { node { id name milestone { id name asyncField(value: "foo") @include (if: $isAsync) project { id name } } } } } } """ expected = [] for p in ProjectFactory.create_batch(2): for m in MilestoneFactory.create_batch(2, project=p): for i in IssueFactory.create_batch(2, milestone=m): r: dict[str, Any] = { "id": to_base64("IssueType", i.id), "name": i.name, "milestone": { "id": to_base64("MilestoneType", m.id), "name": m.name, "project": { "id": to_base64("ProjectType", p.id), "name": p.name, }, }, } if gql_client.is_async: r["milestone"]["asyncField"] = "value: foo" expected.append(r) with assert_num_queries(2 if DjangoOptimizerExtension.enabled.get() else 18): res = gql_client.query(query, {"isAsync": gql_client.is_async}) assert res.data == { "issueConn": { "totalCount": 8, "edges": [{"node": r} for r in expected], }, } @pytest.mark.django_db(transaction=True) def test_query_forward_with_interfaces(db, gql_client: GraphQLTestClient): query = """ query TestQuery ($isAsync: Boolean!) { issueConn { totalCount edges { node { id ... on Named { name } milestone { id ... on Named { name } asyncField(value: "foo") @include (if: $isAsync) project { id ... on Named { name } } } } } } } """ expected = [] for p in ProjectFactory.create_batch(2): for m in MilestoneFactory.create_batch(2, project=p): for i in IssueFactory.create_batch(2, milestone=m): r: dict[str, Any] = { "id": to_base64("IssueType", i.id), "name": i.name, "milestone": { "id": to_base64("MilestoneType", m.id), "name": m.name, "project": { "id": to_base64("ProjectType", p.id), "name": p.name, }, }, } if gql_client.is_async: r["milestone"]["asyncField"] = "value: foo" expected.append(r) with assert_num_queries(2 if DjangoOptimizerExtension.enabled.get() else 18): res = gql_client.query(query, {"isAsync": gql_client.is_async}) assert res.data == { "issueConn": { "totalCount": 8, "edges": [{"node": r} for r in expected], }, } @pytest.mark.django_db(transaction=True) def test_query_forward_with_fragments(db, gql_client: GraphQLTestClient): query = """ fragment issueFrag on IssueType { nameWithKind nameWithPriority } fragment milestoneFrag on MilestoneType { id project { name } } query TestQuery { issueConn { totalCount edges { node { id name ... issueFrag milestone { name project { id name } ... milestoneFrag } } } } } """ expected = [] for p in ProjectFactory.create_batch(3): for m in MilestoneFactory.create_batch(3, project=p): for i in IssueFactory.create_batch(3, milestone=m): m_res = { "id": to_base64("MilestoneType", m.id), "name": m.name, "project": { "id": to_base64("ProjectType", p.id), "name": p.name, }, } expected.append( { "id": to_base64("IssueType", i.id), "name": i.name, "nameWithKind": f"{i.kind}: {i.name}", "nameWithPriority": f"{i.kind}: {i.priority}", "milestone": m_res, }, ) with assert_num_queries(2 if DjangoOptimizerExtension.enabled.get() else 56): res = gql_client.query(query) assert res.data == { "issueConn": { "totalCount": 27, "edges": [{"node": r} for r in expected], }, } @pytest.mark.django_db(transaction=True) def test_query_prefetch(db, gql_client: GraphQLTestClient): query = """ query TestQuery ($node_id: ID!) { project (id: $node_id) { id name milestones { id name project { id name } issues { id name milestone { id name } } } } } """ expected = [] for p in ProjectFactory.create_batch(2): p_res: dict[str, Any] = { "id": to_base64("ProjectType", p.id), "name": p.name, "milestones": [], } expected.append(p_res) for m in MilestoneFactory.create_batch(2, project=p): m_res: dict[str, Any] = { "id": to_base64("MilestoneType", m.id), "name": m.name, "project": { "id": p_res["id"], "name": p_res["name"], }, "issues": [], } p_res["milestones"].append(m_res) for i in IssueFactory.create_batch(2, milestone=m): m_res["issues"].append( { "id": to_base64("IssueType", i.id), "name": i.name, "milestone": { "id": m_res["id"], "name": m_res["name"], }, }, ) assert len(expected) == 2 for e in expected: with assert_num_queries(3 if DjangoOptimizerExtension.enabled.get() else 4): res = gql_client.query(query, {"node_id": e["id"]}) assert res.data == {"project": e} @pytest.mark.django_db(transaction=True) def test_query_prefetch_with_callable(db, gql_client: GraphQLTestClient): query = """ query TestQuery ($node_id: ID!) { project (id: $node_id) { id name milestones { id name project { id name } myIssues { id name milestone { id name } } } } } """ user = UserFactory.create() expected = [] for p in ProjectFactory.create_batch(2): p_res: dict[str, Any] = { "id": to_base64("ProjectType", p.id), "name": p.name, "milestones": [], } expected.append(p_res) for m in MilestoneFactory.create_batch(2, project=p): m_res: dict[str, Any] = { "id": to_base64("MilestoneType", m.id), "name": m.name, "project": { "id": p_res["id"], "name": p_res["name"], }, "myIssues": [], } p_res["milestones"].append(m_res) # Those issues are not assigned to the user, # thus they should not appear in the results IssueFactory.create_batch(2, milestone=m) for i in IssueFactory.create_batch(2, milestone=m): Assignee.objects.create(user=user, issue=i) m_res["myIssues"].append( { "id": to_base64("IssueType", i.id), "name": i.name, "milestone": { "id": m_res["id"], "name": m_res["name"], }, }, ) assert len(expected) == 2 for e in expected: with gql_client.login(user): if DjangoOptimizerExtension.enabled.get(): with assert_num_queries(5): res = gql_client.query(query, {"node_id": e["id"]}) assert res.data == {"project": e} else: # myIssues requires the optimizer to be turned on res = gql_client.query( query, {"node_id": e["id"]}, assert_no_errors=False, ) assert res.errors @pytest.mark.django_db(transaction=True) def test_query_prefetch_with_fragments(db, gql_client: GraphQLTestClient): query = """ fragment issueFrag on IssueType { nameWithKind nameWithPriority } fragment milestoneFrag on MilestoneType { id project { id name } } query TestQuery ($node_id: ID!) { project (id: $node_id) { id name milestones { id name project { id name } issues { id name ... issueFrag milestone { ... milestoneFrag } } } } } """ expected = [] for p in ProjectFactory.create_batch(3): p_res: dict[str, Any] = { "id": to_base64("ProjectType", p.id), "name": p.name, "milestones": [], } expected.append(p_res) for m in MilestoneFactory.create_batch(3, project=p): m_res: dict[str, Any] = { "id": to_base64("MilestoneType", m.id), "name": m.name, "project": { "id": p_res["id"], "name": p_res["name"], }, "issues": [], } p_res["milestones"].append(m_res) for i in IssueFactory.create_batch(3, milestone=m): m_res["issues"].append( { "id": to_base64("IssueType", i.id), "name": i.name, "nameWithKind": f"{i.kind}: {i.name}", "nameWithPriority": f"{i.kind}: {i.priority}", "milestone": { "id": m_res["id"], "project": { "id": p_res["id"], "name": p_res["name"], }, }, }, ) assert len(expected) == 3 for e in expected: with assert_num_queries(3 if DjangoOptimizerExtension.enabled.get() else 5): res = gql_client.query(query, {"node_id": e["id"]}) assert res.data == {"project": e} @pytest.mark.django_db(transaction=True) def test_query_connection_with_resolver(db, gql_client: GraphQLTestClient): query = """ query TestQuery { projectConnWithResolver (name: "Foo") { totalCount edges { node { id name milestones { id } } } } } """ p1 = ProjectFactory.create(name="Foo 1") p2 = ProjectFactory.create(name="2 Foo") p3 = ProjectFactory.create(name="FooBar") for i in range(10): ProjectFactory.create(name=f"Project {i}") with assert_num_queries(3 if DjangoOptimizerExtension.enabled.get() else 5): res = gql_client.query(query) assert res.data == { "projectConnWithResolver": { "totalCount": 3, "edges": [ { "node": { "id": to_base64("ProjectType", p.id), "milestones": [], "name": p.name, }, } for p in [p1, p2, p3] ], }, } @pytest.mark.django_db(transaction=True) def test_query_connection_nested(db, gql_client: GraphQLTestClient): query = """ query TestQuery { tagList { id name issues (first: 2) { totalCount edges { node { id name } } } } } """ t1 = TagFactory.create() t2 = TagFactory.create() t1_issues = IssueFactory.create_batch(10) for issue in t1_issues: t1.issues.add(issue) t2_issues = IssueFactory.create_batch(10) for issue in t2_issues: t2.issues.add(issue) with assert_num_queries(2 if DjangoOptimizerExtension.enabled.get() else 5): res = gql_client.query(query) assert res.data == { "tagList": [ { "id": to_base64("TagType", t1.id), "name": t1.name, "issues": { "totalCount": 10, "edges": [ {"node": {"id": to_base64("IssueType", t.id), "name": t.name}} for t in t1_issues[:2] ], }, }, { "id": to_base64("TagType", t2.id), "name": t2.name, "issues": { "totalCount": 10, "edges": [ {"node": {"id": to_base64("IssueType", t.id), "name": t.name}} for t in t2_issues[:2] ], }, }, ], } @pytest.mark.django_db(transaction=True) def test_query_nested_fragments(db, gql_client: GraphQLTestClient): query = """ query TestQuery { issueConn { ...IssueConnection2 ...IssueConnection1 } } fragment IssueConnection1 on IssueTypeConnection { edges { node { issueAssignees { id } } } } fragment IssueConnection2 on IssueTypeConnection { edges { node { milestone { id project { name } } } } } """ UserFactory.create() expected = {"issueConn": {"edges": []}} for i in IssueFactory.create_batch(2): assert i.milestone assert i.milestone.project assignee1 = Assignee.objects.create(user=UserFactory.create(), issue=i) assignee2 = Assignee.objects.create(user=UserFactory.create(), issue=i) expected["issueConn"]["edges"].append( { "node": { "issueAssignees": [ {"id": to_base64("AssigneeType", assignee1.pk)}, {"id": to_base64("AssigneeType", assignee2.pk)}, ], "milestone": { "id": to_base64("MilestoneType", i.milestone.pk), "project": {"name": i.milestone.project.name}, }, }, }, ) with assert_num_queries(2 if DjangoOptimizerExtension.enabled.get() else 7): res = gql_client.query(query) assert res.data == expected @pytest.mark.django_db(transaction=True) def test_query_annotate(db, gql_client: GraphQLTestClient): query = """ query TestQuery ($node_id: ID!) { project (id: $node_id) { id name isDelayed milestonesCount isSmall } } """ expected = [] today = timezone.now().date() for p in ProjectFactory.create_batch(2): ms = MilestoneFactory.create_batch(3, project=p) assert p.due_date is not None p_res: dict[str, Any] = { "id": to_base64("ProjectType", p.id), "name": p.name, "isDelayed": p.due_date < today, "milestonesCount": len(ms), "isSmall": len(ms) < 3, } expected.append(p_res) for p in ProjectFactory.create_batch( 2, due_date=today - datetime.timedelta(days=1), ): ms = MilestoneFactory.create_batch(2, project=p) assert p.due_date is not None p_res: dict[str, Any] = { "id": to_base64("ProjectType", p.id), "name": p.name, "isDelayed": p.due_date < today, "milestonesCount": len(ms), "isSmall": len(ms) < 3, } expected.append(p_res) assert len(expected) == 4 for e in expected: if DjangoOptimizerExtension.enabled.get(): with assert_num_queries(1): res = gql_client.query(query, {"node_id": e["id"]}) assert res.data == {"project": e} else: # isDelayed and milestonesCount requires the optimizer to be turned on res = gql_client.query( query, {"node_id": e["id"]}, assert_no_errors=False, ) assert res.errors @pytest.mark.django_db(transaction=True) def test_query_annotate_with_callable(db, gql_client: GraphQLTestClient): query = """ query TestQuery ($node_id: ID!) { project (id: $node_id) { id name milestones { id name myBugsCount } } } """ user = UserFactory.create() expected = [] for p in ProjectFactory.create_batch(2): p_res: dict[str, Any] = { "id": to_base64("ProjectType", p.id), "name": p.name, "milestones": [], } expected.append(p_res) for m in MilestoneFactory.create_batch(2, project=p): m_res: dict[str, Any] = { "id": to_base64("MilestoneType", m.id), "name": m.name, "myBugsCount": 0, } p_res["milestones"].append(m_res) # Those issues are not assigned to the user, # thus they should not be counted IssueFactory.create_batch(2, milestone=m, kind=Issue.Kind.BUG) # Those issues are not bugs, # thus they should not be counted IssueFactory.create_batch(3, milestone=m, kind=Issue.Kind.FEATURE) # Those issues are bugs assigned to the user, # thus they will be counted for i in IssueFactory.create_batch(4, milestone=m, kind=Issue.Kind.BUG): Assignee.objects.create(user=user, issue=i) m_res["myBugsCount"] += 1 assert len(expected) == 2 for e in expected: with gql_client.login(user): if DjangoOptimizerExtension.enabled.get(): with assert_num_queries(4): res = gql_client.query(query, {"node_id": e["id"]}) assert res.data == {"project": e} else: # myBugsCount requires the optimizer to be turned on res = gql_client.query( query, {"node_id": e["id"]}, assert_no_errors=False, ) assert res.errors @pytest.mark.django_db(transaction=True) def test_user_query_with_prefetch(): @strawberry_django.type( Project, ) class ProjectTypeWithPrefetch: @strawberry_django.field( prefetch_related=[ Prefetch( "milestones", queryset=Milestone.objects.all(), to_attr="prefetched_milestones", ), ], ) def custom_field(self, info) -> str: if hasattr(self, "prefetched_milestones"): return "prefetched" return "not prefetched" @strawberry_django.type( Milestone, ) class MilestoneTypeWithNestedPrefetch: project: ProjectTypeWithPrefetch MilestoneFactory.create() @strawberry.type class Query: milestones: list[MilestoneTypeWithNestedPrefetch] = strawberry_django.field() query = utils.generate_query(Query, enable_optimizer=True) query_str = """ query TestQuery { milestones { project { customField } } } """ assert DjangoOptimizerExtension.enabled.get() result = query(query_str) assert isinstance(result, ExecutionResult) assert not result.errors assert result.data == { "milestones": [ { "project": { "customField": "prefetched", }, }, ], } result2 = query(query_str) assert isinstance(result2, ExecutionResult) assert not result2.errors assert result2.data == { "milestones": [ { "project": { "customField": "prefetched", }, }, ], } @pytest.mark.django_db(transaction=True) def test_query_select_related_with_only(db, gql_client: GraphQLTestClient): query = """ query TestQuery ($id: ID!) { issue (id: $id) { id milestoneName } } """ milestone = MilestoneFactory.create() issue = IssueFactory.create(milestone=milestone) with assert_num_queries(1 if DjangoOptimizerExtension.enabled.get() else 2): res = gql_client.query(query, {"id": to_base64("IssueType", issue.pk)}) assert res.data == { "issue": { "id": to_base64("IssueType", issue.id), "milestoneName": milestone.name, }, } @pytest.mark.django_db(transaction=True) def test_query_select_related_without_only(db, gql_client: GraphQLTestClient): query = """ query TestQuery ($id: ID!) { issue (id: $id) { id milestoneNameWithoutOnlyOptimization } } """ milestone = MilestoneFactory.create() issue = IssueFactory.create(milestone=milestone) with assert_num_queries(1 if DjangoOptimizerExtension.enabled.get() else 2): res = gql_client.query(query, {"id": to_base64("IssueType", issue.pk)}) assert res.data == { "issue": { "id": to_base64("IssueType", issue.id), "milestoneNameWithoutOnlyOptimization": milestone.name, }, } @pytest.mark.django_db(transaction=True) def test_handles_existing_select_related(db, gql_client: GraphQLTestClient): """select_related should not cause errors, even if the field does not get queried.""" # We're *not* querying the issues' milestones, even though it's # prefetched. query = """ query TestQuery { tagList { issuesWithSelectedRelatedMilestoneAndProject { id name } } } """ tag = TagFactory.create() issues = IssueFactory.create_batch(3) for issue in issues: tag.issues.add(issue) with assert_num_queries(2): res = gql_client.query(query) assert res.data == { "tagList": [ { "issuesWithSelectedRelatedMilestoneAndProject": [ {"id": to_base64("IssueType", t.id), "name": t.name} for t in sorted(issues, key=lambda i: i.pk) ], }, ], } @pytest.mark.django_db(transaction=True) def test_query_nested_connection_with_filter(db, gql_client: GraphQLTestClient): query = """ query TestQuery ($id: ID!) { milestone(id: $id) { id issuesWithFilters (filters: {search: "Foo"}) { edges { node { id } } } } } """ milestone = MilestoneFactory.create() issue1 = IssueFactory.create(milestone=milestone, name="Foo") issue2 = IssueFactory.create(milestone=milestone, name="Foo Bar") issue3 = IssueFactory.create(milestone=milestone, name="Bar Foo") IssueFactory.create(milestone=milestone, name="Bar Bin") with assert_num_queries(2): res = gql_client.query(query, {"id": to_base64("MilestoneType", milestone.pk)}) assert isinstance(res.data, dict) result = res.data["milestone"] assert isinstance(result, dict) expected = {to_base64("IssueType", i.pk) for i in [issue1, issue2, issue3]} assert { edge["node"]["id"] for edge in result["issuesWithFilters"]["edges"] } == expected @pytest.mark.django_db(transaction=True) def test_query_nested_connection_with_filter_and_alias( db, gql_client: GraphQLTestClient ): query = """ query TestQuery ($id: ID!) { milestone(id: $id) { id fooIssues: issuesWithFilters (filters: {search: "Foo"}) { edges { node { id } } } barIssues: issuesWithFilters (filters: {search: "Bar"}) { edges { node { id } } } } } """ milestone = MilestoneFactory.create() issue1 = IssueFactory.create(milestone=milestone, name="Foo") issue2 = IssueFactory.create(milestone=milestone, name="Foo Bar") issue3 = IssueFactory.create(milestone=milestone, name="Bar Foo") issue4 = IssueFactory.create(milestone=milestone, name="Bar Bin") with assert_num_queries(3): res = gql_client.query(query, {"id": to_base64("MilestoneType", milestone.pk)}) assert isinstance(res.data, dict) result = res.data["milestone"] assert isinstance(result, dict) foo_expected = {to_base64("IssueType", i.pk) for i in [issue1, issue2, issue3]} assert {edge["node"]["id"] for edge in result["fooIssues"]["edges"]} == foo_expected bar_expected = {to_base64("IssueType", i.pk) for i in [issue2, issue3, issue4]} assert {edge["node"]["id"] for edge in result["barIssues"]["edges"]} == bar_expected @pytest.mark.django_db(transaction=True) def test_query_with_optimizer_paginated_prefetch(): @strawberry_django.type(Milestone, pagination=True) class MilestoneTypeWithNestedPrefetch: @strawberry_django.field() def name(self, info) -> str: return self.name @strawberry_django.type( Project, ) class ProjectTypeWithPrefetch: @strawberry_django.field() def name(self, info) -> str: return self.name milestones: list[MilestoneTypeWithNestedPrefetch] milestone1 = MilestoneFactory.create() project = milestone1.project MilestoneFactory.create(project=project) @strawberry.type class Query: projects: list[ProjectTypeWithPrefetch] = strawberry_django.field() query1 = utils.generate_query(Query, enable_optimizer=False) query_str = """ fragment f on ProjectTypeWithPrefetch { milestones (pagination: {limit: 1}) { name } } query TestQuery { projects { name ...f } } """ # NOTE: The following assertion doesn't work because the # DjangoOptimizerExtension instance is not the one within the # generate_query wrapper """ assert DjangoOptimizerExtension.enabled.get() """ result1 = query1(query_str) assert isinstance(result1, ExecutionResult) assert not result1.errors assert result1.data == { "projects": [ { "name": project.name, "milestones": [ { "name": milestone1.name, }, ], }, ], } query2 = utils.generate_query(Query, enable_optimizer=True) result2 = query2(query_str) assert isinstance(result2, ExecutionResult) assert result2.data == { "projects": [ { "name": project.name, "milestones": [ { "name": milestone1.name, }, ], }, ], } @pytest.mark.django_db(transaction=True) def test_nested_prefetch_with_filter(db, gql_client: GraphQLTestClient): query = """ query TestQuery ($id: ID!) { milestone(id: $id) { id name issues (filters: {search: "Foo"}) { id name } } } """ milestone = MilestoneFactory.create() issue1 = IssueFactory.create(milestone=milestone, name="Foo") issue2 = IssueFactory.create(milestone=milestone, name="Foo Bar") IssueFactory.create(milestone=milestone, name="Bar") issue4 = IssueFactory.create(milestone=milestone, name="Bar Foo") IssueFactory.create(milestone=milestone, name="Bar Bin") with assert_num_queries(2): res = gql_client.query( query, {"id": to_base64("MilestoneType", milestone.pk)}, ) assert isinstance(res.data, dict) assert res.data == { "milestone": { "id": to_base64("MilestoneType", milestone.pk), "name": milestone.name, "issues": [ { "id": to_base64("IssueType", issue.pk), "name": issue.name, } for issue in [issue1, issue2, issue4] ], }, } @pytest.mark.django_db(transaction=True) def test_nested_prefetch_with_filter_and_pagination(db, gql_client: GraphQLTestClient): query = """ query TestQuery ($id: ID!) { milestone(id: $id) { id name issues (filters: {search: "Foo"}, pagination: {limit: 2}) { id name } } } """ milestone = MilestoneFactory.create() issue1 = IssueFactory.create(milestone=milestone, name="Foo") issue2 = IssueFactory.create(milestone=milestone, name="Foo Bar") IssueFactory.create(milestone=milestone, name="Bar") IssueFactory.create(milestone=milestone, name="Bar Foo") IssueFactory.create(milestone=milestone, name="Bar Bin") with assert_num_queries(2): res = gql_client.query( query, {"id": to_base64("MilestoneType", milestone.pk)}, ) assert isinstance(res.data, dict) assert res.data == { "milestone": { "id": to_base64("MilestoneType", milestone.pk), "name": milestone.name, "issues": [ { "id": to_base64("IssueType", issue.pk), "name": issue.name, } for issue in [issue1, issue2] ], }, } @pytest.mark.django_db(transaction=True) def test_nested_prefetch_with_multiple_levels(db, gql_client: GraphQLTestClient): query = """ query TestQuery ($id: ID!) { milestone(id: $id) { id name issues (order: { name: ASC }) { id name tags { id name } } } } """ milestone = MilestoneFactory.create() issue1 = IssueFactory.create(milestone=milestone, name="2Foo") issue2 = IssueFactory.create(milestone=milestone, name="1Foo") issue3 = IssueFactory.create(milestone=milestone, name="4Foo") issue4 = IssueFactory.create(milestone=milestone, name="3Foo") issue5 = IssueFactory.create(milestone=milestone, name="5Foo") tag1 = TagFactory.create() issue1.tags.add(tag1) issue2.tags.add(tag1) tag2 = TagFactory.create() issue2.tags.add(tag2) issue3.tags.add(tag2) with assert_num_queries(3 if DjangoOptimizerExtension.enabled.get() else 7): res = gql_client.query( query, {"id": to_base64("MilestoneType", milestone.pk)}, ) expected_issues = [ { "id": to_base64("IssueType", issue.pk), "name": issue.name, "tags": [ {"id": to_base64("TagType", tag.pk), "name": tag.name} for tag in tags ], } for issue, tags in [ (issue2, [tag1, tag2]), (issue1, [tag1]), (issue4, []), (issue3, [tag2]), (issue5, []), ] ] assert isinstance(res.data, dict) assert res.data == { "milestone": { "id": to_base64("MilestoneType", milestone.pk), "name": milestone.name, "issues": expected_issues, }, } @pytest.mark.django_db(transaction=True) def test_nested_prefetch_with_get_queryset( db, gql_client: GraphQLTestClient, mocker: MockerFixture, ): mock_get_queryset = mocker.spy(StaffType, "get_queryset") query = """ query TestQuery ($id: ID!) { issue(id: $id) { id staffAssignees { id } } } """ issue = IssueFactory.create() user = UserFactory.create() staff = StaffUserFactory.create() for u in [user, staff]: Assignee.objects.create(user=u, issue=issue) res = gql_client.query( query, {"id": to_base64("IssueType", issue.pk)}, ) assert isinstance(res.data, dict) assert res.data == { "issue": { "id": to_base64("IssueType", issue.pk), "staffAssignees": [{"id": to_base64("StaffType", staff.username)}], }, } mock_get_queryset.assert_called_once() @pytest.mark.django_db(transaction=True) def test_prefetch_hint_with_same_name_field_no_extra_queries( db, ): @strawberry_django.type(Issue) class IssueType: pk: strawberry.ID @strawberry_django.type(Milestone) class MilestoneType: pk: strawberry.ID @strawberry_django.field( prefetch_related=[ lambda info: Prefetch( "issues", queryset=Issue.objects.filter(name__startswith="Foo"), to_attr="_my_issues", ), ], ) def issues(self) -> list[IssueType]: return self._my_issues # type: ignore @strawberry.type class Query: milestone: MilestoneType = strawberry_django.field() schema = strawberry.Schema(query=Query, extensions=[DjangoOptimizerExtension]) milestone1 = MilestoneFactory.create() milestone2 = MilestoneFactory.create() issue1 = IssueFactory.create(name="Foo", milestone=milestone1) IssueFactory.create(name="Bar", milestone=milestone1) IssueFactory.create(name="Foo", milestone=milestone2) query = """\ query TestQuery ($pk: ID!) { milestone(pk: $pk) { pk issues { pk } } } """ with assert_num_queries(2): res = schema.execute_sync(query, {"pk": milestone1.pk}) assert res.errors is None assert res.data == { "milestone": { "pk": str(milestone1.pk), "issues": [{"pk": str(issue1.pk)}], }, } @pytest.mark.django_db(transaction=True) def test_query_paginated(db, gql_client: GraphQLTestClient): query = """ query TestQuery ($pagination: OffsetPaginationInput) { issuesPaginated (pagination: $pagination) { totalCount results { name milestone { name } } } } """ milestone1 = MilestoneFactory.create() milestone2 = MilestoneFactory.create() issue1 = IssueFactory.create(milestone=milestone1) issue2 = IssueFactory.create(milestone=milestone1) issue3 = IssueFactory.create(milestone=milestone1) issue4 = IssueFactory.create(milestone=milestone2) issue5 = IssueFactory.create(milestone=milestone2) with assert_num_queries(2 if DjangoOptimizerExtension.enabled.get() else 7): res = gql_client.query(query) assert res.data == { "issuesPaginated": { "totalCount": 5, "results": [ {"name": issue1.name, "milestone": {"name": milestone1.name}}, {"name": issue2.name, "milestone": {"name": milestone1.name}}, {"name": issue3.name, "milestone": {"name": milestone1.name}}, {"name": issue4.name, "milestone": {"name": milestone2.name}}, {"name": issue5.name, "milestone": {"name": milestone2.name}}, ], } } with assert_num_queries(2 if DjangoOptimizerExtension.enabled.get() else 4): res = gql_client.query(query, variables={"pagination": {"limit": 2}}) assert res.data == { "issuesPaginated": { "totalCount": 5, "results": [ {"name": issue1.name, "milestone": {"name": milestone1.name}}, {"name": issue2.name, "milestone": {"name": milestone1.name}}, ], } } with assert_num_queries(2 if DjangoOptimizerExtension.enabled.get() else 4): res = gql_client.query( query, variables={"pagination": {"limit": 2, "offset": 2}} ) assert res.data == { "issuesPaginated": { "totalCount": 5, "results": [ {"name": issue3.name, "milestone": {"name": milestone1.name}}, {"name": issue4.name, "milestone": {"name": milestone2.name}}, ], } } @pytest.mark.django_db(transaction=True) def test_query_paginated_nested(db, gql_client: GraphQLTestClient): query = """ query TestQuery ($pagination: OffsetPaginationInput) { milestoneList { name issuesPaginated (pagination: $pagination) { totalCount results { name milestone { name } } } } } """ milestone1 = MilestoneFactory.create() milestone2 = MilestoneFactory.create() issue1 = IssueFactory.create(milestone=milestone1) issue2 = IssueFactory.create(milestone=milestone1) issue3 = IssueFactory.create(milestone=milestone1) issue4 = IssueFactory.create(milestone=milestone2) issue5 = IssueFactory.create(milestone=milestone2) with assert_num_queries(2 if DjangoOptimizerExtension.enabled.get() else 5): res = gql_client.query(query) assert res.data == { "milestoneList": [ { "name": milestone1.name, "issuesPaginated": { "totalCount": 3, "results": [ {"name": issue1.name, "milestone": {"name": milestone1.name}}, {"name": issue2.name, "milestone": {"name": milestone1.name}}, {"name": issue3.name, "milestone": {"name": milestone1.name}}, ], }, }, { "name": milestone2.name, "issuesPaginated": { "totalCount": 2, "results": [ {"name": issue4.name, "milestone": {"name": milestone2.name}}, {"name": issue5.name, "milestone": {"name": milestone2.name}}, ], }, }, ] } with assert_num_queries(2 if DjangoOptimizerExtension.enabled.get() else 5): res = gql_client.query(query, variables={"pagination": {"limit": 1}}) assert res.data == { "milestoneList": [ { "name": milestone1.name, "issuesPaginated": { "totalCount": 3, "results": [ {"name": issue1.name, "milestone": {"name": milestone1.name}}, ], }, }, { "name": milestone2.name, "issuesPaginated": { "totalCount": 2, "results": [ {"name": issue4.name, "milestone": {"name": milestone2.name}}, ], }, }, ] } with assert_num_queries(3 if DjangoOptimizerExtension.enabled.get() else 5): res = gql_client.query( query, variables={"pagination": {"limit": 1, "offset": 2}} ) assert res.data == { "milestoneList": [ { "name": milestone1.name, "issuesPaginated": { "totalCount": 3, "results": [ {"name": issue3.name, "milestone": {"name": milestone1.name}}, ], }, }, { "name": milestone2.name, "issuesPaginated": { "totalCount": 2, "results": [], }, }, ] } @pytest.mark.django_db(transaction=True) def test_prefetch_multi_field_single_optional(db, gql_client: GraphQLTestClient): milestone1 = MilestoneFactory.create() milestone2 = MilestoneFactory.create() issue = IssueFactory.create(name="Foo", milestone=milestone1) issue_id = str( GlobalID(get_object_definition(IssueType, strict=True).name, str(issue.id)) ) milestone_id_1 = str( GlobalID( get_object_definition(MilestoneType, strict=True).name, str(milestone1.id) ) ) milestone_id_2 = str( GlobalID( get_object_definition(MilestoneType, strict=True).name, str(milestone2.id) ) ) query = """\ query TestQuery($id1: ID!, $id2: ID!) { a: milestone(id: $id1) { firstIssue { id } } b: milestone(id: $id2) { firstIssue { id } } } """ with assert_num_queries(4): res = gql_client.query( query, variables={"id1": milestone_id_1, "id2": milestone_id_2} ) assert res.errors is None assert res.data == { "a": { "firstIssue": { "id": issue_id, }, }, "b": { "firstIssue": None, }, } @pytest.mark.django_db(transaction=True) def test_prefetch_multi_field_single_required(db, gql_client: GraphQLTestClient): milestone = MilestoneFactory.create() issue = IssueFactory.create(name="Foo", milestone=milestone) issue_id = str( GlobalID(get_object_definition(IssueType, strict=True).name, str(issue.id)) ) milestone_id = str( GlobalID( get_object_definition(MilestoneType, strict=True).name, str(milestone.id) ) ) query = """\ query TestQuery($id: ID!) { milestone(id: $id) { firstIssueRequired { id } } } """ with assert_num_queries(2): res = gql_client.query(query, variables={"id": milestone_id}) assert res.errors is None assert res.data == { "milestone": { "firstIssueRequired": { "id": issue_id, }, }, } @pytest.mark.django_db(transaction=True) def test_prefetch_multi_field_single_required_missing( db, gql_client: GraphQLTestClient ): milestone1 = MilestoneFactory.create() milestone_id = str( GlobalID( get_object_definition(MilestoneType, strict=True).name, str(milestone1.id) ) ) query = """\ query TestQuery($id: ID!) { milestone(id: $id) { firstIssueRequired { id } } } """ with assert_num_queries(2): res = gql_client.query( query, variables={"id": milestone_id}, assert_no_errors=False ) assert res.errors is not None assert res.errors == [ { "locations": [{"column": 11, "line": 3}], "message": "Issue matching query does not exist.", "path": ["milestone", "firstIssueRequired"], } ] @pytest.mark.django_db(transaction=True) def test_prefetch_multi_field_single_required_multiple_returned( db, gql_client: GraphQLTestClient ): milestone = MilestoneFactory.create() milestone_id = str( GlobalID( get_object_definition(MilestoneType, strict=True).name, str(milestone.id) ) ) IssueFactory.create(name="Foo", milestone=milestone) IssueFactory.create(name="Bar", milestone=milestone) query = """\ query TestQuery($id: ID!) { milestone(id: $id) { firstIssueRequired { id } } } """ with assert_num_queries(2): res = gql_client.query( query, variables={"id": milestone_id}, assert_no_errors=False ) assert res.errors is not None assert res.errors == [ { "locations": [{"column": 11, "line": 3}], "message": "get() returned more than one Issue -- it returned 2!", "path": ["milestone", "firstIssueRequired"], } ] @pytest.mark.django_db(transaction=True) def test_no_window_function_for_normal_prefetch( db, ): @strawberry_django.type(Project) class ProjectType: pk: strawberry.ID name: str @staticmethod def get_queryset(qs, info): # get_queryset exists to force the optimizer to use prefetch instead of select_related return qs @strawberry_django.type(Milestone) class MilestoneType: pk: strawberry.ID project: ProjectType @strawberry.type class Query: milestones: list[MilestoneType] = strawberry_django.field() schema = strawberry.Schema(query=Query, extensions=[DjangoOptimizerExtension]) milestone1 = MilestoneFactory.create() milestone2 = MilestoneFactory.create() query = """\ query TestQuery { milestones { pk project { pk name } } } """ with CaptureQueriesContext(connection=connections[DEFAULT_DB_ALIAS]) as ctx: res = schema.execute_sync(query) assert len(ctx.captured_queries) == 2 # Test that the Prefetch does not use Window pagination unnecessarily assert not any( '"_strawberry_row_number"' in q["sql"] for q in ctx.captured_queries ) assert res.errors is None assert res.data == { "milestones": [ { "pk": str(milestone1.pk), "project": { "pk": str(milestone1.project.pk), "name": milestone1.project.name, }, }, { "pk": str(milestone2.pk), "project": { "pk": str(milestone2.project.pk), "name": milestone2.project.name, }, }, ] } @pytest.mark.django_db(transaction=True) @pytest.mark.parametrize("gql_client", ["async", "sync"], indirect=True) def test_custom_prefetch_optimization(gql_client): project = ProjectFactory.create() milestone = MilestoneFactory.create(project=project, name="Hello") project_id = str( GlobalID(get_object_definition(ProjectType, strict=True).name, str(project.id)) ) milestone_id = str( GlobalID( get_object_definition(MilestoneType, strict=True).name, str(milestone.id) ) ) query = """\ query TestQuery($id: ID!) { project(id: $id) { id customMilestones { id name } } } """ with assert_num_queries(2) as ctx: res = gql_client.query( query, variables={"id": project_id}, assert_no_errors=False ) assert Milestone._meta.db_table in ctx.captured_queries[1]["sql"] assert ( Milestone._meta.get_field("due_date").name not in ctx.captured_queries[1]["sql"] ) assert res.errors is None assert res.data == { "project": { "id": project_id, "customMilestones": [{"id": milestone_id, "name": milestone.name}], } } @pytest.mark.django_db(transaction=True) @pytest.mark.parametrize("gql_client", ["async", "sync"], indirect=True) def test_custom_prefetch_optimization_nested(gql_client): project = ProjectFactory.create() milestone1 = MilestoneFactory.create(project=project, name="Hello1") milestone2 = MilestoneFactory.create(project=project, name="Hello2") project_id = str( GlobalID(get_object_definition(ProjectType, strict=True).name, str(project.id)) ) milestone1_id = str( GlobalID( get_object_definition(MilestoneType, strict=True).name, str(milestone1.id) ) ) milestone2_id = str( GlobalID( get_object_definition(MilestoneType, strict=True).name, str(milestone2.id) ) ) query = """\ query TestQuery($id: ID!) { milestone(id: $id) { id project { id customMilestones { id name } } } } """ with assert_num_queries(2) as ctx: res = gql_client.query( query, variables={"id": milestone1_id}, assert_no_errors=False ) assert Milestone._meta.db_table in ctx.captured_queries[1]["sql"] assert ( Milestone._meta.get_field("due_date").name not in ctx.captured_queries[1]["sql"] ) assert res.errors is None assert res.data == { "milestone": { "id": milestone1_id, "project": { "id": project_id, "customMilestones": [ {"id": milestone1_id, "name": milestone1.name}, {"id": milestone2_id, "name": milestone2.name}, ], }, } } @pytest.mark.django_db(transaction=True) @pytest.mark.parametrize("gql_client", ["async", "sync"], indirect=True) def test_custom_prefetch_model_property_optimization(gql_client): project = ProjectFactory.create() milestone = MilestoneFactory.create(project=project, name="Hello") project_id = str( GlobalID(get_object_definition(ProjectType, strict=True).name, str(project.id)) ) milestone_id = str( GlobalID( get_object_definition(MilestoneType, strict=True).name, str(milestone.id) ) ) query = """\ query TestQuery($id: ID!) { project(id: $id) { id customMilestonesModelProperty { id name } } } """ with assert_num_queries(2) as ctx: res = gql_client.query( query, variables={"id": project_id}, assert_no_errors=False ) assert Milestone._meta.db_table in ctx.captured_queries[1]["sql"] assert ( Milestone._meta.get_field("due_date").name not in ctx.captured_queries[1]["sql"] ) assert res.errors is None assert res.data == { "project": { "id": project_id, "customMilestonesModelProperty": [ {"id": milestone_id, "name": milestone.name} ], } } @pytest.mark.django_db(transaction=True) @pytest.mark.parametrize("gql_client", ["async", "sync"], indirect=True) def test_custom_prefetch_optimization_model_property_nested(gql_client): project = ProjectFactory.create() milestone1 = MilestoneFactory.create(project=project, name="Hello1") milestone2 = MilestoneFactory.create(project=project, name="Hello2") project_id = str( GlobalID(get_object_definition(ProjectType, strict=True).name, str(project.id)) ) milestone1_id = str( GlobalID( get_object_definition(MilestoneType, strict=True).name, str(milestone1.id) ) ) milestone2_id = str( GlobalID( get_object_definition(MilestoneType, strict=True).name, str(milestone2.id) ) ) query = """\ query TestQuery($id: ID!) { milestone(id: $id) { id project { id customMilestonesModelProperty { id name } } } } """ with assert_num_queries(2) as ctx: res = gql_client.query( query, variables={"id": milestone1_id}, assert_no_errors=False ) assert Milestone._meta.db_table in ctx.captured_queries[1]["sql"] assert ( Milestone._meta.get_field("due_date").name not in ctx.captured_queries[1]["sql"] ) assert res.errors is None assert res.data == { "milestone": { "id": milestone1_id, "project": { "id": project_id, "customMilestonesModelProperty": [ {"id": milestone1_id, "name": milestone1.name}, {"id": milestone2_id, "name": milestone2.name}, ], }, } } @pytest.mark.django_db(transaction=True) @pytest.mark.parametrize("gql_client", ["async", "sync"], indirect=True) def test_correct_annotation_info(gql_client): project = ProjectFactory.create() milestone = MilestoneFactory.create(project=project, name="Hello") project_id = str( GlobalID(get_object_definition(ProjectType, strict=True).name, str(project.id)) ) milestone_id = str( GlobalID( get_object_definition(MilestoneType, strict=True).name, str(milestone.id) ) ) query = """\ query TestQuery($id: ID!) { project(id: $id) { id milestones { id graphqlPath } } } """ res = gql_client.query(query, variables={"id": project_id}, assert_no_errors=False) assert res.errors is None assert res.data == { "project": { "id": project_id, "milestones": [ { "id": milestone_id, "graphqlPath": "project,0,milestones,0,graphqlPath", } ], } } @pytest.mark.django_db(transaction=True) @pytest.mark.parametrize("gql_client", ["async", "sync"], indirect=True) def test_correct_annotation_info_nested(gql_client): project = ProjectFactory.create() milestone1 = MilestoneFactory.create(project=project, name="Hello1") milestone2 = MilestoneFactory.create(project=project, name="Hello2") project_id = str( GlobalID(get_object_definition(ProjectType, strict=True).name, str(project.id)) ) milestone1_id = str( GlobalID( get_object_definition(MilestoneType, strict=True).name, str(milestone1.id) ) ) milestone2_id = str( GlobalID( get_object_definition(MilestoneType, strict=True).name, str(milestone2.id) ) ) query = """\ query TestQuery($id: ID!) { milestone(id: $id) { id graphqlPath project { id milestones { id graphqlPath } } } } """ res = gql_client.query( query, variables={"id": milestone1_id}, assert_no_errors=False ) assert res.errors is None assert res.data == { "milestone": { "id": milestone1_id, "graphqlPath": "milestone,0,graphqlPath", "project": { "id": project_id, "milestones": [ { "id": milestone1_id, "graphqlPath": "milestone,0,project,0,milestones,0,graphqlPath", }, { "id": milestone2_id, "graphqlPath": "milestone,0,project,0,milestones,0,graphqlPath", }, ], }, } } @pytest.mark.django_db(transaction=True) @pytest.mark.parametrize("gql_client", ["async", "sync"], indirect=True) def test_mixed_annotation_prefetch(gql_client): project = ProjectFactory.create() MilestoneFactory.create(project=project, name="Hello") project_id = str( GlobalID(get_object_definition(ProjectType, strict=True).name, str(project.id)) ) query = """\ query TestQuery($id: ID!) { project(id: $id) { milestones { mixedAnnotatedPrefetch mixedPrefetchAnnotated } } } """ res = gql_client.query(query, variables={"id": project_id}, assert_no_errors=False) assert res.errors is None assert res.data == { "project": { "milestones": [ { "mixedAnnotatedPrefetch": "dummy", "mixedPrefetchAnnotated": "dummy", } ], } } strawberry-graphql-django-0.62.0/tests/test_ordering.py000066400000000000000000000311331502405145400232640ustar00rootroot00000000000000import textwrap from typing import Annotated, Optional import pytest import strawberry from django.db.models import Case, Count, Value, When from django.db.models.functions import Reverse from strawberry import auto from strawberry.annotation import StrawberryAnnotation from strawberry.relay import Node from strawberry.types.base import ( StrawberryOptional, get_object_definition, ) from strawberry.types.field import StrawberryField import strawberry_django from strawberry_django.fields.field import StrawberryDjangoField from strawberry_django.fields.filter_order import ( FilterOrderField, FilterOrderFieldResolver, ) from strawberry_django.ordering import Ordering from strawberry_django.pagination import OffsetPaginated from strawberry_django.relay import DjangoListConnection from tests import models, utils from tests.types import Fruit @strawberry_django.order_type(models.Color, name="ColorOrder") class _ColorOrder: pk: auto @strawberry_django.order_field def name(self, prefix, value: auto): return [value.resolve(f"{prefix}name")] ColorOrder = Annotated["_ColorOrder", strawberry.lazy("tests.test_ordering")] @strawberry_django.order_type(models.Fruit) class FruitOrder: color_id: auto name: auto sweetness: auto color: Optional[ColorOrder] @strawberry_django.order_field def types_number(self, queryset, prefix, value: auto): return queryset.annotate( count=Count(f"{prefix}types__id"), count_nulls=Case( When(count=0, then=Value(None)), default="count", ), ), [value.resolve("count_nulls")] @strawberry_django.order_type(models.Fruit) class CustomFruitOrder: reverse_name: auto @strawberry_django.order_field def order(self, info, queryset, prefix): queryset = queryset.annotate(reverse_name=Reverse(f"{prefix}name")) return strawberry_django.ordering.process_ordering_default( self, info, queryset, prefix ) @strawberry_django.type(models.Fruit, ordering=FruitOrder) class FruitWithOrder: id: auto name: auto @strawberry_django.type(models.Fruit) class FruitNode(Node): name: auto @strawberry_django.type(models.Fruit, ordering=FruitOrder) class FruitWithOrderNode(Node): name: auto @strawberry.type class Query: fruits: list[Fruit] = strawberry_django.field(ordering=FruitOrder) fruits_connection: DjangoListConnection[FruitNode] = strawberry_django.connection( ordering=FruitOrder ) fruits_paginated: OffsetPaginated[Fruit] = strawberry_django.offset_paginated( ordering=FruitOrder ) custom_order_fruits: list[Fruit] = strawberry_django.field( ordering=CustomFruitOrder ) fruits_with_order: list[FruitWithOrder] = strawberry_django.field() fruits_with_order_connection: DjangoListConnection[FruitWithOrderNode] = ( strawberry_django.connection() ) fruits_with_order_paginated: OffsetPaginated[FruitWithOrder] = ( strawberry_django.offset_paginated() ) @pytest.fixture def query(): return utils.generate_query(Query) def test_correct_ordering_schema(): @strawberry_django.type(models.Fruit, name="Fruit") class MiniFruit: id: auto name: auto @strawberry_django.order_type(models.Fruit, name="FruitOrder") class MiniFruitOrder: name: auto @strawberry.type(name="Query") class MiniQuery: fruits: list[MiniFruit] = strawberry_django.field(ordering=MiniFruitOrder) schema = strawberry.Schema(query=MiniQuery) expected = """\ directive @oneOf on INPUT_OBJECT type Fruit { id: ID! name: String! } input FruitOrder @oneOf { name: Ordering } enum Ordering { ASC ASC_NULLS_FIRST ASC_NULLS_LAST DESC DESC_NULLS_FIRST DESC_NULLS_LAST } type Query { fruits(ordering: [FruitOrder!]! = []): [Fruit!]! } """ assert textwrap.dedent(str(schema)) == textwrap.dedent(expected).strip() def test_custom_order_method(query, fruits): result = query( "{ customOrderFruits(ordering: [{ reverseName: ASC }]) { id name } }" ) assert not result.errors assert result.data["customOrderFruits"] == [ {"id": "3", "name": "banana"}, {"id": "2", "name": "raspberry"}, {"id": "1", "name": "strawberry"}, ] def test_field_order_definition(): field = StrawberryDjangoField(type_annotation=StrawberryAnnotation(FruitWithOrder)) assert field.get_ordering() == FruitOrder field = StrawberryDjangoField( type_annotation=StrawberryAnnotation(FruitWithOrder), ordering=None, ) assert field.get_ordering() is None def test_type_ordering(query, fruits): result = query("{ fruitsWithOrder(ordering: [{ name: ASC }]) { id name } }") assert not result.errors assert result.data["fruitsWithOrder"] == [ {"id": "3", "name": "banana"}, {"id": "2", "name": "raspberry"}, {"id": "1", "name": "strawberry"}, ] def test_type_ordering_connection(query, fruits): result = query( "{ fruitsWithOrderConnection(ordering: [{ name: ASC }]) { edges { node { name } } } }" ) assert not result.errors assert result.data["fruitsWithOrderConnection"] == { "edges": [ {"node": {"name": "banana"}}, {"node": {"name": "raspberry"}}, {"node": {"name": "strawberry"}}, ] } def test_type_ordering_paginated(query, fruits): result = query( "{ fruitsWithOrderPaginated(ordering: [{ name: ASC }]) { results { id name } } }" ) assert not result.errors assert result.data["fruitsWithOrderPaginated"] == { "results": [ {"id": "3", "name": "banana"}, {"id": "2", "name": "raspberry"}, {"id": "1", "name": "strawberry"}, ] } def test_ordering_connection(query, fruits): result = query( "{ fruitsConnection(ordering: [{ name: ASC }]) { edges { node { name } } } }" ) assert not result.errors assert result.data["fruitsConnection"] == { "edges": [ {"node": {"name": "banana"}}, {"node": {"name": "raspberry"}}, {"node": {"name": "strawberry"}}, ] } def test_ordering_paginated(query, fruits): result = query( "{ fruitsPaginated(ordering: [{ name: ASC }]) { results { id name } } }" ) assert not result.errors assert result.data["fruitsPaginated"] == { "results": [ {"id": "3", "name": "banana"}, {"id": "2", "name": "raspberry"}, {"id": "1", "name": "strawberry"}, ] } def test_asc(query, fruits): result = query("{ fruits(ordering: [{ name: ASC }]) { id name } }") assert not result.errors assert result.data["fruits"] == [ {"id": "3", "name": "banana"}, {"id": "2", "name": "raspberry"}, {"id": "1", "name": "strawberry"}, ] def test_desc(query, fruits): result = query("{ fruits(ordering: [{ name: DESC }]) { id name } }") assert not result.errors assert result.data["fruits"] == [ {"id": "1", "name": "strawberry"}, {"id": "2", "name": "raspberry"}, {"id": "3", "name": "banana"}, ] def test_multi_order(query, db): for fruit in ("strawberry", "banana", "raspberry"): models.Fruit.objects.create(name=fruit, sweetness=7) result = query( "{ fruits(ordering: [{ sweetness: ASC }, { name: ASC }]) { id name sweetness } }" ) assert not result.errors assert result.data["fruits"] == [ {"id": "2", "name": "banana", "sweetness": 7}, {"id": "3", "name": "raspberry", "sweetness": 7}, {"id": "1", "name": "strawberry", "sweetness": 7}, ] def test_relationship(query, fruits): def add_color(fruit, color_name): fruit.color = models.Color.objects.create(name=color_name) fruit.save() color_names = ["red", "dark red", "yellow"] for fruit, color_name in zip(fruits, color_names): add_color(fruit, color_name) result = query( "{ fruits(ordering: [{ color: { name: DESC } }]) { id name color { name } } }", ) assert not result.errors assert result.data["fruits"] == [ {"id": "3", "name": "banana", "color": {"name": "yellow"}}, {"id": "1", "name": "strawberry", "color": {"name": "red"}}, {"id": "2", "name": "raspberry", "color": {"name": "dark red"}}, ] def test_multi_order_respected(query, db): yellow = models.Color.objects.create(name="yellow") red = models.Color.objects.create(name="red") f1 = models.Fruit.objects.create( name="strawberry", sweetness=1, color=red, ) f2 = models.Fruit.objects.create( name="banana", sweetness=2, color=yellow, ) f3 = models.Fruit.objects.create( name="apple", sweetness=0, color=red, ) result = query("{ fruits(ordering: [{ name: ASC }, { sweetness: ASC }]) { id } }") assert not result.errors assert result.data["fruits"] == [{"id": str(f.pk)} for f in [f3, f2, f1]] result = query("{ fruits(ordering: [{ sweetness: DESC }, { name: ASC }]) { id } }") assert not result.errors assert result.data["fruits"] == [{"id": str(f.pk)} for f in [f2, f1, f3]] result = query( "{ fruits(ordering: [{ color: {name: ASC} }, { name: ASC }]) { id } }" ) assert not result.errors assert result.data["fruits"] == [{"id": str(f.pk)} for f in [f3, f1, f2]] result = query("{ fruits(ordering: [{ color: {pk: ASC} }, { name: ASC }]) { id } }") assert not result.errors assert result.data["fruits"] == [{"id": str(f.pk)} for f in [f2, f3, f1]] result = query("{ fruits(ordering: [{ colorId: ASC }, { name: ASC }]) { id } }") assert not result.errors assert result.data["fruits"] == [{"id": str(f.pk)} for f in [f2, f3, f1]] result = query("{ fruits(ordering: [{ name: ASC }, { colorId: ASC }]) { id } }") assert not result.errors assert result.data["fruits"] == [{"id": str(f.pk)} for f in [f3, f2, f1]] def test_order_type(): @strawberry_django.ordering.order_type(models.Fruit) class FruitOrder: color_id: auto name: auto sweetness: auto @strawberry_django.order_field def custom_order(self, value: auto, prefix: str): pass annotated_type = StrawberryOptional(Ordering._enum_definition) # type: ignore assert [ ( f.name, f.__class__, f.type, f.base_resolver.__class__ if f.base_resolver else None, ) for f in get_object_definition(FruitOrder, strict=True).fields ] == [ ("color_id", StrawberryField, annotated_type, None), ("name", StrawberryField, annotated_type, None), ("sweetness", StrawberryField, annotated_type, None), ( "custom_order", FilterOrderField, annotated_type, FilterOrderFieldResolver, ), ] def test_order_nulls(query, db, fruits): t1 = models.FruitType.objects.create(name="Type1") t2 = models.FruitType.objects.create(name="Type2") f1, f2, f3 = models.Fruit.objects.all() f2.types.add(t1) f3.types.add(t1, t2) result = query("{ fruits(ordering: [{ typesNumber: ASC }]) { id } }") assert not result.errors assert result.data["fruits"] == [ {"id": str(f1.id)}, {"id": str(f2.id)}, {"id": str(f3.id)}, ] result = query("{ fruits(ordering: [{ typesNumber: DESC }]) { id } }") assert not result.errors assert result.data["fruits"] == [ {"id": str(f3.id)}, {"id": str(f2.id)}, {"id": str(f1.id)}, ] result = query("{ fruits(ordering: [{ typesNumber: ASC_NULLS_FIRST }]) { id } }") assert not result.errors assert result.data["fruits"] == [ {"id": str(f1.id)}, {"id": str(f2.id)}, {"id": str(f3.id)}, ] result = query("{ fruits(ordering: [{ typesNumber: ASC_NULLS_LAST }]) { id } }") assert not result.errors assert result.data["fruits"] == [ {"id": str(f2.id)}, {"id": str(f3.id)}, {"id": str(f1.id)}, ] result = query("{ fruits(ordering: [{ typesNumber: DESC_NULLS_LAST }]) { id } }") assert not result.errors assert result.data["fruits"] == [ {"id": str(f3.id)}, {"id": str(f2.id)}, {"id": str(f1.id)}, ] result = query("{ fruits(ordering: [{ typesNumber: DESC_NULLS_FIRST }]) { id } }") assert not result.errors assert result.data["fruits"] == [ {"id": str(f1.id)}, {"id": str(f3.id)}, {"id": str(f2.id)}, ] strawberry-graphql-django-0.62.0/tests/test_paginated_type.py000066400000000000000000000611221502405145400244510ustar00rootroot00000000000000import textwrap from typing import Annotated import pytest import strawberry from django.db.models import QuerySet from django.test.utils import override_settings import strawberry_django from strawberry_django.optimizer import DjangoOptimizerExtension from strawberry_django.pagination import OffsetPaginated, OffsetPaginationInput from strawberry_django.settings import StrawberryDjangoSettings from tests import models def test_paginated_schema(): @strawberry_django.type(models.Fruit) class Fruit: id: int name: str @strawberry_django.type(models.Color) class Color: id: int name: str fruits: OffsetPaginated[Fruit] @strawberry.type class Query: fruits: OffsetPaginated[Fruit] = strawberry_django.offset_paginated() colors: OffsetPaginated[Color] = strawberry_django.offset_paginated() schema = strawberry.Schema(query=Query) expected = '''\ type Color { id: Int! name: String! fruits(pagination: OffsetPaginationInput): FruitOffsetPaginated! } type ColorOffsetPaginated { pageInfo: OffsetPaginationInfo! """Total count of existing results.""" totalCount: Int! """List of paginated results.""" results: [Color!]! } type Fruit { id: Int! name: String! } type FruitOffsetPaginated { pageInfo: OffsetPaginationInfo! """Total count of existing results.""" totalCount: Int! """List of paginated results.""" results: [Fruit!]! } type OffsetPaginationInfo { offset: Int! limit: Int } input OffsetPaginationInput { offset: Int! = 0 limit: Int } type Query { fruits(pagination: OffsetPaginationInput): FruitOffsetPaginated! colors(pagination: OffsetPaginationInput): ColorOffsetPaginated! } ''' assert textwrap.dedent(str(schema)) == textwrap.dedent(expected).strip() @pytest.mark.django_db(transaction=True) def test_pagination_query(): @strawberry_django.type(models.Fruit) class Fruit: id: int name: str @strawberry.type class Query: fruits: OffsetPaginated[Fruit] = strawberry_django.offset_paginated() models.Fruit.objects.create(name="Apple") models.Fruit.objects.create(name="Banana") models.Fruit.objects.create(name="Strawberry") schema = strawberry.Schema(query=Query) query = """\ query GetFruits ($pagination: OffsetPaginationInput) { fruits (pagination: $pagination) { totalCount results { name } } } """ res = schema.execute_sync(query) assert res.errors is None assert res.data == { "fruits": { "totalCount": 3, "results": [{"name": "Apple"}, {"name": "Banana"}, {"name": "Strawberry"}], } } res = schema.execute_sync(query, variable_values={"pagination": {"limit": 1}}) assert res.errors is None assert res.data == { "fruits": { "totalCount": 3, "results": [{"name": "Apple"}], } } res = schema.execute_sync( query, variable_values={"pagination": {"limit": 1, "offset": 1}} ) assert res.errors is None assert res.data == { "fruits": { "totalCount": 3, "results": [{"name": "Banana"}], } } @pytest.mark.django_db(transaction=True) async def test_pagination_query_async(): @strawberry_django.type(models.Fruit) class Fruit: id: int name: str @strawberry.type class Query: fruits: OffsetPaginated[Fruit] = strawberry_django.offset_paginated() await models.Fruit.objects.acreate(name="Apple") await models.Fruit.objects.acreate(name="Banana") await models.Fruit.objects.acreate(name="Strawberry") schema = strawberry.Schema(query=Query) query = """\ query GetFruits ($pagination: OffsetPaginationInput) { fruits (pagination: $pagination) { totalCount results { name } } } """ res = await schema.execute(query) assert res.errors is None assert res.data == { "fruits": { "totalCount": 3, "results": [{"name": "Apple"}, {"name": "Banana"}, {"name": "Strawberry"}], } } res = await schema.execute(query, variable_values={"pagination": {"limit": 1}}) assert res.errors is None assert res.data == { "fruits": { "totalCount": 3, "results": [{"name": "Apple"}], } } res = await schema.execute( query, variable_values={"pagination": {"limit": 1, "offset": 1}} ) assert res.errors is None assert res.data == { "fruits": { "totalCount": 3, "results": [{"name": "Banana"}], } } @strawberry_django.type(models.Fruit) class FruitLazyTest: id: int name: str @strawberry_django.type(models.Color) class ColorLazyTest: id: int name: str fruits: OffsetPaginated[ Annotated["FruitLazyTest", strawberry.lazy("tests.test_paginated_type")] ] = strawberry_django.offset_paginated() @pytest.mark.django_db(transaction=True) def test_pagination_with_lazy_type_and_django_query_optimizer(): @strawberry.type class Query: colors: OffsetPaginated[ColorLazyTest] = strawberry_django.offset_paginated() red = models.Color.objects.create(name="Red") yellow = models.Color.objects.create(name="Yellow") models.Fruit.objects.create(name="Apple", color=red) models.Fruit.objects.create(name="Banana", color=yellow) models.Fruit.objects.create(name="Strawberry", color=red) schema = strawberry.Schema(query=Query, extensions=[DjangoOptimizerExtension]) query = """\ query GetColors ($pagination: OffsetPaginationInput) { colors { totalCount results { fruits (pagination: $pagination) { totalCount results { name } } } } } """ res = schema.execute_sync(query) assert res.errors is None assert res.data == { "colors": { "totalCount": 2, "results": [ { "fruits": { "totalCount": 2, "results": [{"name": "Apple"}, {"name": "Strawberry"}], } }, { "fruits": { "totalCount": 1, "results": [{"name": "Banana"}], } }, ], } } @pytest.mark.django_db(transaction=True) def test_pagination_nested_query(): @strawberry_django.type(models.Fruit) class Fruit: id: int name: str @strawberry_django.type(models.Color) class Color: id: int name: str fruits: OffsetPaginated[Fruit] = strawberry_django.offset_paginated() @strawberry.type class Query: colors: OffsetPaginated[Color] = strawberry_django.offset_paginated() red = models.Color.objects.create(name="Red") yellow = models.Color.objects.create(name="Yellow") models.Fruit.objects.create(name="Apple", color=red) models.Fruit.objects.create(name="Banana", color=yellow) models.Fruit.objects.create(name="Strawberry", color=red) schema = strawberry.Schema(query=Query) query = """\ query GetColors ($pagination: OffsetPaginationInput) { colors { totalCount results { fruits (pagination: $pagination) { totalCount results { name } } } } } """ res = schema.execute_sync(query) assert res.errors is None assert res.data == { "colors": { "totalCount": 2, "results": [ { "fruits": { "totalCount": 2, "results": [{"name": "Apple"}, {"name": "Strawberry"}], } }, { "fruits": { "totalCount": 1, "results": [{"name": "Banana"}], } }, ], } } res = schema.execute_sync(query, variable_values={"pagination": {"limit": 1}}) assert res.errors is None assert res.data == { "colors": { "totalCount": 2, "results": [ { "fruits": { "totalCount": 2, "results": [{"name": "Apple"}], } }, { "fruits": { "totalCount": 1, "results": [{"name": "Banana"}], } }, ], } } res = schema.execute_sync( query, variable_values={"pagination": {"limit": 1, "offset": 1}} ) assert res.errors is None assert res.data == { "colors": { "totalCount": 2, "results": [ { "fruits": { "totalCount": 2, "results": [{"name": "Strawberry"}], } }, { "fruits": { "totalCount": 1, "results": [], } }, ], } } @pytest.mark.django_db(transaction=True) async def test_pagination_nested_query_async(): @strawberry_django.type(models.Fruit) class Fruit: id: int name: str @strawberry_django.type(models.Color) class Color: id: int name: str fruits: OffsetPaginated[Fruit] = strawberry_django.offset_paginated() @strawberry.type class Query: colors: OffsetPaginated[Color] = strawberry_django.offset_paginated() red = await models.Color.objects.acreate(name="Red") yellow = await models.Color.objects.acreate(name="Yellow") await models.Fruit.objects.acreate(name="Apple", color=red) await models.Fruit.objects.acreate(name="Banana", color=yellow) await models.Fruit.objects.acreate(name="Strawberry", color=red) schema = strawberry.Schema(query=Query) query = """\ query GetColors ($pagination: OffsetPaginationInput) { colors { totalCount results { fruits (pagination: $pagination) { totalCount results { name } } } } } """ res = await schema.execute(query) assert res.errors is None assert res.data == { "colors": { "totalCount": 2, "results": [ { "fruits": { "totalCount": 2, "results": [{"name": "Apple"}, {"name": "Strawberry"}], } }, { "fruits": { "totalCount": 1, "results": [{"name": "Banana"}], } }, ], } } res = await schema.execute(query, variable_values={"pagination": {"limit": 1}}) assert res.errors is None assert res.data == { "colors": { "totalCount": 2, "results": [ { "fruits": { "totalCount": 2, "results": [{"name": "Apple"}], } }, { "fruits": { "totalCount": 1, "results": [{"name": "Banana"}], } }, ], } } res = await schema.execute( query, variable_values={"pagination": {"limit": 1, "offset": 1}} ) assert res.errors is None assert res.data == { "colors": { "totalCount": 2, "results": [ { "fruits": { "totalCount": 2, "results": [{"name": "Strawberry"}], } }, { "fruits": { "totalCount": 1, "results": [], } }, ], } } @pytest.mark.django_db(transaction=True) def test_pagination_query_with_subclass(): @strawberry_django.type(models.Fruit) class Fruit: id: int name: str @strawberry.type class FruitPaginated(OffsetPaginated[Fruit]): _custom_field: strawberry.Private[str] @strawberry_django.field def custom_field(self) -> str: return self._custom_field @classmethod def resolve_paginated(cls, queryset, *, info, pagination=None, **kwargs): return cls( queryset=queryset, pagination=pagination or OffsetPaginationInput(), _custom_field="pagination rocks", ) @strawberry.type class Query: fruits: FruitPaginated = strawberry_django.offset_paginated() models.Fruit.objects.create(name="Apple") models.Fruit.objects.create(name="Banana") models.Fruit.objects.create(name="Strawberry") schema = strawberry.Schema(query=Query) query = """\ query GetFruits ($pagination: OffsetPaginationInput) { fruits (pagination: $pagination) { totalCount customField results { name } } } """ res = schema.execute_sync(query) assert res.errors is None assert res.data == { "fruits": { "totalCount": 3, "customField": "pagination rocks", "results": [{"name": "Apple"}, {"name": "Banana"}, {"name": "Strawberry"}], } } res = schema.execute_sync(query, variable_values={"pagination": {"limit": 1}}) assert res.errors is None assert res.data == { "fruits": { "totalCount": 3, "customField": "pagination rocks", "results": [{"name": "Apple"}], } } res = schema.execute_sync( query, variable_values={"pagination": {"limit": 1, "offset": 2}} ) assert res.errors is None assert res.data == { "fruits": { "totalCount": 3, "customField": "pagination rocks", "results": [{"name": "Strawberry"}], } } @pytest.mark.django_db(transaction=True) def test_pagination_query_with_resolver_schema(): @strawberry_django.type(models.Fruit) class Fruit: id: int name: str @strawberry_django.filter_type(models.Fruit) class FruitFilter: name: str @strawberry_django.order(models.Fruit) class FruitOrder: name: str @strawberry.type class Query: @strawberry_django.offset_paginated(OffsetPaginated[Fruit]) def fruits(self) -> QuerySet[models.Fruit]: ... @strawberry_django.offset_paginated( OffsetPaginated[Fruit], filters=FruitFilter, order=FruitOrder, ) def fruits_with_order_and_filter(self) -> QuerySet[models.Fruit]: ... schema = strawberry.Schema(query=Query) expected = ''' type Fruit { id: Int! name: String! } input FruitFilter { name: String! AND: FruitFilter OR: FruitFilter NOT: FruitFilter DISTINCT: Boolean } type FruitOffsetPaginated { pageInfo: OffsetPaginationInfo! """Total count of existing results.""" totalCount: Int! """List of paginated results.""" results: [Fruit!]! } input FruitOrder { name: String } type OffsetPaginationInfo { offset: Int! limit: Int } input OffsetPaginationInput { offset: Int! = 0 limit: Int } type Query { fruits(pagination: OffsetPaginationInput): FruitOffsetPaginated! fruitsWithOrderAndFilter(filters: FruitFilter, order: FruitOrder, pagination: OffsetPaginationInput): FruitOffsetPaginated! } ''' assert textwrap.dedent(str(schema)) == textwrap.dedent(expected).strip() @pytest.mark.django_db(transaction=True) def test_pagination_query_with_resolver(): @strawberry_django.type(models.Fruit) class Fruit: id: int name: str @strawberry_django.filter_type(models.Fruit) class FruitFilter: name: strawberry.auto @strawberry_django.order(models.Fruit) class FruitOrder: name: strawberry.auto @strawberry.type class Query: @strawberry_django.offset_paginated(OffsetPaginated[Fruit]) def fruits(self) -> QuerySet[models.Fruit]: return models.Fruit.objects.filter(name__startswith="S") @strawberry_django.offset_paginated( OffsetPaginated[Fruit], filters=FruitFilter, order=FruitOrder, ) def fruits_with_order_and_filter(self) -> QuerySet[models.Fruit]: return models.Fruit.objects.filter(name__startswith="S") models.Fruit.objects.create(name="Apple") models.Fruit.objects.create(name="Strawberry") models.Fruit.objects.create(name="Banana") models.Fruit.objects.create(name="Sugar Apple") models.Fruit.objects.create(name="Starfruit") schema = strawberry.Schema(query=Query) query = """\ query GetFruits ( $pagination: OffsetPaginationInput $filters: FruitFilter $order: FruitOrder ) { fruits (pagination: $pagination) { totalCount results { name } } fruitsWithOrderAndFilter ( pagination: $pagination filters: $filters order: $order ) { totalCount results { name } } } """ res = schema.execute_sync(query) assert res.errors is None assert res.data == { "fruits": { "totalCount": 3, "results": [ {"name": "Strawberry"}, {"name": "Sugar Apple"}, {"name": "Starfruit"}, ], }, "fruitsWithOrderAndFilter": { "totalCount": 3, "results": [ {"name": "Strawberry"}, {"name": "Sugar Apple"}, {"name": "Starfruit"}, ], }, } res = schema.execute_sync(query, variable_values={"pagination": {"limit": 1}}) assert res.errors is None assert res.data == { "fruits": { "totalCount": 3, "results": [ {"name": "Strawberry"}, ], }, "fruitsWithOrderAndFilter": { "totalCount": 3, "results": [ {"name": "Strawberry"}, ], }, } res = schema.execute_sync( query, variable_values={ "pagination": {"limit": 2}, "order": {"name": "ASC"}, "filters": {"name": "Strawberry"}, }, ) assert res.errors is None assert res.data == { "fruits": { "totalCount": 3, "results": [ {"name": "Strawberry"}, {"name": "Sugar Apple"}, ], }, "fruitsWithOrderAndFilter": { "totalCount": 1, "results": [ {"name": "Strawberry"}, ], }, } @pytest.mark.django_db(transaction=True) def test_pagination_query_with_resolver_arguments(): @strawberry_django.type(models.Fruit) class Fruit: id: int name: str @strawberry_django.filter_type(models.Fruit) class FruitFilter: name: strawberry.auto @strawberry_django.order(models.Fruit) class FruitOrder: name: strawberry.auto @strawberry.type class Query: @strawberry_django.offset_paginated(OffsetPaginated[Fruit]) def fruits(self, starts_with: str) -> QuerySet[models.Fruit]: return models.Fruit.objects.filter(name__startswith=starts_with) @strawberry_django.offset_paginated( OffsetPaginated[Fruit], filters=FruitFilter, order=FruitOrder, ) def fruits_with_order_and_filter( self, starts_with: str ) -> QuerySet[models.Fruit]: return models.Fruit.objects.filter(name__startswith=starts_with) models.Fruit.objects.create(name="Apple") models.Fruit.objects.create(name="Strawberry") models.Fruit.objects.create(name="Banana") models.Fruit.objects.create(name="Sugar Apple") models.Fruit.objects.create(name="Starfruit") schema = strawberry.Schema(query=Query) query = """\ query GetFruits ( $pagination: OffsetPaginationInput $filters: FruitFilter $order: FruitOrder $startsWith: String! ) { fruits (startsWith: $startsWith, pagination: $pagination) { totalCount results { name } } fruitsWithOrderAndFilter ( startsWith: $startsWith pagination: $pagination filters: $filters order: $order ) { totalCount results { name } } } """ res = schema.execute_sync(query, variable_values={"startsWith": "S"}) assert res.errors is None assert res.data == { "fruits": { "totalCount": 3, "results": [ {"name": "Strawberry"}, {"name": "Sugar Apple"}, {"name": "Starfruit"}, ], }, "fruitsWithOrderAndFilter": { "totalCount": 3, "results": [ {"name": "Strawberry"}, {"name": "Sugar Apple"}, {"name": "Starfruit"}, ], }, } res = schema.execute_sync( query, variable_values={"startsWith": "S", "pagination": {"limit": 1}}, ) assert res.errors is None assert res.data == { "fruits": { "totalCount": 3, "results": [ {"name": "Strawberry"}, ], }, "fruitsWithOrderAndFilter": { "totalCount": 3, "results": [ {"name": "Strawberry"}, ], }, } res = schema.execute_sync( query, variable_values={ "startsWith": "S", "pagination": {"limit": 2}, "order": {"name": "ASC"}, "filters": {"name": "Strawberry"}, }, ) assert res.errors is None assert res.data == { "fruits": { "totalCount": 3, "results": [ {"name": "Strawberry"}, {"name": "Sugar Apple"}, ], }, "fruitsWithOrderAndFilter": { "totalCount": 1, "results": [ {"name": "Strawberry"}, ], }, } @pytest.mark.django_db(transaction=True) @override_settings( STRAWBERRY_DJANGO=StrawberryDjangoSettings( # type: ignore PAGINATION_DEFAULT_LIMIT=2, ), ) def test_pagination_default_limit(): @strawberry_django.type(models.Fruit) class Fruit: id: int name: str @strawberry.type class Query: fruits: OffsetPaginated[Fruit] = strawberry_django.offset_paginated() models.Fruit.objects.create(name="Apple") models.Fruit.objects.create(name="Banana") models.Fruit.objects.create(name="Strawberry") models.Fruit.objects.create(name="Watermelon") schema = strawberry.Schema(query=Query) query = """\ query GetFruits ($pagination: OffsetPaginationInput) { fruits (pagination: $pagination) { totalCount results { name } } } """ res = schema.execute_sync(query) assert res.errors is None assert res.data == { "fruits": { "totalCount": 4, "results": [{"name": "Apple"}, {"name": "Banana"}], } } res = schema.execute_sync(query, variable_values={"pagination": {"offset": 1}}) assert res.errors is None assert res.data == { "fruits": { "totalCount": 4, "results": [{"name": "Banana"}, {"name": "Strawberry"}], } } # Setting limit to None should return all results res = schema.execute_sync(query, variable_values={"pagination": {"limit": None}}) assert res.errors is None assert res.data == { "fruits": { "totalCount": 4, "results": [ {"name": "Apple"}, {"name": "Banana"}, {"name": "Strawberry"}, {"name": "Watermelon"}, ], } } strawberry-graphql-django-0.62.0/tests/test_pagination.py000066400000000000000000000143261502405145400236110ustar00rootroot00000000000000import sys from typing import cast import pytest import strawberry from strawberry import auto from strawberry.types import ExecutionResult import strawberry_django from strawberry_django.optimizer import DjangoOptimizerExtension from strawberry_django.pagination import ( OffsetPaginationInput, apply, apply_window_pagination, ) from tests import models, utils from tests.projects.faker import ( IssueFactory, MilestoneFactory, ProjectFactory, TagFactory, ) @strawberry_django.type(models.Fruit, pagination=True) class Fruit: id: auto name: auto @strawberry_django.type(models.Fruit, pagination=True) class BerryFruit: name: auto @classmethod def get_queryset(cls, queryset, info, **kwargs): return queryset.filter(name__contains="berry") @strawberry.type class Query: fruits: list[Fruit] = strawberry_django.field() berries: list[BerryFruit] = strawberry_django.field() @pytest.fixture def query(): return utils.generate_query(Query) def test_pagination(query, fruits): result = query("{ fruits(pagination: { offset: 1, limit:1 }) { name } }") assert not result.errors assert result.data["fruits"] == [ {"name": "raspberry"}, ] def test_pagination_of_filtered_query(query, fruits): result = query("{ berries(pagination: { offset: 1, limit:1 }) { name } }") assert not result.errors assert result.data["berries"] == [ {"name": "raspberry"}, ] @pytest.mark.django_db(transaction=True) def test_nested_pagination(fruits, gql_client: utils.GraphQLTestClient): # Test nested pagination with optimizer enabled # Test query color and nested fruits, paginating the nested fruits # Enable optimizer query = """ query testNestedPagination { projectList { milestones(pagination: { limit: 1 }) { name } } } """ p = ProjectFactory.create() MilestoneFactory.create_batch(2, project=p) result = gql_client.query(query) assert not result.errors assert isinstance(result.data, dict) project_list = result.data["projectList"] assert isinstance(project_list, list) assert len(project_list) == 1 assert len(project_list[0]["milestones"]) == 1 def test_resolver_pagination(fruits): @strawberry.type class Query: @strawberry.field def fruits(self, pagination: OffsetPaginationInput) -> list[Fruit]: queryset = models.Fruit.objects.all() return cast("list[Fruit]", apply(pagination, queryset)) query = utils.generate_query(Query) result = query("{ fruits(pagination: { limit: 1 }) { id name } }") assert isinstance(result, ExecutionResult) assert not result.errors assert result.data is not None assert result.data["fruits"] == [ {"id": "1", "name": "strawberry"}, ] @pytest.mark.django_db(transaction=True) def test_apply_window_pagination(): color = models.Color.objects.create(name="Red") for i in range(10): models.Fruit.objects.create(name=f"fruit{i}", color=color) queryset = apply_window_pagination( models.Fruit.objects.all(), related_field_id="color_id", offset=1, limit=1, ) assert queryset.count() == 1 fruit = queryset.get() assert fruit.name == "fruit1" assert fruit._strawberry_row_number == 2 # type: ignore assert fruit._strawberry_total_count == 10 # type: ignore @pytest.mark.parametrize("limit", [-1, sys.maxsize]) @pytest.mark.django_db(transaction=True) def test_apply_window_pagination_with_no_limites(limit): color = models.Color.objects.create(name="Red") for i in range(10): models.Fruit.objects.create(name=f"fruit{i}", color=color) queryset = apply_window_pagination( models.Fruit.objects.all(), related_field_id="color_id", offset=2, limit=limit, ) assert queryset.count() == 8 first_fruit = queryset.first() assert first_fruit is not None assert first_fruit.name == "fruit2" assert first_fruit._strawberry_row_number == 3 # type: ignore assert first_fruit._strawberry_total_count == 10 # type: ignore @pytest.mark.django_db(transaction=True) def test_nested_pagination_m2m(gql_client: utils.GraphQLTestClient): # Create 2 tags and 3 issues tags = [TagFactory(name=f"Tag {i + 1}") for i in range(2)] issues = [IssueFactory(name=f"Issue {i + 1}") for i in range(3)] # Assign issues 1 and 2 to the 1st tag # Assign issues 2 and 3 to the 2nd tag # This means that both tags share the 2nd issue tags[0].issues.set(issues[:2]) tags[1].issues.set(issues[1:]) with utils.assert_num_queries(3 if DjangoOptimizerExtension.enabled.get() else 6): result = gql_client.query( """ query { tagConn { totalCount edges { node { name issues { totalCount edges { node { name } } } } } } } """ ) # Check the results assert not result.errors assert result.data == { "tagConn": { "totalCount": 2, "edges": [ { "node": { "name": "Tag 1", "issues": { "totalCount": 2, "edges": [ {"node": {"name": "Issue 1"}}, {"node": {"name": "Issue 2"}}, ], }, } }, { "node": { "name": "Tag 2", "issues": { "totalCount": 2, "edges": [ {"node": {"name": "Issue 2"}}, {"node": {"name": "Issue 3"}}, ], }, } }, ], } } strawberry-graphql-django-0.62.0/tests/test_permissions.py000066400000000000000000001032511502405145400240270ustar00rootroot00000000000000import pytest from django.contrib.auth.models import Permission from guardian.shortcuts import assign_perm from strawberry.relay import to_base64 from typing_extensions import Literal, TypeAlias from strawberry_django.optimizer import DjangoOptimizerExtension from .projects.faker import ( GroupFactory, IssueFactory, MilestoneFactory, StaffUserFactory, SuperuserUserFactory, UserFactory, ) from .utils import GraphQLTestClient, assert_num_queries PermKind: TypeAlias = Literal["user", "group", "superuser"] perm_kinds: list[PermKind] = ["user", "group", "superuser"] @pytest.mark.django_db(transaction=True) def test_is_authenticated(db, gql_client: GraphQLTestClient): query = """ query Issue ($id: ID!) { issueLoginRequired (id: $id) { id name } } """ issue = IssueFactory.create() res = gql_client.query( query, {"id": to_base64("IssueType", issue.pk)}, assert_no_errors=False, ) assert res.data is None assert res.errors == [ { "message": "User is not authenticated.", "locations": [{"line": 3, "column": 9}], "path": ["issueLoginRequired"], }, ] user = UserFactory.create() with gql_client.login(user): res = gql_client.query(query, {"id": to_base64("IssueType", issue.pk)}) assert res.data == { "issueLoginRequired": { "id": to_base64("IssueType", issue.pk), "name": issue.name, }, } @pytest.mark.django_db(transaction=True) def test_is_authenticated_optional(db, gql_client: GraphQLTestClient): query = """ query Issue ($id: ID!) { issueLoginRequiredOptional (id: $id) { id name } } """ issue = IssueFactory.create() res = gql_client.query(query, {"id": to_base64("IssueType", issue.pk)}) assert res.data == {"issueLoginRequiredOptional": None} user = UserFactory.create() with gql_client.login(user): res = gql_client.query(query, {"id": to_base64("IssueType", issue.pk)}) assert res.data == { "issueLoginRequiredOptional": { "id": to_base64("IssueType", issue.pk), "name": issue.name, }, } @pytest.mark.django_db(transaction=True) def test_staff_required(db, gql_client: GraphQLTestClient): query = """ query Issue ($id: ID!) { issueStaffRequired (id: $id) { id name } } """ issue = IssueFactory.create() res = gql_client.query( query, {"id": to_base64("IssueType", issue.pk)}, assert_no_errors=False, ) assert res.data is None assert res.errors == [ { "message": "User is not a staff member.", "locations": [{"line": 3, "column": 9}], "path": ["issueStaffRequired"], }, ] user = UserFactory.create() with gql_client.login(user): assert res.data is None assert res.errors == [ { "message": "User is not a staff member.", "locations": [{"line": 3, "column": 9}], "path": ["issueStaffRequired"], }, ] staff = StaffUserFactory.create() with gql_client.login(staff): res = gql_client.query(query, {"id": to_base64("IssueType", issue.pk)}) assert res.data == { "issueStaffRequired": { "id": to_base64("IssueType", issue.pk), "name": issue.name, }, } @pytest.mark.django_db(transaction=True) def test_staff_required_optional(db, gql_client: GraphQLTestClient): query = """ query Issue ($id: ID!) { issueStaffRequiredOptional (id: $id) { id name } } """ issue = IssueFactory.create() res = gql_client.query(query, {"id": to_base64("IssueType", issue.pk)}) assert res.data == {"issueStaffRequiredOptional": None} user = UserFactory.create() with gql_client.login(user): res = gql_client.query(query, {"id": to_base64("IssueType", issue.pk)}) assert res.data == {"issueStaffRequiredOptional": None} staff = StaffUserFactory.create() with gql_client.login(staff): res = gql_client.query(query, {"id": to_base64("IssueType", issue.pk)}) assert res.data == { "issueStaffRequiredOptional": { "id": to_base64("IssueType", issue.pk), "name": issue.name, }, } @pytest.mark.django_db(transaction=True) def test_superuser_required(db, gql_client: GraphQLTestClient): query = """ query Issue ($id: ID!) { issueSuperuserRequired (id: $id) { id name } } """ issue = IssueFactory.create() res = gql_client.query( query, {"id": to_base64("IssueType", issue.pk)}, assert_no_errors=False, ) assert res.data is None assert res.errors == [ { "message": "User is not a superuser.", "locations": [{"line": 3, "column": 9}], "path": ["issueSuperuserRequired"], }, ] user = UserFactory.create() with gql_client.login(user): res = gql_client.query( query, {"id": to_base64("IssueType", issue.pk)}, assert_no_errors=False, ) assert res.data is None assert res.errors == [ { "message": "User is not a superuser.", "locations": [{"line": 3, "column": 9}], "path": ["issueSuperuserRequired"], }, ] superuser = SuperuserUserFactory.create() with gql_client.login(superuser): res = gql_client.query(query, {"id": to_base64("IssueType", issue.pk)}) assert res.data == { "issueSuperuserRequired": { "id": to_base64("IssueType", issue.pk), "name": issue.name, }, } @pytest.mark.django_db(transaction=True) def test_async_user_resolve(db, gql_client: GraphQLTestClient): query = """ query asyncUserResolve { asyncUserResolve } """ if not gql_client.is_async: pytest.skip("needs async client") user = UserFactory.create() with gql_client.login(user): res = gql_client.query( query, assert_no_errors=False, ) assert res.data is None assert res.errors == [ { "message": "You don't have permission to access this app.", "locations": [{"line": 3, "column": 9}], "path": ["asyncUserResolve"], }, ] superuser = SuperuserUserFactory.create() with gql_client.login(superuser): res = gql_client.query(query) assert res.data == {"asyncUserResolve": True} user_with_perm = UserFactory.create() user_with_perm.user_permissions.add( Permission.objects.get(codename="view_issue"), ) with gql_client.login(user_with_perm): res = gql_client.query(query) assert res.data == {"asyncUserResolve": True} @pytest.mark.django_db(transaction=True) def test_superuser_required_optional(db, gql_client: GraphQLTestClient): query = """ query Issue ($id: ID!) { issueSuperuserRequiredOptional (id: $id) { id name } } """ issue = IssueFactory.create() res = gql_client.query(query, {"id": to_base64("IssueType", issue.pk)}) assert res.data == {"issueSuperuserRequiredOptional": None} user = UserFactory.create() with gql_client.login(user): res = gql_client.query(query, {"id": to_base64("IssueType", issue.pk)}) assert res.data == {"issueSuperuserRequiredOptional": None} superuser = SuperuserUserFactory.create() with gql_client.login(superuser): res = gql_client.query(query, {"id": to_base64("IssueType", issue.pk)}) assert res.data == { "issueSuperuserRequiredOptional": { "id": to_base64("IssueType", issue.pk), "name": issue.name, }, } @pytest.mark.django_db(transaction=True) def test_perm_cached(db, gql_client: GraphQLTestClient): """Validates that the permission caching mechanism correctly stores permissions as a set of strings. The test targets the `_perm_cache` attribute used in `utils/query.py`. It verifies that the attribute behaves as expected, holding a `Set[str]` that represents permission codenames, rather than direct Permission objects. This test addresses a regression captured by the following error: ``` user_perms: Set[str] = {p.codename for p in perm_cache} ^^^^^^^^^^ AttributeError: 'str' object has no attribute 'codename' ``` """ query = """ query Issue ($id: ID!) { issuePermRequired (id: $id) { id privateName } } """ issue = IssueFactory.create(name="Test") # User with permission user_with_perm = UserFactory.create() user_with_perm.user_permissions.add( Permission.objects.get(codename="view_issue"), ) assign_perm("view_issue", user_with_perm, issue) with gql_client.login(user_with_perm): if DjangoOptimizerExtension.enabled.get(): res = gql_client.query(query, {"id": to_base64("IssueType", issue.pk)}) assert res.data == { "issuePermRequired": { "id": to_base64("IssueType", issue.pk), "privateName": issue.name, }, } @pytest.mark.django_db(transaction=True) @pytest.mark.parametrize("kind", perm_kinds) def test_perm_required(db, gql_client: GraphQLTestClient, kind: PermKind): query = """ query Issue ($id: ID!) { issuePermRequired (id: $id) { id name } } """ issue = IssueFactory.create() res = gql_client.query( query, {"id": to_base64("IssueType", issue.pk)}, assert_no_errors=False, ) assert res.data is None assert res.errors == [ { "message": "You don't have permission to access this app.", "locations": [{"line": 3, "column": 9}], "path": ["issuePermRequired"], }, ] user = UserFactory.create() with gql_client.login(user): res = gql_client.query( query, {"id": to_base64("IssueType", issue.pk)}, assert_no_errors=False, ) assert res.data is None assert res.errors == [ { "message": "You don't have permission to access this app.", "locations": [{"line": 3, "column": 9}], "path": ["issuePermRequired"], }, ] if kind == "user": user_with_perm = UserFactory.create() user_with_perm.user_permissions.add( Permission.objects.get(codename="view_issue"), ) elif kind == "group": user_with_perm = UserFactory.create() group = GroupFactory.create() group.permissions.add(Permission.objects.get(codename="view_issue")) user_with_perm.groups.add(group) elif kind == "superuser": user_with_perm = SuperuserUserFactory.create() else: # pragma:nocover raise AssertionError with gql_client.login(user_with_perm): res = gql_client.query(query, {"id": to_base64("IssueType", issue.pk)}) assert res.data == { "issuePermRequired": { "id": to_base64("IssueType", issue.pk), "name": issue.name, }, } @pytest.mark.django_db(transaction=True) @pytest.mark.parametrize("kind", perm_kinds) def test_perm_required_optional(db, gql_client: GraphQLTestClient, kind: PermKind): query = """ query Issue ($id: ID!) { issuePermRequiredOptional (id: $id) { id name } } """ issue = IssueFactory.create() res = gql_client.query(query, {"id": to_base64("IssueType", issue.pk)}) assert res.data == {"issuePermRequiredOptional": None} user = UserFactory.create() with gql_client.login(user): res = gql_client.query(query, {"id": to_base64("IssueType", issue.pk)}) assert res.data == {"issuePermRequiredOptional": None} if kind == "user": user_with_perm = UserFactory.create() user_with_perm.user_permissions.add( Permission.objects.get(codename="view_issue"), ) elif kind == "group": user_with_perm = UserFactory.create() group = GroupFactory.create() group.permissions.add(Permission.objects.get(codename="view_issue")) user_with_perm.groups.add(group) elif kind == "superuser": user_with_perm = SuperuserUserFactory.create() else: # pragma:nocover raise AssertionError with gql_client.login(user_with_perm): res = gql_client.query(query, {"id": to_base64("IssueType", issue.pk)}) assert res.data == { "issuePermRequiredOptional": { "id": to_base64("IssueType", issue.pk), "name": issue.name, }, } @pytest.mark.django_db(transaction=True) @pytest.mark.parametrize("kind", perm_kinds) def test_list_perm_required(db, gql_client: GraphQLTestClient, kind: PermKind): query = """ query Issue { issueListPermRequired { id name } } """ issue = IssueFactory.create() res = gql_client.query(query) assert res.data == {"issueListPermRequired": []} user = UserFactory.create() with gql_client.login(user): res = gql_client.query(query) assert res.data == {"issueListPermRequired": []} if kind == "user": user_with_perm = UserFactory.create() user_with_perm.user_permissions.add( Permission.objects.get(codename="view_issue"), ) elif kind == "group": user_with_perm = UserFactory.create() group = GroupFactory.create() group.permissions.add(Permission.objects.get(codename="view_issue")) user_with_perm.groups.add(group) elif kind == "superuser": user_with_perm = SuperuserUserFactory.create() else: # pragma:nocover raise AssertionError with gql_client.login(user_with_perm): res = gql_client.query(query) assert res.data == { "issueListPermRequired": [ { "id": to_base64("IssueType", issue.pk), "name": issue.name, }, ], } @pytest.mark.django_db(transaction=True) @pytest.mark.parametrize("kind", perm_kinds) def test_conn_perm_required(db, gql_client: GraphQLTestClient, kind: PermKind): query = """ query Issue { issueConnPermRequired { totalCount edges { node { id name } } } } """ issue = IssueFactory.create() res = gql_client.query(query) assert res.data == {"issueConnPermRequired": {"edges": [], "totalCount": 0}} user = UserFactory.create() with gql_client.login(user): res = gql_client.query(query) assert res.data == {"issueConnPermRequired": {"edges": [], "totalCount": 0}} if kind == "user": user_with_perm = UserFactory.create() user_with_perm.user_permissions.add( Permission.objects.get(codename="view_issue"), ) elif kind == "group": user_with_perm = UserFactory.create() group = GroupFactory.create() group.permissions.add(Permission.objects.get(codename="view_issue")) user_with_perm.groups.add(group) elif kind == "superuser": user_with_perm = SuperuserUserFactory.create() else: # pragma:nocover raise AssertionError with gql_client.login(user_with_perm): res = gql_client.query(query) assert res.data == { "issueConnPermRequired": { "edges": [ { "node": { "id": to_base64("IssueType", issue.pk), "name": issue.name, }, }, ], "totalCount": 1, }, } @pytest.mark.django_db(transaction=True) @pytest.mark.parametrize("kind", perm_kinds) def test_obj_perm_required(db, gql_client: GraphQLTestClient, kind: PermKind): query = """ query Issue ($id: ID!) { issueObjPermRequired (id: $id) { id name } } """ issue_no_perm = IssueFactory.create() issue_with_perm = IssueFactory.create() user = UserFactory.create() if kind == "user": user_with_perm = UserFactory.create() assign_perm("view_issue", user_with_perm, issue_with_perm) elif kind == "group": user_with_perm = UserFactory.create() group = GroupFactory.create() assign_perm("view_issue", group, issue_with_perm) user_with_perm.groups.add(group) elif kind == "superuser": user_with_perm = SuperuserUserFactory.create() else: # pragma:nocover raise AssertionError for issue in [issue_no_perm, issue_with_perm]: res = gql_client.query( query, {"id": to_base64("IssueType", issue.pk)}, assert_no_errors=False, ) assert res.data is None assert res.errors == [ { "message": "You don't have permission to access this app.", "locations": [{"line": 3, "column": 9}], "path": ["issueObjPermRequired"], }, ] for u in [user, user_with_perm]: # Superusers will have access to everything... if kind == "superuser": continue with gql_client.login(u): res = gql_client.query( query, {"id": to_base64("IssueType", issue_no_perm.pk)}, assert_no_errors=False, ) assert res.data is None assert res.errors == [ { "message": "You don't have permission to access this app.", "locations": [{"line": 3, "column": 9}], "path": ["issueObjPermRequired"], }, ] with gql_client.login(user_with_perm): res = gql_client.query( query, {"id": to_base64("IssueType", issue_with_perm.pk)}, ) assert res.data == { "issueObjPermRequired": { "id": to_base64("IssueType", issue_with_perm.pk), "name": issue_with_perm.name, }, } @pytest.mark.django_db(transaction=True) @pytest.mark.parametrize("kind", perm_kinds) def test_obj_perm_required_global(db, gql_client: GraphQLTestClient, kind: PermKind): query = """ query Issue ($id: ID!) { issueObjPermRequired (id: $id) { id name } } """ issue_no_perm = IssueFactory.create() issue_with_perm = IssueFactory.create() user = UserFactory.create() if kind == "user": user_with_perm = UserFactory.create() user_with_perm.user_permissions.add( Permission.objects.get(codename="view_issue"), ) elif kind == "group": user_with_perm = UserFactory.create() group = GroupFactory.create() group.permissions.add(Permission.objects.get(codename="view_issue")) user_with_perm.groups.add(group) elif kind == "superuser": user_with_perm = SuperuserUserFactory.create() else: # pragma:nocover raise AssertionError for issue in [issue_no_perm, issue_with_perm]: res = gql_client.query( query, {"id": to_base64("IssueType", issue.pk)}, assert_no_errors=False, ) assert res.data is None assert res.errors == [ { "message": "You don't have permission to access this app.", "locations": [{"line": 3, "column": 9}], "path": ["issueObjPermRequired"], }, ] with gql_client.login(user): res = gql_client.query( query, {"id": to_base64("IssueType", issue.pk)}, assert_no_errors=False, ) assert res.data is None assert res.errors == [ { "message": "You don't have permission to access this app.", "locations": [{"line": 3, "column": 9}], "path": ["issueObjPermRequired"], }, ] with gql_client.login(user_with_perm): res = gql_client.query( query, {"id": to_base64("IssueType", issue_with_perm.pk)}, ) assert res.data == { "issueObjPermRequired": { "id": to_base64("IssueType", issue_with_perm.pk), "name": issue_with_perm.name, }, } @pytest.mark.django_db(transaction=True) @pytest.mark.parametrize("kind", perm_kinds) def test_obj_perm_required_optional(db, gql_client: GraphQLTestClient, kind: PermKind): query = """ query Issue ($id: ID!) { issueObjPermRequiredOptional (id: $id) { id name } } """ issue_no_perm = IssueFactory.create() issue_with_perm = IssueFactory.create() user = UserFactory.create() if kind == "user": user_with_perm = UserFactory.create() assign_perm("view_issue", user_with_perm, issue_with_perm) elif kind == "group": user_with_perm = UserFactory.create() group = GroupFactory.create() assign_perm("view_issue", group, issue_with_perm) user_with_perm.groups.add(group) elif kind == "superuser": user_with_perm = SuperuserUserFactory.create() else: # pragma:nocover raise AssertionError for issue in [issue_no_perm, issue_with_perm]: res = gql_client.query( query, {"id": to_base64("IssueType", issue.pk)}, ) assert res.data == {"issueObjPermRequiredOptional": None} for u in [user, user_with_perm]: # Superusers will have access to everything... if kind == "superuser": continue with gql_client.login(u): res = gql_client.query( query, {"id": to_base64("IssueType", issue_no_perm.pk)}, ) assert res.data == {"issueObjPermRequiredOptional": None} with gql_client.login(user_with_perm): res = gql_client.query( query, {"id": to_base64("IssueType", issue_with_perm.pk)}, ) assert res.data == { "issueObjPermRequiredOptional": { "id": to_base64("IssueType", issue_with_perm.pk), "name": issue_with_perm.name, }, } @pytest.mark.django_db(transaction=True) @pytest.mark.parametrize("kind", perm_kinds) def test_list_obj_perm_required(db, gql_client: GraphQLTestClient, kind: PermKind): query = """ query Issue { issueListObjPermRequired { id name } } """ IssueFactory.create() issue_with_perm = IssueFactory.create() user = UserFactory.create() if kind == "user": user_with_perm = UserFactory.create() assign_perm("view_issue", user_with_perm, issue_with_perm) elif kind == "group": user_with_perm = UserFactory.create() group = GroupFactory.create() assign_perm("view_issue", group, issue_with_perm) user_with_perm.groups.add(group) elif kind == "superuser": user_with_perm = SuperuserUserFactory.create() else: # pragma:nocover raise AssertionError res = gql_client.query(query) assert res.data == {"issueListObjPermRequired": []} with gql_client.login(user): res = gql_client.query(query) assert res.data == {"issueListObjPermRequired": []} if kind == "superuser": # Even though the user is a superuser, he doesn't have the permission # assigned directly to him for the listing. with gql_client.login(user_with_perm): res = gql_client.query(query) assert res.data == {"issueListObjPermRequired": []} else: with gql_client.login(user_with_perm): res = gql_client.query(query) assert res.data == { "issueListObjPermRequired": [ { "id": to_base64("IssueType", issue_with_perm.pk), "name": issue_with_perm.name, }, ], } @pytest.mark.django_db(transaction=True) @pytest.mark.parametrize("kind", perm_kinds) def test_list_obj_perm_required_paginated( db, gql_client: GraphQLTestClient, kind: PermKind ): query = """ query Issue { issueListObjPermRequiredPaginated(pagination: {limit: 10, offset: 0}) { id name } } """ IssueFactory.create() issue_with_perm = IssueFactory.create() user = UserFactory.create() if kind == "user": user_with_perm = UserFactory.create() assign_perm("view_issue", user_with_perm, issue_with_perm) elif kind == "group": user_with_perm = UserFactory.create() group = GroupFactory.create() assign_perm("view_issue", group, issue_with_perm) user_with_perm.groups.add(group) elif kind == "superuser": user_with_perm = SuperuserUserFactory.create() else: # pragma:nocover raise AssertionError res = gql_client.query(query) assert res.data == {"issueListObjPermRequiredPaginated": []} with gql_client.login(user): res = gql_client.query(query) assert res.data == {"issueListObjPermRequiredPaginated": []} if kind == "superuser": # Even though the user is a superuser, he doesn't have the permission # assigned directly to him for the listing. with gql_client.login(user_with_perm): res = gql_client.query(query) assert res.data == {"issueListObjPermRequiredPaginated": []} else: with gql_client.login(user_with_perm): res = gql_client.query(query) assert res.data == { "issueListObjPermRequiredPaginated": [ { "id": to_base64("IssueType", issue_with_perm.pk), "name": issue_with_perm.name, }, ], } @pytest.mark.django_db(transaction=True) @pytest.mark.parametrize("kind", perm_kinds) def test_conn_obj_perm_required(db, gql_client: GraphQLTestClient, kind: PermKind): query = """ query Issue { issueConnObjPermRequired { totalCount edges { node { id name } } } } """ IssueFactory.create() issue_with_perm = IssueFactory.create() user = UserFactory.create() if kind == "user": user_with_perm = UserFactory.create() assign_perm("view_issue", user_with_perm, issue_with_perm) elif kind == "group": user_with_perm = UserFactory.create() group = GroupFactory.create() assign_perm("view_issue", group, issue_with_perm) user_with_perm.groups.add(group) elif kind == "superuser": user_with_perm = SuperuserUserFactory.create() else: # pragma:nocover raise AssertionError res = gql_client.query(query) assert res.data == {"issueConnObjPermRequired": {"edges": [], "totalCount": 0}} with gql_client.login(user): res = gql_client.query(query) assert res.data == {"issueConnObjPermRequired": {"edges": [], "totalCount": 0}} if kind == "superuser": # Even though the user is a superuser, he doesn't have the permission # assigned directly to him for the listing. with gql_client.login(user_with_perm): res = gql_client.query(query) assert res.data == { "issueConnObjPermRequired": {"edges": [], "totalCount": 0}, } else: with gql_client.login(user_with_perm): res = gql_client.query(query) assert res.data == { "issueConnObjPermRequired": { "edges": [ { "node": { "id": to_base64("IssueType", issue_with_perm.pk), "name": issue_with_perm.name, }, }, ], "totalCount": 1, }, } @pytest.mark.django_db(transaction=True) def test_query_paginated_with_permissions(db, gql_client: GraphQLTestClient): query = """ query TestQuery ($pagination: OffsetPaginationInput) { issuesPaginatedPermRequired (pagination: $pagination) { totalCount results { name milestone { name } } } } """ milestone1 = MilestoneFactory.create() milestone2 = MilestoneFactory.create() issue1 = IssueFactory.create(milestone=milestone1) issue2 = IssueFactory.create(milestone=milestone1) issue3 = IssueFactory.create(milestone=milestone1) issue4 = IssueFactory.create(milestone=milestone2) issue5 = IssueFactory.create(milestone=milestone2) # No user logged in with assert_num_queries(0): res = gql_client.query(query) assert res.data == { "issuesPaginatedPermRequired": { "totalCount": 0, "results": [], } } user = UserFactory.create() # User logged in without permissions with gql_client.login(user): with assert_num_queries(4): res = gql_client.query(query) assert res.data == { "issuesPaginatedPermRequired": { "totalCount": 0, "results": [], } } # User logged in with permissions user.user_permissions.add(Permission.objects.get(codename="view_issue")) with gql_client.login(user): with assert_num_queries(6 if DjangoOptimizerExtension.enabled.get() else 11): res = gql_client.query(query) assert res.data == { "issuesPaginatedPermRequired": { "totalCount": 5, "results": [ {"name": issue1.name, "milestone": {"name": milestone1.name}}, {"name": issue2.name, "milestone": {"name": milestone1.name}}, {"name": issue3.name, "milestone": {"name": milestone1.name}}, {"name": issue4.name, "milestone": {"name": milestone2.name}}, {"name": issue5.name, "milestone": {"name": milestone2.name}}, ], } } with assert_num_queries(6 if DjangoOptimizerExtension.enabled.get() else 8): res = gql_client.query(query, variables={"pagination": {"limit": 2}}) assert res.data == { "issuesPaginatedPermRequired": { "totalCount": 5, "results": [ {"name": issue1.name, "milestone": {"name": milestone1.name}}, {"name": issue2.name, "milestone": {"name": milestone1.name}}, ], } } @pytest.mark.django_db(transaction=True) def test_query_paginated_with_obj_permissions(db, gql_client: GraphQLTestClient): query = """ query TestQuery ($pagination: OffsetPaginationInput) { issuesPaginatedObjPermRequired (pagination: $pagination) { totalCount results { name milestone { name } } } } """ milestone1 = MilestoneFactory.create() milestone2 = MilestoneFactory.create() IssueFactory.create(milestone=milestone1) issue2 = IssueFactory.create(milestone=milestone1) IssueFactory.create(milestone=milestone1) issue4 = IssueFactory.create(milestone=milestone2) IssueFactory.create(milestone=milestone2) # No user logged in with assert_num_queries(0): res = gql_client.query(query) assert res.data == { "issuesPaginatedObjPermRequired": { "totalCount": 0, "results": [], } } user = UserFactory.create() # User logged in without permissions with gql_client.login(user): with assert_num_queries(5): res = gql_client.query(query) assert res.data == { "issuesPaginatedObjPermRequired": { "totalCount": 0, "results": [], } } assign_perm("view_issue", user, issue2) assign_perm("view_issue", user, issue4) # User logged in with permissions with gql_client.login(user): with assert_num_queries(4 if DjangoOptimizerExtension.enabled.get() else 6): res = gql_client.query(query) assert res.data == { "issuesPaginatedObjPermRequired": { "totalCount": 2, "results": [ {"name": issue2.name, "milestone": {"name": milestone1.name}}, {"name": issue4.name, "milestone": {"name": milestone2.name}}, ], } } with assert_num_queries(4 if DjangoOptimizerExtension.enabled.get() else 5): res = gql_client.query(query, variables={"pagination": {"limit": 1}}) assert res.data == { "issuesPaginatedObjPermRequired": { "totalCount": 2, "results": [ {"name": issue2.name, "milestone": {"name": milestone1.name}}, ], } } strawberry-graphql-django-0.62.0/tests/test_pyutils.py000066400000000000000000000044311502405145400231650ustar00rootroot00000000000000from strawberry_django.utils.pyutils import ( dicttree_insersection_differs, dicttree_merge, ) def test_dicctree_merge(): assert dicttree_merge( { "foo": 1, "bar": 2, "baz": 7, "sub1": { "a": "asub1", "b": "bsub1", "c": "csub1", }, "sub2": { "a": "asub2", "b": "bsub2", "c": "csub2", }, }, { "bar": 3, "bin": 4, "sub1": { "a": "force_asub1", "d": "force_dsub1", }, "sub3": { "a": "asub3", "b": "bsub3", "c": "csub3", }, }, ) == { "foo": 1, "bar": 3, "baz": 7, "bin": 4, "sub1": { "a": "force_asub1", "b": "bsub1", "c": "csub1", "d": "force_dsub1", }, "sub2": { "a": "asub2", "b": "bsub2", "c": "csub2", }, "sub3": { "a": "asub3", "b": "bsub3", "c": "csub3", }, } def test_dicctree_intersection_differs(): assert not dicttree_insersection_differs({"a": 1}, {"b": 1}) assert not dicttree_insersection_differs({"a": 1}, {"b": 2}) assert not dicttree_insersection_differs({"a": 1}, {"a": 1}) assert not dicttree_insersection_differs({"a": 1}, {"a": 1, "b": 1}) assert not dicttree_insersection_differs( {"a": 1, "c": {"foobar": 3}}, {"a": 1, "b": 1}, ) assert not dicttree_insersection_differs( {"a": 1, "c": {"foobar": 1}}, {"a": 1, "b": 1, "c": {"yyy": "abc"}}, ) assert not dicttree_insersection_differs( {"a": 1, "c": {"foobar": 1}}, {"a": 1, "b": 1, "c": {"foobar": 1}}, ) assert dicttree_insersection_differs({"a": 1}, {"a": 2}) assert dicttree_insersection_differs({"a": 1}, {"a": 2, "b": 1}) assert dicttree_insersection_differs( {"a": 1, "c": {"foobar": 1}}, {"a": 2, "c": {"foobar": 1}}, ) assert dicttree_insersection_differs( {"a": 1, "c": {"foobar": 1}}, {"a": 1, "c": {"foobar": 2}}, ) strawberry-graphql-django-0.62.0/tests/test_queries.py000066400000000000000000000171601502405145400231340ustar00rootroot00000000000000import io import textwrap from typing import Optional from unittest import mock import pytest import strawberry from asgiref.sync import sync_to_async from django.core.files.uploadedfile import SimpleUploadedFile from django.test import override_settings from graphql import GraphQLError from PIL import Image from strawberry import auto import strawberry_django from strawberry_django.settings import StrawberryDjangoSettings from . import models, utils @pytest.fixture def user_group(users, groups): users[0].group = groups[0] users[0].save() @strawberry_django.type(models.User) class User: id: auto name: auto group: Optional["Group"] @strawberry_django.type(models.Group) class Group: id: auto name: auto users: list[User] @strawberry_django.type(models.Fruit) class Fruit: id: auto name: auto picture: auto @strawberry_django.type(models.Fruit) class BerryFruit: id: auto name: auto name_upper: str name_lower: str @classmethod def get_queryset(cls, queryset, info, **kwargs): return queryset.filter(name__contains="berry") @strawberry_django.type(models.Fruit, is_interface=True) class FruitInterface: id: auto name: auto @strawberry_django.type(models.Fruit) class BananaFruit(FruitInterface): @classmethod def get_queryset(cls, queryset, info, **kwargs): return queryset.filter(name__contains="banana") @strawberry.type class Query: user: User = strawberry_django.field() users: list[User] = strawberry_django.field() group: Group = strawberry_django.field() groups: list[Group] = strawberry_django.field() fruit: Fruit = strawberry_django.field() berries: list[BerryFruit] = strawberry_django.field() bananas: list[BananaFruit] = strawberry_django.field() @pytest.fixture def query(db): return utils.generate_query(Query) @pytest.fixture def query_id_as_pk(db): with override_settings( STRAWBERRY_DJANGO=StrawberryDjangoSettings( # type: ignore DEFAULT_PK_FIELD_NAME="id", ), ): yield utils.generate_query(Query) pytestmark = [ pytest.mark.django_db(transaction=True), ] async def test_single(query, users): result = await query( """ query GetUser($pk: ID!) { user(pk: $pk) { name } } """, {"pk": users[0].pk}, ) assert not result.errors assert result.data["user"] == {"name": users[0].name} async def test_required_pk_single(query, users): result = await query("{ user { name } }") assert bool(result.errors) assert len(result.errors) == 1 assert isinstance(result.errors[0], GraphQLError) assert ( result.errors[0].message == "Field 'user' argument 'pk' of type 'ID!' is " "required, but it was not provided." ) async def test_id_as_pk_single(query_id_as_pk, users): # Users are created for each test, it's impossible to know what will be the id of users in the database. user_id = users[0].id result = await query_id_as_pk(f"{{ user(id: {user_id}) {{ name }} }}") assert not result.errors assert result.data["user"] == {"name": users[0].name} async def test_required_id_as_pk_single(query_id_as_pk, users): result = await query_id_as_pk("{ user { name } }") assert bool(result.errors) assert len(result.errors) == 1 assert isinstance(result.errors[0], GraphQLError) assert ( result.errors[0].message == "Field 'user' argument 'id' of type 'ID!' is " "required, but it was not provided." ) async def test_many(query, users): result = await query("{ users { name } }") assert not result.errors assert result.data["users"] == [ {"name": users[0].name}, {"name": users[1].name}, {"name": users[2].name}, ] async def test_relation(query, users, groups, user_group): result = await query("{ users { name group { name } } }") assert not result.errors assert result.data["users"] == [ {"name": users[0].name, "group": {"name": groups[0].name}}, {"name": users[1].name, "group": None}, {"name": users[2].name, "group": None}, ] async def test_reverse_relation(query, users, groups, user_group): result = await query("{ groups { name users { name } } }") assert not result.errors assert result.data["groups"] == [ {"name": groups[0].name, "users": [{"name": users[0].name}]}, {"name": groups[1].name, "users": []}, {"name": groups[2].name, "users": []}, ] async def test_type_queryset(query, fruits): result = await query("{ berries { name } }") assert not result.errors assert result.data["berries"] == [ {"name": "strawberry"}, {"name": "raspberry"}, ] async def test_querying_type_implementing_interface(query, fruits): result = await query("{ bananas { name } }") assert not result.errors assert result.data["bananas"] == [{"name": "banana"}] async def test_model_properties(query, fruits): result = await query("{ berries { nameUpper nameLower } }") assert not result.errors assert result.data["berries"] == [ {"nameUpper": "STRAWBERRY", "nameLower": "strawberry"}, {"nameUpper": "RASPBERRY", "nameLower": "raspberry"}, ] async def test_query_file_field(query): img_f = io.BytesIO() img = Image.new(mode="RGB", size=(1, 1), color="red") img.save(img_f, format="jpeg") upload = SimpleUploadedFile("strawberry-picture.png", img_f.getvalue()) fruit = await sync_to_async(models.Fruit.objects.create)( name="Strawberry", picture=upload, ) result = await query( """\ query Fruit ($pk: ID!) { fruit (pk: $pk) { id name picture { name } } } """, {"pk": fruit.pk}, ) assert not result.errors assert result.data is not None assert result.data["fruit"] == { "id": str(fruit.pk), "name": "Strawberry", "picture": {"name": ".tmp_upload/strawberry-picture.png"}, } async def test_query_file_field_when_null(query): fruit = await sync_to_async(models.Fruit.objects.create)(name="Strawberry") result = await query( """\ query Fruit ($pk: ID!) { fruit (pk: $pk) { id name picture { name } } } """, {"pk": fruit.pk}, ) assert not result.errors assert result.data is not None assert result.data["fruit"] == { "id": str(fruit.pk), "name": "Strawberry", "picture": None, } def test_field_name(): """Make sure that field_name overriding is not ignored.""" @strawberry_django.type(models.Fruit) class Fruit: name: auto color_id: int = strawberry_django.field(field_name="color_id") @strawberry.type class Query: @strawberry_django.field def fruit(self) -> Fruit: color = models.Color.objects.create(name="Yellow") return models.Fruit.objects.create( # type: ignore name="Banana", color=color, ) schema = strawberry.Schema(query=Query) expected = """\ type Fruit { name: String! colorId: Int! } type Query { fruit: Fruit! } """ assert textwrap.dedent(str(schema)) == textwrap.dedent(expected).strip() result = schema.execute_sync("""\ query TestQuery { fruit { name colorId } } """) assert result.data == {"fruit": {"colorId": mock.ANY, "name": "Banana"}} strawberry-graphql-django-0.62.0/tests/test_queryset_config.py000066400000000000000000000024671502405145400246710ustar00rootroot00000000000000import pytest from django.db.models import Prefetch from strawberry_django.queryset import get_queryset_config from tests.projects.models import Milestone, Project def test_queryset_config_survives_filter(): qs = Project.objects.all() config = get_queryset_config(qs) config.optimized = True new_qs = qs.filter(pk=1) assert get_queryset_config(new_qs).optimized is True def test_queryset_config_survives_prefetch_related(): qs = Project.objects.all() config = get_queryset_config(qs) config.optimized = True new_qs = qs.prefetch_related("milestones") assert get_queryset_config(new_qs).optimized is True def test_queryset_config_survives_select_related(): qs = Milestone.objects.all() config = get_queryset_config(qs) config.optimized = True new_qs = qs.select_related("project") assert get_queryset_config(new_qs).optimized is True @pytest.mark.django_db(transaction=True) def test_queryset_config_survives_in_prefetch_queryset(): Project.objects.create() qs = Milestone.objects.all() config = get_queryset_config(qs) config.optimized = True project = ( Project.objects.all() .prefetch_related(Prefetch("milestones", queryset=qs)) .get() ) assert get_queryset_config(project.milestones.all()).optimized is True strawberry-graphql-django-0.62.0/tests/test_relay.py000066400000000000000000000035361502405145400225750ustar00rootroot00000000000000import strawberry from strawberry import auto, relay from typing_extensions import Self import strawberry_django from .models import User # textdedent not possible because of comments expected_schema = ''' """An object with a Globally Unique ID""" interface Node { """The Globally Unique ID of this object""" id: ID! } type Query { user: UserType! } type UserType implements Node { """The Globally Unique ID of this object""" id: ID! name: String! } ''' def test_relay_with_nodeid(): @strawberry_django.type(User) class UserType(relay.Node): id: relay.NodeID[int] name: auto @strawberry.type class Query: user: UserType schema = strawberry.Schema(query=Query) # test print_schema assert str(schema) == expected_schema.strip() # check that resolve_id_attr resolves correctly assert UserType.resolve_id_attr() == "id" def test_relay_with_resolve_id_attr(): @strawberry_django.type(User) class UserType(relay.Node): name: auto @classmethod def resolve_id_attr(cls): return "foobar" @strawberry.type class Query: user: UserType # Crash because of early check schema = strawberry.Schema(query=Query) # test print_schema assert str(schema) == expected_schema.strip() def test_relay_with_resolve_id_and_node_id(): @strawberry_django.type(User) class UserType(relay.Node): id: relay.NodeID[int] name: auto @classmethod def resolve_id(cls, root: Self, *, info): # type: ignore return str(root.id) @strawberry.type class Query: user: UserType schema = strawberry.Schema(query=Query) # test print_schema assert str(schema) == expected_schema.strip() # check that resolve_id_attr resolves correctly assert UserType.resolve_id_attr() == "id" strawberry-graphql-django-0.62.0/tests/test_settings.py000066400000000000000000000034261502405145400233170ustar00rootroot00000000000000"""Tests for `strawberry_django/settings.py`.""" from django.test import override_settings from strawberry_django import settings def test_defaults(): """Test defaults. Test that `strawberry_django_settings()` provides the default settings if they don't exist in the Django settings file. """ assert settings.strawberry_django_settings() == settings.DEFAULT_DJANGO_SETTINGS def test_non_defaults(): """Test non defaults. Test that `strawberry_django_settings()` provides the user's settings if they are defined in the Django settings file. """ with override_settings( STRAWBERRY_DJANGO=settings.StrawberryDjangoSettings( FIELD_DESCRIPTION_FROM_HELP_TEXT=True, TYPE_DESCRIPTION_FROM_MODEL_DOCSTRING=True, GENERATE_ENUMS_FROM_CHOICES=True, MUTATIONS_DEFAULT_ARGUMENT_NAME="id", MUTATIONS_DEFAULT_HANDLE_ERRORS=True, MAP_AUTO_ID_AS_GLOBAL_ID=True, DEFAULT_PK_FIELD_NAME="id", USE_DEPRECATED_FILTERS=True, PAGINATION_DEFAULT_LIMIT=250, ALLOW_MUTATIONS_WITHOUT_FILTERS=True, ), ): assert ( settings.strawberry_django_settings() == settings.StrawberryDjangoSettings( FIELD_DESCRIPTION_FROM_HELP_TEXT=True, TYPE_DESCRIPTION_FROM_MODEL_DOCSTRING=True, GENERATE_ENUMS_FROM_CHOICES=True, MUTATIONS_DEFAULT_ARGUMENT_NAME="id", MUTATIONS_DEFAULT_HANDLE_ERRORS=True, MAP_AUTO_ID_AS_GLOBAL_ID=True, DEFAULT_PK_FIELD_NAME="id", USE_DEPRECATED_FILTERS=True, PAGINATION_DEFAULT_LIMIT=250, ALLOW_MUTATIONS_WITHOUT_FILTERS=True, ) ) strawberry-graphql-django-0.62.0/tests/test_type.py000066400000000000000000000070501502405145400224350ustar00rootroot00000000000000import dataclasses import textwrap import strawberry from django.db import models from strawberry.types import get_object_definition import strawberry_django from strawberry_django.fields.field import StrawberryDjangoField from strawberry_django.utils.typing import get_django_definition def test_non_dataclass_annotations_are_ignored_on_type(): class SomeModel(models.Model): name = models.CharField(max_length=255) class NonDataclass: non_dataclass_attr: str @dataclasses.dataclass class SomeDataclass: some_dataclass_attr: str @strawberry.type class SomeStrawberryType: some_strawberry_attr: str @strawberry_django.type(SomeModel) class SomeModelType(SomeStrawberryType, SomeDataclass, NonDataclass): name: str @strawberry.type class Query: my_type: SomeModelType schema = strawberry.Schema(query=Query) expected = """\ type Query { myType: SomeModelType! } type SomeModelType { someStrawberryAttr: String! someDataclassAttr: String! name: String! } """ assert textwrap.dedent(str(schema)) == textwrap.dedent(expected).strip() def test_non_dataclass_annotations_are_ignored_on_input(): class SomeModel2(models.Model): name = models.CharField(max_length=255) class NonDataclass: non_dataclass_attr: str @dataclasses.dataclass class SomeDataclass: some_dataclass_attr: str @strawberry.input class SomeStrawberryInput: some_strawberry_attr: str @strawberry_django.input(SomeModel2) class SomeModelInput(SomeStrawberryInput, SomeDataclass, NonDataclass): name: str @strawberry.type class Query: @strawberry.field def some_field(self, my_input: SomeModelInput) -> str: ... schema = strawberry.Schema(query=Query) expected = """\ type Query { someField(myInput: SomeModelInput!): String! } input SomeModelInput { someStrawberryAttr: String! someDataclassAttr: String! name: String! } """ assert textwrap.dedent(str(schema)) == textwrap.dedent(expected).strip() def test_optimizer_hints_on_type(): class OtherModel(models.Model): name = models.CharField(max_length=255) class SomeModel3(models.Model): name = models.CharField(max_length=255) other = models.ForeignKey(OtherModel, on_delete=models.CASCADE) @strawberry_django.type( SomeModel3, only=["name", "other", "other_name"], select_related=["other"], prefetch_related=["other"], annotate={"other_name": models.F("other__name")}, ) class SomeModelType: name: str store = get_django_definition(SomeModelType, strict=True).store assert store.only == ["name", "other", "other_name"] assert store.select_related == ["other"] assert store.prefetch_related == ["other"] assert store.annotate == {"other_name": models.F("other__name")} def test_custom_field_kept_on_inheritance(): class SomeModel4(models.Model): foo = models.CharField(max_length=255) class CustomField(StrawberryDjangoField): ... @strawberry_django.type(SomeModel4) class SomeModelType: foo: strawberry.auto = CustomField() @strawberry_django.type(SomeModel4) class SomeModelSubclassType(SomeModelType): ... for type_ in [SomeModelType, SomeModelSubclassType]: object_definition = get_object_definition(type_, strict=True) field = object_definition.get_field("foo") assert isinstance(field, CustomField) strawberry-graphql-django-0.62.0/tests/test_types.py000066400000000000000000000306701502405145400226240ustar00rootroot00000000000000import textwrap import pytest import strawberry from django.test import override_settings from strawberry import auto from strawberry.types import get_object_definition from strawberry.types.object_type import StrawberryObjectDefinition import strawberry_django from strawberry_django.fields.field import StrawberryDjangoField from strawberry_django.settings import StrawberryDjangoSettings from .models import Book as BookModel from .models import Color, Fruit, User def test_type_instance(): @strawberry_django.type(User) class UserType: id: auto name: auto user = UserType(id=1, name="user") assert user.id == 1 assert user.name == "user" def test_type_instance_auto_as_str(): @strawberry_django.type(User) class UserType: id: "auto" name: "auto" user = UserType(id=1, name="user") assert user.id == 1 assert user.name == "user" def test_input_instance(): @strawberry_django.input(User) class InputType: id: auto name: auto user = InputType(id=1, name="user") assert user.id == 1 assert user.name == "user" def test_custom_field_cls(): """Custom field_cls is applied to all fields.""" class CustomStrawberryDjangoField(StrawberryDjangoField): pass @strawberry_django.type(User, field_cls=CustomStrawberryDjangoField) class UserType: id: int name: auto assert all( isinstance(field, CustomStrawberryDjangoField) for field in get_object_definition(UserType, strict=True).fields ) def test_custom_field_cls__explicit_field_type(): """Custom field_cls is applied to all fields.""" class CustomStrawberryDjangoField(StrawberryDjangoField): pass @strawberry_django.type(User, field_cls=CustomStrawberryDjangoField) class UserType: id: int name: auto = strawberry_django.field() assert isinstance( get_object_definition(UserType, strict=True).get_field("id"), CustomStrawberryDjangoField, ) assert isinstance( get_object_definition(UserType, strict=True).get_field("name"), StrawberryDjangoField, ) assert not isinstance( get_object_definition(UserType, strict=True).get_field("name"), CustomStrawberryDjangoField, ) def test_field_metadata_default(): """Test metadata default. Test that textual metadata from the Django model isn't reflected in the Strawberry type by default. """ @strawberry_django.type(BookModel) class Book: title: auto type_def = get_object_definition(Book, strict=True) assert type_def.description is None title_field = type_def.get_field("title") assert title_field is not None assert title_field.description is None @override_settings( STRAWBERRY_DJANGO=StrawberryDjangoSettings( # type: ignore FIELD_DESCRIPTION_FROM_HELP_TEXT=True, TYPE_DESCRIPTION_FROM_MODEL_DOCSTRING=True, ), ) def test_field_metadata_preserved(): """Test metadata preserved. Test that textual metadata from the Django model is reflected in the Strawberry type if the settings are enabled. """ @strawberry_django.type(BookModel) class Book: title: auto type_def = get_object_definition(Book, strict=True) assert type_def.description == BookModel.__doc__ title_field = type_def.get_field("title") assert title_field is not None assert title_field.description == BookModel._meta.get_field("title").help_text assert get_object_definition(Book, strict=True).description == BookModel.__doc__ @override_settings( STRAWBERRY_DJANGO=StrawberryDjangoSettings( # type: ignore FIELD_DESCRIPTION_FROM_HELP_TEXT=True, TYPE_DESCRIPTION_FROM_MODEL_DOCSTRING=True, ), ) def test_field_metadata_overridden(): """Test field metadata overriden. Test that the textual metadata from the Django model can be ignored in favor of custom metadata. """ @strawberry_django.type(BookModel, description="A story with pages") class Book: title: auto = strawberry_django.field(description="The name of the story") type_def = get_object_definition(Book, strict=True) assert type_def.description == "A story with pages" title_field = type_def.get_field("title") assert title_field is not None assert title_field.description == "The name of the story" @override_settings( STRAWBERRY_DJANGO=StrawberryDjangoSettings( # type: ignore FIELD_DESCRIPTION_FROM_HELP_TEXT=True, TYPE_DESCRIPTION_FROM_MODEL_DOCSTRING=True, ), ) def test_field_no_empty_strings(monkeypatch: pytest.MonkeyPatch): """Test no empty strings on fields. Test that an empty Django model docstring doesn't get used for the description. """ monkeypatch.setattr(BookModel, "__doc__", "") @strawberry_django.type(BookModel) class Book: title: auto assert get_object_definition(Book, strict=True).description is None @strawberry_django.type(Color) class ColorType: id: auto name: auto @strawberry_django.type(Fruit) class FruitType: id: auto name: auto @strawberry.field def color(self, info, root) -> "ColorType": return root.color def test_type_resolution_with_resolvers(): @strawberry.type class Query: fruit: FruitType = strawberry_django.field() schema = strawberry.Schema(query=Query) type_def = schema.get_type_by_name("FruitType") assert isinstance(type_def, StrawberryObjectDefinition) field = type_def.get_field("color") assert field assert field.type is ColorType @override_settings( STRAWBERRY_DJANGO=StrawberryDjangoSettings( # type: ignore FIELD_DESCRIPTION_FROM_HELP_TEXT=True, TYPE_DESCRIPTION_FROM_MODEL_DOCSTRING=True, ), ) def test_all_fields_works(): @strawberry_django.type(Fruit, fields="__all__") class FruitType: pass @strawberry.type class Query: fruit: FruitType schema = strawberry.Schema(query=Query) expected = '''\ type DjangoImageType { name: String! path: String! size: Int! url: String! width: Int! height: Int! } type DjangoModelType { pk: ID! } """Fruit(id, name, color, sweetness, picture)""" type FruitType { id: ID! name: String! color: DjangoModelType """Level of sweetness, from 1 to 10""" sweetness: Int! picture: DjangoImageType } type Query { fruit: FruitType! } ''' assert textwrap.dedent(str(schema)) == textwrap.dedent(expected).strip() def test_can_override_type_when_fields_all(): @strawberry_django.type(Fruit, fields="__all__") class FruitType: name: int @strawberry.type class Query: fruit: FruitType schema = strawberry.Schema(query=Query) expected = """\ type DjangoImageType { name: String! path: String! size: Int! url: String! width: Int! height: Int! } type DjangoModelType { pk: ID! } type FruitType { name: Int! id: ID! color: DjangoModelType sweetness: Int! picture: DjangoImageType } type Query { fruit: FruitType! } """ assert textwrap.dedent(str(schema)) == textwrap.dedent(expected).strip() def test_fields_can_be_enumerated(): @strawberry_django.type(Fruit, fields=["name", "sweetness"]) class FruitType: pass @strawberry.type class Query: fruit: FruitType schema = strawberry.Schema(query=Query) expected = """\ type FruitType { name: String! sweetness: Int! } type Query { fruit: FruitType! } """ assert textwrap.dedent(str(schema)) == textwrap.dedent(expected).strip() def test_non_existent_fields_ignored(): @strawberry_django.type(Fruit, fields=["name", "sourness"]) class FruitType: pass @strawberry.type class Query: fruit: FruitType schema = strawberry.Schema(query=Query) expected = """\ type FruitType { name: String! } type Query { fruit: FruitType! } """ assert textwrap.dedent(str(schema)) == textwrap.dedent(expected).strip() def test_resolvers_with_fields(): @strawberry_django.type(Fruit, fields=["name"]) class FruitType: @strawberry.field def color(self, info, root) -> "ColorType": return root.color @strawberry.type class Query: fruit: FruitType = strawberry_django.field() schema = strawberry.Schema(query=Query) expected = """\ type ColorType { id: ID! name: String! } type FruitType { name: String! color: ColorType! } type Query { fruit(pk: ID!): FruitType! } """ assert textwrap.dedent(str(schema)) == textwrap.dedent(expected).strip() def test_exclude_with_fields_is_ignored(): @strawberry_django.type(Fruit, fields=["name", "sweetness"], exclude=["name"]) class FruitType: pass @strawberry.type class Query: fruit: FruitType schema = strawberry.Schema(query=Query) expected = """\ type FruitType { name: String! sweetness: Int! } type Query { fruit: FruitType! } """ assert textwrap.dedent(str(schema)) == textwrap.dedent(expected).strip() def test_exclude_includes_non_enumerated_fields(): @strawberry_django.type(Fruit, exclude=["name"]) class FruitType: pass @strawberry.type class Query: fruit: FruitType schema = strawberry.Schema(query=Query) expected = """\ type DjangoImageType { name: String! path: String! size: Int! url: String! width: Int! height: Int! } type DjangoModelType { pk: ID! } type FruitType { id: ID! color: DjangoModelType sweetness: Int! picture: DjangoImageType } type Query { fruit: FruitType! } """ assert textwrap.dedent(str(schema)) == textwrap.dedent(expected).strip() def test_non_existent_fields_exclude_ignored(): @strawberry_django.type(Fruit, exclude=["sourness"]) class FruitType: pass @strawberry.type class Query: fruit: FruitType schema = strawberry.Schema(query=Query) expected = """\ type DjangoImageType { name: String! path: String! size: Int! url: String! width: Int! height: Int! } type DjangoModelType { pk: ID! } type FruitType { id: ID! name: String! color: DjangoModelType sweetness: Int! picture: DjangoImageType } type Query { fruit: FruitType! } """ assert textwrap.dedent(str(schema)) == textwrap.dedent(expected).strip() def test_can_override_type_with_exclude(): @strawberry_django.type(Fruit, exclude=["name"]) class FruitType: sweetness: str @strawberry.type class Query: fruit: FruitType schema = strawberry.Schema(query=Query) expected = """\ type DjangoImageType { name: String! path: String! size: Int! url: String! width: Int! height: Int! } type DjangoModelType { pk: ID! } type FruitType { sweetness: String! id: ID! color: DjangoModelType picture: DjangoImageType } type Query { fruit: FruitType! } """ assert textwrap.dedent(str(schema)) == textwrap.dedent(expected).strip() def test_can_override_fields_with_exclude(): @strawberry_django.type(Fruit, exclude=["name"]) class FruitType: name: auto @strawberry.type class Query: fruit: FruitType schema = strawberry.Schema(query=Query) expected = """\ type DjangoImageType { name: String! path: String! size: Int! url: String! width: Int! height: Int! } type DjangoModelType { pk: ID! } type FruitType { name: String! id: ID! color: DjangoModelType sweetness: Int! picture: DjangoImageType } type Query { fruit: FruitType! } """ assert textwrap.dedent(str(schema)) == textwrap.dedent(expected).strip() strawberry-graphql-django-0.62.0/tests/types.py000066400000000000000000000047501502405145400215650ustar00rootroot00000000000000from __future__ import annotations from django.conf import settings from strawberry import auto import strawberry_django from . import models @strawberry_django.type(models.Fruit) class Fruit: id: auto name: auto color: Color | None types: list[FruitType] picture: auto sweetness: auto @strawberry_django.type(models.Color) class Color: id: auto name: auto fruits: list[Fruit] @strawberry_django.type(models.FruitType) class FruitType: id: auto name: auto fruits: list[Fruit] @strawberry_django.type(models.Vegetable) class Vegetable: id: auto name: auto @strawberry_django.type(models.TomatoWithRequiredPicture, fields="__all__") class TomatoWithRequiredPictureType: pass if settings.GEOS_IMPORTED: @strawberry_django.type(models.GeosFieldsModel) class GeoField: id: auto point: auto line_string: auto polygon: auto multi_point: auto multi_line_string: auto multi_polygon: auto @strawberry_django.input(models.GeosFieldsModel) class GeoFieldInput(GeoField): pass @strawberry_django.input(models.GeosFieldsModel, partial=True) class GeoFieldPartialInput(GeoField): pass @strawberry_django.input(models.Fruit) class FruitInput(Fruit): types: list[FruitTypeInput] | None # type: ignore @strawberry_django.input(models.TomatoWithRequiredPicture) class TomatoWithRequiredPictureInput: id: auto name: auto @strawberry_django.input(models.Color) class ColorInput(Color): pass @strawberry_django.input(models.FruitType) class FruitTypeInput(FruitType): pass @strawberry_django.input(models.Fruit, partial=True) class FruitPartialInput(FruitInput): types: list[FruitTypePartialInput] | None # type: ignore @strawberry_django.partial(models.TomatoWithRequiredPicture, fields="__all__") class TomatoWithRequiredPicturePartialInput(TomatoWithRequiredPictureType): pass @strawberry_django.input(models.Color, partial=True) class ColorPartialInput(ColorInput): pass @strawberry_django.input(models.FruitType, partial=True) class FruitTypePartialInput(FruitTypeInput): pass @strawberry_django.type(models.User) class User: id: auto name: auto group: Group tag: Tag @strawberry_django.type(models.Group) class Group: id: auto name: auto tags: list[Tag] users: list[User] @strawberry_django.type(models.Tag) class Tag: id: auto name: auto groups: list[Group] user: User strawberry-graphql-django-0.62.0/tests/types2/000077500000000000000000000000001502405145400212675ustar00rootroot00000000000000strawberry-graphql-django-0.62.0/tests/types2/__init__.py000066400000000000000000000000001502405145400233660ustar00rootroot00000000000000strawberry-graphql-django-0.62.0/tests/types2/test_input.py000066400000000000000000000065311502405145400240440ustar00rootroot00000000000000from typing import cast import strawberry from strawberry import auto from strawberry.types import get_object_definition from strawberry.types.base import StrawberryOptional import strawberry_django from .test_type import TypeModel def test_input(): @strawberry_django.input(TypeModel) class Input: id: auto boolean: auto string: auto object_definition = get_object_definition(Input, strict=True) assert [(f.name, f.type) for f in object_definition.fields] == [ ("id", StrawberryOptional(cast("type", strawberry.ID))), ("boolean", bool), ("string", str), ] def test_inherit(testtype): @testtype(TypeModel) class Base: id: auto boolean: auto @strawberry_django.input(TypeModel) class Input(Base): string: auto object_definition = get_object_definition(Input, strict=True) assert [(f.name, f.type) for f in object_definition.fields] == [ ("id", StrawberryOptional(cast("type", strawberry.ID))), ("boolean", bool), ("string", str), ] def test_relationship(testtype): @strawberry_django.input(TypeModel) class Input: foreign_key: auto related_foreign_key: auto one_to_one: auto related_one_to_one: auto many_to_many: auto related_many_to_many: auto object_definition = get_object_definition(Input, strict=True) assert [(f.name, f.type) for f in object_definition.fields] == [ ("foreign_key", StrawberryOptional(strawberry_django.OneToManyInput)), ( "related_foreign_key", StrawberryOptional(strawberry_django.ManyToOneInput), ), ("one_to_one", StrawberryOptional(strawberry_django.OneToOneInput)), ( "related_one_to_one", StrawberryOptional(strawberry_django.OneToOneInput), ), ( "many_to_many", StrawberryOptional(strawberry_django.ManyToManyInput), ), ( "related_many_to_many", StrawberryOptional(strawberry_django.ManyToManyInput), ), ] def test_relationship_inherit(testtype): @testtype(TypeModel) class Base: foreign_key: auto related_foreign_key: auto one_to_one: auto related_one_to_one: auto many_to_many: auto related_many_to_many: auto another_name: auto = strawberry_django.field(field_name="foreign_key") @strawberry_django.input(TypeModel) class Input(Base): pass object_definition = get_object_definition(Input, strict=True) assert [(f.name, f.type) for f in object_definition.fields] == [ ("foreign_key", StrawberryOptional(strawberry_django.OneToManyInput)), ( "related_foreign_key", StrawberryOptional(strawberry_django.ManyToOneInput), ), ("one_to_one", StrawberryOptional(strawberry_django.OneToOneInput)), ( "related_one_to_one", StrawberryOptional(strawberry_django.OneToOneInput), ), ( "many_to_many", StrawberryOptional(strawberry_django.ManyToManyInput), ), ( "related_many_to_many", StrawberryOptional(strawberry_django.ManyToManyInput), ), ("another_name", StrawberryOptional(strawberry_django.OneToManyInput)), ] strawberry-graphql-django-0.62.0/tests/types2/test_type.py000066400000000000000000000073531502405145400236710ustar00rootroot00000000000000from typing import Union import strawberry from django.db import models from strawberry import auto from strawberry.types import get_object_definition from strawberry.types.base import ( StrawberryContainer, StrawberryList, StrawberryOptional, ) import strawberry_django from strawberry_django.fields.field import StrawberryDjangoField class TypeModel(models.Model): boolean = models.BooleanField() string = models.CharField(max_length=50) foreign_key = models.ForeignKey( "TypeModel", blank=True, related_name="related_foreign_key", on_delete=models.CASCADE, ) one_to_one = models.OneToOneField( "TypeModel", blank=True, related_name="related_one_to_one", on_delete=models.CASCADE, ) many_to_many = models.ManyToManyField( "TypeModel", related_name="related_many_to_many", ) def test_type(): @strawberry_django.type(TypeModel) class Type: id: auto boolean: auto string: auto object_definition = get_object_definition(Type, strict=True) assert [(f.name, f.type) for f in object_definition.fields] == [ ("id", strawberry.ID), ("boolean", bool), ("string", str), ] def test_inherit(testtype): @testtype(TypeModel) class Base: id: auto boolean: auto @strawberry_django.type(TypeModel) class Type(Base): string: auto object_definition = get_object_definition(Type, strict=True) assert [(f.name, f.type) for f in object_definition.fields] == [ ("id", strawberry.ID), ("boolean", bool), ("string", str), ] def test_default_value(): @strawberry_django.type(TypeModel) class Type: string: auto = "data" string2: str = strawberry.field(default="data2") string3: str = strawberry_django.field(default="data3") object_definition = get_object_definition(Type, strict=True) assert [(f.name, f.type) for f in object_definition.fields] == [ ("string", str), ("string2", str), ("string3", str), ] assert Type().string == "data" assert Type().string2 == "data2" assert Type().string3 == "data3" def test_relationship_inherit(testtype): @testtype(TypeModel) class Base: foreign_key: auto related_foreign_key: auto one_to_one: auto related_one_to_one: auto many_to_many: auto related_many_to_many: auto another_name: auto = strawberry_django.field(field_name="foreign_key") @strawberry_django.type(TypeModel) class Type(Base): pass expected_fields: dict[str, tuple[Union[type, StrawberryContainer], bool]] = { "foreign_key": (strawberry_django.DjangoModelType, False), "related_foreign_key": ( StrawberryList(strawberry_django.DjangoModelType), True, ), "one_to_one": (strawberry_django.DjangoModelType, False), "related_one_to_one": ( StrawberryOptional(strawberry_django.DjangoModelType), False, ), "many_to_many": ( StrawberryList(strawberry_django.DjangoModelType), True, ), "related_many_to_many": ( StrawberryList(strawberry_django.DjangoModelType), True, ), "another_name": (strawberry_django.DjangoModelType, False), } object_definition = get_object_definition(Type, strict=True) assert len(object_definition.fields) == len(expected_fields) for f in object_definition.fields: expected_type, expected_is_list = expected_fields[f.name] assert isinstance(f, StrawberryDjangoField) assert f.is_list == expected_is_list assert f.type == expected_type strawberry-graphql-django-0.62.0/tests/urls.py000066400000000000000000000006641502405145400214060ustar00rootroot00000000000000from django.conf.urls.static import static from django.urls import path from django.urls.conf import include from strawberry.django.views import AsyncGraphQLView, GraphQLView from .projects.schema import schema urlpatterns = [ path("graphql/", GraphQLView.as_view(schema=schema)), path("graphql_async/", AsyncGraphQLView.as_view(schema=schema)), path("__debug__/", include("debug_toolbar.urls")), *static("/media"), ] strawberry-graphql-django-0.62.0/tests/utils.py000066400000000000000000000134071502405145400215600ustar00rootroot00000000000000import asyncio import contextlib import contextvars import dataclasses import inspect import warnings from typing import ( Any, Optional, Union, cast, ) import strawberry from asgiref.sync import sync_to_async from django.db import DEFAULT_DB_ALIAS, connections from django.test.client import AsyncClient, Client from django.test.utils import CaptureQueriesContext from strawberry.test.client import Response from strawberry.utils.inspect import in_async_context from typing_extensions import override from strawberry_django.optimizer import DjangoOptimizerExtension from strawberry_django.test.client import TestClient _client: contextvars.ContextVar["GraphQLTestClient"] = contextvars.ContextVar( "_client_ctx", ) def generate_query(query=None, mutation=None, enable_optimizer=False): append_mutation = mutation and not query if query is None: @strawberry.type class Query: x: int query = Query extensions = [] if enable_optimizer: extensions = [DjangoOptimizerExtension()] schema = strawberry.Schema(query=query, mutation=mutation, extensions=extensions) def process_result(result): return result async def query_async(query, variable_values, context_value): result = await schema.execute( query, variable_values=variable_values, context_value=context_value, ) return process_result(result) def query_sync(query, variable_values=None, context_value=None): if append_mutation and not query.startswith("mutation"): query = f"mutation {query}" if in_async_context(): return query_async( query, variable_values=variable_values, context_value=context_value, ) result = schema.execute_sync( query, variable_values=variable_values, context_value=context_value, ) return process_result(result) return query_sync def dataclass(model): def wrapper(cls): return dataclasses.dataclass(cls) return wrapper def deep_tuple_to_list(data: tuple) -> list: return_list = [] for elem in data: if isinstance(elem, tuple): return_list.append(deep_tuple_to_list(elem)) else: return_list.append(elem) return return_list class AsyncCaptureQueriesContext: wrapped: CaptureQueriesContext def __init__(self, using: str): super().__init__() self.using = using @sync_to_async def wrapped_enter(self): self.wrapped = CaptureQueriesContext(connection=connections[self.using]) return self.wrapped.__enter__() # noqa: PLC2801 def __enter__(self): return asyncio.run(self.wrapped_enter()) def __exit__(self, exc_type, exc_value, traceback, /): return asyncio.run( sync_to_async(self.wrapped.__exit__)(exc_type, exc_value, traceback) ) @contextlib.contextmanager def assert_num_queries(n: int, *, using=DEFAULT_DB_ALIAS): is_async = (gql_client := _client.get(None)) is not None and gql_client.is_async if is_async: ctx_manager = AsyncCaptureQueriesContext(using) else: ctx_manager = CaptureQueriesContext(connection=connections[using]) with ctx_manager as ctx: yield ctx executed = len(ctx) assert executed == n, ( "{} queries executed, {} expected\nCaptured queries were:\n{}".format( executed, n, "\n".join( f"{i}. {q['sql']}" for i, q in enumerate(ctx.captured_queries, start=1) ), ) ) class GraphQLTestClient(TestClient): def __init__( self, path: str, client: Union[Client, AsyncClient], ): super().__init__(path, client=cast("Client", client)) self._token: Optional[contextvars.Token] = None self.is_async = isinstance(client, AsyncClient) def __enter__(self): self._token = _client.set(self) return self def __exit__(self, *args, **kwargs): assert self._token _client.reset(self._token) def request( self, body: dict[str, object], headers: Optional[dict[str, object]] = None, files: Optional[dict[str, object]] = None, ): kwargs: dict[str, object] = {"data": body} if files: # pragma:nocover kwargs["format"] = "multipart" else: kwargs["content_type"] = "application/json" return self.client.post( self.path, **kwargs, # type: ignore ) @override def query( self, query: str, variables: Optional[dict[str, Any]] = None, headers: Optional[dict[str, object]] = None, asserts_errors: Optional[bool] = None, files: Optional[dict[str, object]] = None, assert_no_errors: Optional[bool] = True, ) -> Response: body = self._build_body(query, variables, files) resp = self.request(body, headers, files) if inspect.iscoroutine(resp): resp = asyncio.run(resp) data = self._decode(resp, type="multipart" if files else "json") response = Response( errors=data.get("errors"), data=data.get("data"), extensions=data.get("extensions"), ) if asserts_errors is not None: warnings.warn( "The `asserts_errors` argument has been renamed to `assert_no_errors`", DeprecationWarning, stacklevel=2, ) assert_no_errors = ( assert_no_errors if asserts_errors is None else asserts_errors ) if assert_no_errors: assert response.errors is None return response