diff --git a/.coveragerc b/.coveragerc index 1ee3561b23..17dacb2d7f 100644 --- a/.coveragerc +++ b/.coveragerc @@ -2,6 +2,8 @@ [run] branch = True +source = . + [report] # Regexes for lines to exclude from consideration exclude_lines = @@ -10,6 +12,7 @@ exclude_lines = # Don't complain about missing debug-only code: def __repr__ + def __str__ if self\.debug # Don't complain if tests don't hit defensive assertion code: @@ -21,19 +24,12 @@ exclude_lines = if 0: if __name__ == .__main__.: - # Don't complain about abstract methods, they aren't run: - @(abc\.)?abstractmethod - # Don't complain about TYPE_CHECKING if TYPE_CHECKING: @overload -ignore_errors = True omit = ./.venv/** ./tests/* - -[html] -directory = coverage_html_report diff --git a/.devcontainer/post-install.sh b/.devcontainer/post-install.sh index 87139cc967..3d364f76ee 100755 --- a/.devcontainer/post-install.sh +++ b/.devcontainer/post-install.sh @@ -1,2 +1,2 @@ poetry install -pre-commit install --install-hooks +pre-commit install diff --git a/.dockerignore b/.dockerignore deleted file mode 100644 index f35180d280..0000000000 --- a/.dockerignore +++ /dev/null @@ -1,8 +0,0 @@ -.git -.github -.benchmarks -.devcotainer -.venv -.mypy_cache -.nox -.ruff_cache diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000000..3785ce9d93 --- /dev/null +++ b/.flake8 @@ -0,0 +1,14 @@ +[flake8] +max-line-length = 89 +exclude=.venv,.git +ignore = W503 +extend-ignore = + # See https://github.com/PyCQA/pycodestyle/issues/373 + E203, + +per-file-ignores = + tests/types/test_lazy_types.py:E800 + tests/test_forward_references.py:E800 + tests/schema/test_resolvers.py:E800 + tests/types/test_string_annotations.py:E800 + tests/federation/test_printer.py:E800 diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000000..eaca5f0d55 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,3 @@ +# These are supported funding model platforms + +github: [patrick91, BryceBeagle] diff --git a/.github/ISSUE_TEMPLATES/bug_report.md b/.github/ISSUE_TEMPLATES/bug_report.md new file mode 100644 index 0000000000..e4f6503ed6 --- /dev/null +++ b/.github/ISSUE_TEMPLATES/bug_report.md @@ -0,0 +1,21 @@ +--- +name: Bug report +about: Create a bug report to help us improve the Strawberry GraphQL library +--- + + + + + +## Describe the Bug + + + +## System Information + + - Operating system: + - Strawberry version: + +## Additional Context + + diff --git a/.github/ISSUE_TEMPLATES/feature_request.md b/.github/ISSUE_TEMPLATES/feature_request.md new file mode 100644 index 0000000000..88b6a1faff --- /dev/null +++ b/.github/ISSUE_TEMPLATES/feature_request.md @@ -0,0 +1,18 @@ +--- +name: Feature request +about: Suggest a new feature or changes to existing features +--- + + + + + +## Feature Request Type + +- [ ] Core functionality +- [ ] Alteration (enhancement/optimization) of existing feature(s) +- [ ] New behavior + +## Description + + diff --git a/.github/ISSUE_TEMPLATES/other_issues.md b/.github/ISSUE_TEMPLATES/other_issues.md new file mode 100644 index 0000000000..c4a6545569 --- /dev/null +++ b/.github/ISSUE_TEMPLATES/other_issues.md @@ -0,0 +1,7 @@ +--- +name: Other issues +about: Anything else that doesn't fall into the above categories. +--- + + + diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000000..f4191dae6b --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,32 @@ + + + + + +## Description + + + +## Types of Changes + + +- [ ] Core +- [ ] Bugfix +- [ ] New feature +- [ ] Enhancement/optimization +- [ ] Documentation + +## Issues Fixed or Closed by This PR + +* + +## Checklist + + + +- [ ] My code follows the code style of this project. +- [ ] My change requires a change to the documentation. +- [ ] I have updated the documentation accordingly. +- [ ] I have read the CONTRIBUTING document. +- [ ] I have added tests to cover my changes. +- [ ] I have tested the changes and verified that they work and don't break anything (as well as I can manage). diff --git a/.github/bot-action/main.py b/.github/bot-action/main.py index 3189c2e741..c18f477cf4 100644 --- a/.github/bot-action/main.py +++ b/.github/bot-action/main.py @@ -4,6 +4,7 @@ import httpx + API_URL = os.environ["BOT_API_URL"] API_TOKEN = os.environ["API_SECRET"] diff --git a/.github/pyproject.toml b/.github/pyproject.toml deleted file mode 100644 index cc0cf13d81..0000000000 --- a/.github/pyproject.toml +++ /dev/null @@ -1,5 +0,0 @@ -[tool.ruff] -extend = "../pyproject.toml" -extend-ignore = [ - "T201", -] diff --git a/.github/release-check-action/check.py b/.github/release-check-action/check.py index df0abfaac9..8af0c3dfdc 100644 --- a/.github/release-check-action/check.py +++ b/.github/release-check-action/check.py @@ -4,6 +4,7 @@ from config import GITHUB_WORKSPACE, RELEASE_FILE_PATH from release import InvalidReleaseFileError, get_release_info + release_file = pathlib.Path(GITHUB_WORKSPACE) / RELEASE_FILE_PATH release_info = None diff --git a/.github/release-check-action/config.py b/.github/release-check-action/config.py index 16b7af90fb..01d2fe78b1 100644 --- a/.github/release-check-action/config.py +++ b/.github/release-check-action/config.py @@ -1,5 +1,6 @@ import os + RELEASE_FILE_PATH = "RELEASE.md" GITHUB_SHA = os.environ["GITHUB_SHA"] GITHUB_EVENT_PATH = os.environ["GITHUB_EVENT_PATH"] diff --git a/.github/release-check-action/release.py b/.github/release-check-action/release.py index 5f00c432cf..af7fa7c537 100644 --- a/.github/release-check-action/release.py +++ b/.github/release-check-action/release.py @@ -3,6 +3,7 @@ from enum import Enum from pathlib import Path + RELEASE_TYPE_REGEX = re.compile(r"^[Rr]elease [Tt]ype: (major|minor|patch)$") diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 8f51e34a09..7b7fa94fa6 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -1,4 +1,4 @@ -name: ๐Ÿ” CodeQL +name: "CodeQL" on: push: @@ -10,21 +10,35 @@ on: jobs: analyze: + name: Analyze runs-on: ubuntu-latest - permissions: - security-events: write - actions: read - contents: read + strategy: + fail-fast: false + matrix: + language: ['python'] steps: - - name: Checkout repository - uses: actions/checkout@v3 + - name: Checkout repository + uses: actions/checkout@v2 + with: + # We must fetch at least the immediate parents so that if this is + # a pull request then we can checkout the head. + fetch-depth: 2 - - name: Initialize CodeQL - uses: github/codeql-action/init@v2 - with: - languages: python + # If this run was triggered by a pull request event, then checkout + # the head of the pull request instead of the merge commit. + - run: git checkout HEAD^2 + if: ${{ github.event_name == 'pull_request' }} - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v1 + with: + languages: ${{ matrix.language }} + + - name: Autobuild + uses: github/codeql-action/autobuild@v1 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v1 diff --git a/.github/workflows/federation-compatibility.yml b/.github/workflows/federation-compatibility.yml deleted file mode 100644 index 3428bc3ebd..0000000000 --- a/.github/workflows/federation-compatibility.yml +++ /dev/null @@ -1,47 +0,0 @@ -name: ๐Ÿ›ฐ๏ธ Federation compatibility tests - -concurrency: - group: ${{ github.head_ref || github.run_id }} - cancel-in-progress: true - -on: - push: - branches: [main] - pull_request: - branches: [main] - paths: - - "strawberry/federation/**" - - "strawberry/printer/**" - - "pyproject.toml" - - "poetry.lock" - - ".github/workflows/federation-compatibility.yml" - -jobs: - federation-tests: - name: Federation tests - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v3 - - - run: pipx install poetry - - uses: actions/setup-python@v4 - id: setup-python - with: - python-version: "3.10" - cache: "poetry" - - run: poetry env use python3.10 - - run: poetry install - - - name: export schema - run: poetry run strawberry export-schema schema:schema > schema.graphql - working-directory: federation-compatibility - - - uses: apollographql/federation-subgraph-compatibility@v1 - with: - compose: 'federation-compatibility/docker-compose.yml' - schema: 'federation-compatibility/schema.graphql' - port: 4001 - token: ${{ secrets.BOT_TOKEN }} - failOnWarning: true - failOnRequired: true diff --git a/.github/workflows/invite-contributors.yml b/.github/workflows/invite-contributors.yml index fe1c983ed9..9baba1d761 100644 --- a/.github/workflows/invite-contributors.yml +++ b/.github/workflows/invite-contributors.yml @@ -1,4 +1,4 @@ -name: ๐Ÿ‘ฅ Invite contributors +name: Invite contributors on: push: diff --git a/.github/workflows/mypy.yml b/.github/workflows/mypy.yml index 25ed706a26..70ae1fc6c5 100644 --- a/.github/workflows/mypy.yml +++ b/.github/workflows/mypy.yml @@ -1,4 +1,4 @@ -name: ๐Ÿฆบ MyPy +name: MyPy on: push: @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v2 id: setup-python with: python-version: '3.10' @@ -38,5 +38,4 @@ jobs: run: poetry install if: steps.cache-poetry-dependencies.outputs.cache-hit != 'true' - - run: mkdir -p .mypy_cache - run: poetry run mypy --ignore-missing-imports --config-file mypy.ini --install-types --non-interactive --show-traceback strawberry diff --git a/.github/workflows/ok-to-preview.yml b/.github/workflows/ok-to-preview.yml index fbc1985c43..a62a85420f 100644 --- a/.github/workflows/ok-to-preview.yml +++ b/.github/workflows/ok-to-preview.yml @@ -1,4 +1,4 @@ -name: ๐Ÿ†— Ok to preview +name: Ok to preview on: pull_request_target: diff --git a/.github/workflows/pre-release-pr.yml b/.github/workflows/pre-release-pr.yml index 03ef215bd6..84acbf815a 100644 --- a/.github/workflows/pre-release-pr.yml +++ b/.github/workflows/pre-release-pr.yml @@ -1,4 +1,4 @@ -name: ๐ŸŽ Release test pre-release version +name: Release test pre-release version on: repository_dispatch: @@ -13,7 +13,7 @@ jobs: with: ref: ${{ github.event.client_payload.pull_request.head.sha }} - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v2 with: python-version: "3.10" - name: Install deps @@ -60,7 +60,7 @@ jobs: POETRY_PYPI_TOKEN_PYPI: ${{ secrets.PYPI_TOKEN }} run: | autopub prepare - poetry version $(poetry version -s).dev.$(date '+%s') + poetry version $(poetry version -s)-dev.$(date '+%s') poetry build poetry publish --username __token__ echo "::set-output name=version::$(poetry version -s)" diff --git a/.github/workflows/release-check.yml b/.github/workflows/release-check.yml index a4f3a6e1a7..2cee430707 100644 --- a/.github/workflows/release-check.yml +++ b/.github/workflows/release-check.yml @@ -1,4 +1,4 @@ -name: ๐Ÿ†™ Release file check +name: Release file check on: pull_request_target: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index de55c70fa6..1ffd0b5427 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,4 +1,4 @@ -name: ๐Ÿ†™ Release +name: Release concurrency: release @@ -36,9 +36,9 @@ jobs: with: persist-credentials: false - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v2 with: - python-version: "3.10" + python-version: 3.7 - name: Install deps run: | python -m pip install pip --upgrade @@ -67,12 +67,9 @@ jobs: id: get-version shell: python run: | - import os - from pathlib import Path from autopub.base import get_project_version - with Path(os.environ["GITHUB_OUTPUT"]).open('a') as f: - f.write(f"version={get_project_version()}\n") + print(f"::set-output name=version::{get_project_version()}") get-contributor-info: name: Get PR info @@ -82,74 +79,13 @@ jobs: outputs: contributor-name: ${{ steps.get-info.outputs.contributor-name }} - contributor-username: ${{ steps.get-info.outputs.contributor-username }} contributor-twitter-username: ${{ steps.get-info.outputs.contributor-twitter-username }} - pr-number: ${{ steps.get-info.outputs.pr-number }} steps: - name: Get PR info id: get-info uses: strawberry-graphql/get-pr-info-action@v6 - update-release-on-github: - name: Update release on github - runs-on: ubuntu-latest - needs: [release-file-check, get-contributor-info, release] - if: ${{ needs.release-file-check.outputs.status == 'OK' }} - steps: - - uses: actions/setup-python@v4 - with: - python-version: '3.9' - - name: Install dependencies - run: pip install httpx - - name: Send tweet - shell: python - run: | - import os - - import httpx - - - tag = os.environ["TAG"] - contributor_username = os.environ["CONTRIBUTOR_USERNAME"] - pr_number = os.environ["PR_NUMBER"] - - - response = httpx.get( - url=f"https://api.github.com/repos/strawberry-graphql/strawberry/releases/tags/{tag}", - headers={ - "Accept": "application/vnd.github.v3+json", - }, - ) - - response.raise_for_status() - data = response.json() - release_id = data["id"] - release_body = data["body"].strip() - - release_footer = f""" - Releases contributed by @{contributor_username} via #{pr_number} - """.strip() - - updated_release_body = f"{release_body}\n\n{release_footer}" - - response = httpx.patch( - url=f"https://api.github.com/repos/strawberry-graphql/strawberry/releases/{release_id}", - json={"body": updated_release_body}, - headers={ - "Accept": "application/vnd.github.v3+json", - "Authorization": f"token {os.environ['GITHUB_TOKEN']}", - }, - ) - - response.raise_for_status() - - env: - GITHUB_TOKEN: ${{ secrets.BOT_TOKEN }} - TAG: ${{ needs.release.outputs.version }} - CONTRIBUTOR_USERNAME: ${{ needs.get-contributor-info.outputs.contributor-username }} - PR_NUMBER: ${{ needs.get-contributor-info.outputs.pr-number }} - read-tweet-md: name: Read TWEET.md runs-on: ubuntu-latest @@ -203,7 +139,7 @@ jobs: - uses: actions/download-artifact@v2 with: name: card - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v2 with: python-version: '3.9' - name: Install dependencies diff --git a/.github/workflows/slash-commands.yml b/.github/workflows/slash-commands.yml index cad2a2adde..38625574ae 100644 --- a/.github/workflows/slash-commands.yml +++ b/.github/workflows/slash-commands.yml @@ -1,4 +1,4 @@ -name: ๐Ÿ’ฌ Slash Command Dispatch +name: Slash Command Dispatch on: issue_comment: diff --git a/.github/workflows/test-type-checkers.yml b/.github/workflows/test-type-checkers.yml deleted file mode 100644 index a8cdb4ca75..0000000000 --- a/.github/workflows/test-type-checkers.yml +++ /dev/null @@ -1,72 +0,0 @@ -name: ๐Ÿฆบ Type checkers tests - -concurrency: - group: type-checkers-${{ github.head_ref || github.run_id }} - cancel-in-progress: true - -on: - push: - branches: [main] - pull_request: - branches: [main] - paths: - - "strawberry/**" - - "tests/mypy/**" - - "tests/pyright/**" - - "pyproject.toml" - - "poetry.lock" - - ".github/workflows/test-type-checkers.yml" - -jobs: - mypy: - strategy: - matrix: - python-version: ["3.7", "3.8", "3.9", "3.10"] - - name: Mypy on Python ${{ matrix.python-version }} - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v3 - - run: pipx install poetry - - uses: actions/setup-python@v4 - id: setup-python - with: - python-version: ${{ matrix.python-version }} - cache: "poetry" - - - run: poetry env use ${{ matrix.python-version }} - - run: poetry install - if: steps.setup-python.outputs.cache-hit != 'true' - - - name: pytest - run: - poetry run pytest tests/mypy - - pyright: - strategy: - matrix: - python-version: ["3.7", "3.8", "3.9", "3.10"] - - name: Pyright on Python ${{ matrix.python-version }} - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v3 - - run: pipx install poetry - - uses: actions/setup-python@v4 - id: setup-python - with: - python-version: ${{ matrix.python-version }} - cache: "poetry" - - - uses: actions/setup-node@v2 - - run: npm install -g --no-package-lock --no-save pyright - - - run: poetry env use ${{ matrix.python-version }} - - run: poetry install - if: steps.setup-python.outputs.cache-hit != 'true' - - - name: pytest - run: - poetry run pytest tests/pyright diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3345e6246c..33f8f9de2d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,4 +1,4 @@ -name: ๐Ÿ”‚ Unit tests +name: Backend tests concurrency: group: ${{ github.head_ref || github.run_id }} @@ -20,60 +20,50 @@ jobs: unit-tests: strategy: matrix: - python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] + python-version: ['3.7', '3.8', '3.9', '3.10'] + os: [ubuntu-latest, windows-latest] + include: + - os: ubuntu-latest + cache-path: ~/.cache/pypoetry/virtualenvs + - os: windows-latest + cache-path: ~\AppData\Local\pypoetry\Cache - name: Python ${{ matrix.python-version }} - runs-on: ubuntu-latest + name: Test ${{ matrix.os }} - Python ${{ matrix.python-version }} + runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v3 - - run: pipx install poetry - - uses: actions/setup-python@v4 + - uses: actions/checkout@v2 + + - uses: actions/setup-python@v2 id: setup-python with: python-version: ${{ matrix.python-version }} - cache: "poetry" + architecture: x64 - - run: poetry env use ${{ matrix.python-version }} - - run: poetry install - if: steps.setup-python.outputs.cache-hit != 'true' - - - name: pytest - run: - poetry run pytest --cov=strawberry --cov-append --cov-report=xml -n auto --showlocals -vv -m "not - starlette" -m "not django" --ignore=tests/mypy --ignore=tests/pyright --ignore=tests/starlite - - if: ${{ always() }} - - uses: codecov/codecov-action@v3 - if: ${{ always() }} + - uses: actions/setup-node@v2 with: - token: ${{ secrets.CODECOV_TOKEN }} - fail_ci_if_error: true - verbose: true + node-version: '14' - unit-tests-on-windows: - name: Python 3.10.0 on Windows - runs-on: windows-latest + - run: npm install -g --no-package-lock --no-save pyright + - run: pip install poetry + - run: poetry config experimental.new-installer false - steps: - - uses: actions/checkout@v3 - - run: pipx install poetry - - uses: actions/setup-python@v4 - id: setup-python + - name: "Python dependencies cache" + id: cache-poetry-dependencies + uses: actions/cache@v2 with: - python-version: "3.10.0" - cache: "poetry" + path: ${{ matrix.cache-path }} + key: ${{ runner.os }}-py-${{ steps.setup-python.outputs.python-version }}-poetry-${{ hashFiles('poetry.lock') }} + restore-keys: ${{ runner.os }}-py-${{ steps.setup-python.outputs.python-version }}-poetry- - - run: poetry install - if: steps.setup-python.outputs.cache-hit != 'true' + - name: Install dependencies + run: poetry install + if: steps.cache-poetry-dependencies.outputs.cache-hit != 'true' - name: pytest - run: - poetry run pytest --cov=strawberry --cov-append --cov-report=xml -n auto --showlocals -vv -m "not - django" --ignore=tests/mypy --ignore=tests/pyright + run: poetry run pytest --cov-report xml --cov=. --showlocals -vv -m "not django" - if: ${{ always() }} - - uses: codecov/codecov-action@v3 + - uses: codecov/codecov-action@v1 if: ${{ always() }} with: token: ${{ secrets.CODECOV_TOKEN }} @@ -83,109 +73,44 @@ jobs: django-unit-tests: strategy: matrix: - django: ["4.0", "3.2"] + django: ["4.0", "3.2", "2.2"] - name: Django ${{ matrix.django }} + name: Test Django ${{ matrix.django }} runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - run: pipx install poetry - - uses: actions/setup-python@v4 - id: setup-python - with: - python-version: "3.10" - cache: "poetry" - - - run: poetry env use python3.10 - - run: poetry install - if: steps.setup-python.outputs.cache-hit != 'true' - - - name: Install django ${{ matrix.django }} - run: poetry add --python ^3.10 django@^${{ matrix.django }} - - - name: pytest - run: - poetry run pytest --cov=strawberry --cov-append --cov-report=xml -n auto --showlocals -vv -m django - - if: ${{ always() }} - - uses: codecov/codecov-action@v3 - if: ${{ always() }} - with: - token: ${{ secrets.CODECOV_TOKEN }} - fail_ci_if_error: true - verbose: true + - uses: actions/checkout@v2 - starlette-unit-tests: - strategy: - matrix: - starlette: ["0.20.4", "0.21.0", "0.22.0", "0.23.0"] - - name: Starlette ${{ matrix.starlette }} - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v3 - - run: pipx install poetry - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v2 id: setup-python with: - python-version: "3.10" - cache: "poetry" - - - run: poetry env use python3.10 - - run: poetry install - if: steps.setup-python.outputs.cache-hit != 'true' + # Pin to 3.9 because Django 2.2 does not support 3.10 + python-version: '3.9' + architecture: x64 - - name: Install starlette ${{ matrix.starlette }} - run: poetry run pip install starlette==${{ matrix.starlette }} + - run: pip install poetry + - run: poetry config experimental.new-installer false - - name: Install fastapi 0.88.0 - run: poetry run pip install "fastapi<0.88.0" - if: matrix.starlette == '0.20.4' - - - name: pytest - run: - poetry run pytest --cov=strawberry --cov-append --cov-report=xml -n auto --showlocals -vv -m - starlette - - if: ${{ always() }} - - uses: codecov/codecov-action@v3 - if: ${{ always() }} + - name: "Python dependencies cache" + id: cache-poetry-dependencies + uses: actions/cache@v2 with: - token: ${{ secrets.CODECOV_TOKEN }} - fail_ci_if_error: true - verbose: true + path: ~/.cache/pypoetry/virtualenvs + key: ${{ runner.os }}-py-${{ steps.setup-python.outputs.python-version }}-poetry-${{ hashFiles('poetry.lock') }} + restore-keys: ${{ runner.os }}-py-${{ steps.setup-python.outputs.python-version }}-poetry- - starlite-unit-tests: - strategy: - matrix: - python-version: ["3.8", "3.9", "3.10", "3.11"] + - name: Install dependencies + run: poetry install + if: steps.cache-poetry-dependencies.outputs.cache-hit != 'true' - name: Python ${{ matrix.python-version }} - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v3 - - run: pipx install poetry - - uses: actions/setup-python@v4 - id: setup-python - with: - python-version: ${{ matrix.python-version }} - cache: "poetry" - - - run: poetry env use ${{ matrix.python-version }} - - run: poetry install - if: steps.setup-python.outputs.cache-hit != 'true' + - name: Install django ${{ matrix.django }} + # Add --python ^3.9 marker because django 4.0 does not support 3.7 + run: poetry add --python ^3.9 django@^${{ matrix.django }} - name: pytest - run: - poetry run coverage run -m pytest --showlocals -vv tests/starlite + run: poetry run pytest --cov-report xml --cov=. --showlocals -vv -m django - - name: coverage xml - run: poetry run coverage xml -i - if: ${{ always() }} - - uses: codecov/codecov-action@v3 + - uses: codecov/codecov-action@v1 if: ${{ always() }} with: token: ${{ secrets.CODECOV_TOKEN }} diff --git a/.gitignore b/.gitignore index 0b1023af7d..968f8ff3e1 100644 --- a/.gitignore +++ b/.gitignore @@ -146,8 +146,6 @@ venv/ ENV/ env.bak/ venv.bak/ -Pipfile -Pipfile.lock # Spyder project settings .spyderproject diff --git a/.gitpod.yml b/.gitpod.yml deleted file mode 100644 index c27247692a..0000000000 --- a/.gitpod.yml +++ /dev/null @@ -1,8 +0,0 @@ -tasks: - - before: | - curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py | python - - source $HOME/.poetry/env - pip install pre-commit - init: | - poetry install - pre-commit install --install-hooks diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d8ec60a0c0..81a5ac148d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,15 +1,25 @@ repos: - - repo: https://github.com/psf/black - rev: 22.12.0 + - repo: https://github.com/myint/autoflake + rev: 'v1.4' hooks: - - id: black - exclude: ^tests/codegen/snapshots/python/ + - id: autoflake + args: ['--in-place', '--remove-all-unused-imports', '--ignore-init-module-imports'] + name: autoflake + entry: autoflake + language: python + 'types': [python] + require_serial: true + + - repo: https://github.com/PyCQA/flake8 + rev: 4.0.1 + hooks: + - id: flake8 + additional_dependencies: ["flake8-eradicate==1.2.0"] - - repo: https://github.com/charliermarsh/ruff-pre-commit - rev: v0.0.233 + - repo: https://github.com/pycqa/isort + rev: 5.10.1 hooks: - - id: ruff - exclude: ^tests/codegen/snapshots/python/ + - id: isort - repo: https://github.com/patrick91/pre-commit-alex rev: aa5da9e54b92ab7284feddeaf52edf14b1690de3 @@ -18,20 +28,26 @@ repos: exclude: CHANGELOG.md - repo: https://github.com/pre-commit/mirrors-prettier - rev: v3.0.0-alpha.4 + rev: v2.5.1 hooks: - id: prettier files: '^docs/.*\.mdx?$' - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.4.0 + rev: v4.1.0 hooks: - id: trailing-whitespace - id: check-merge-conflict - id: end-of-file-fixer - id: check-toml - - repo: https://github.com/adamchainz/blacken-docs - rev: 1.13.0 + - repo: https://github.com/humitos/mirrors-autoflake.git + rev: v1.1 hooks: - - id: blacken-docs + - id: autoflake + args: ['--in-place', '--remove-all-unused-imports', '--remove-unused-variable'] + + - repo: https://github.com/psf/black + rev: 22.1.0 + hooks: + - id: black diff --git a/CHANGELOG.md b/CHANGELOG.md index 16aa22b89e..2352e558a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,2906 +1,6 @@ CHANGELOG ========= -0.155.2 - 2023-01-25 --------------------- - -This release fixes a bug in subscriptions using the graphql-transport-ws protocol -where the conversion of the NextMessage object to a dictionary took an unnecessary -amount of time leading to an increase in CPU usage. - -Contributed by [rjwills28](https://github.com/rjwills28) via [PR #2481](https://github.com/strawberry-graphql/strawberry/pull/2481/) - - -0.155.1 - 2023-01-24 --------------------- - -A link to the changelog has been added to the package metadata, so it shows up on PyPI. - -Contributed by [Tom Most](https://github.com/twm) via [PR #2490](https://github.com/strawberry-graphql/strawberry/pull/2490/) - - -0.155.0 - 2023-01-23 --------------------- - -This release adds a new utility function to convert a Strawberry object to a -dictionary. - -You can use `strawberry.asdict(...)` function to convert a Strawberry object to -a dictionary: - -```python -@strawberry.type -class User: - name: str - age: int - - -# should be {"name": "Lorem", "age": 25} -user_dict = strawberry.asdict(User(name="Lorem", age=25)) -``` - -> Note: This function uses the `dataclasses.asdict` function under the hood, so -> you can safely replace `dataclasses.asdict` with `strawberry.asdict` in your -> code. This will make it easier to update your code to newer versions of -> Strawberry if we decide to change the implementation. - -Contributed by [Haze Lee](https://github.com/Hazealign) via [PR #2417](https://github.com/strawberry-graphql/strawberry/pull/2417/) - - -0.154.1 - 2023-01-17 --------------------- - -Fix `DuplicatedTypeName` exception being raised on generics declared using -`strawberry.lazy`. Previously the following would raise: - -```python -# issue_2397.py -from typing import Annotated, Generic, TypeVar - -import strawberry - -T = TypeVar("T") - - -@strawberry.type -class Item: - name: str - - -@strawberry.type -class Edge(Generic[T]): - node: T - - -@strawberry.type -class Query: - edges_normal: Edge[Item] - edges_lazy: Edge[Annotated["Item", strawberry.lazy("issue_2397")]] - - -if __name__ == "__main__": - schema = strawberry.Schema(query=Query) -``` - -Contributed by [pre-commit-ci](https://github.com/pre-commit-ci) via [PR #2462](https://github.com/strawberry-graphql/strawberry/pull/2462/) - - -0.154.0 - 2023-01-13 --------------------- - -Support constrained float field types in Pydantic models. - -i.e. - -```python -import pydantic - - -class Model(pydantic.BaseModel): - field: pydantic.confloat(le=100.0) - equivalent_field: float = pydantic.Field(le=100.0) -``` - -Contributed by [Etienne Wodey](https://github.com/airwoodix) via [PR #2455](https://github.com/strawberry-graphql/strawberry/pull/2455/) - - -0.153.0 - 2023-01-13 --------------------- - -This change allows clients to define connectionParams when making Subscription requests similar to the way [Apollo-Server](https://www.apollographql.com/docs/apollo-server/data/subscriptions/#operation-context) does it. - -With [Apollo-Client (React)](https://www.apollographql.com/docs/react/data/subscriptions/#5-authenticate-over-websocket-optional) as an example, define a Websocket Link: -``` -import { GraphQLWsLink } from '@apollo/client/link/subscriptions'; -import { createClient } from 'graphql-ws'; - -const wsLink = new GraphQLWsLink(createClient({ - url: 'ws://localhost:4000/subscriptions', - connectionParams: { - authToken: user.authToken, - }, -})); -``` -and the JSON passed to `connectionParams` here will appear within Strawberry's context as the `connection_params` attribute when accessing `info.context` within a Subscription resolver. - -Contributed by [Tommy Smith](https://github.com/tsmith023) via [PR #2380](https://github.com/strawberry-graphql/strawberry/pull/2380/) - - -0.152.0 - 2023-01-10 --------------------- - -This release adds support for updating (or adding) the query document inside an -extension's `on_request_start` method. - -This can be useful for implementing persisted queries. The old behavior of -returning a 400 error if no query is present in the request is still supported. - -Example usage: - -```python -from strawberry.extensions import Extension - - -def get_doc_id(request) -> str: - """Implement this to get the document ID using your framework's request object""" - ... - - -def load_persisted_query(doc_id: str) -> str: - """Implement this load a query by document ID. For example, from a database.""" - ... - - -class PersistedQuery(Extension): - def on_request_start(self): - request = self.execution_context.context.request - - doc_id = get_doc_id(request) - - self.execution_context.query = load_persisted_query(doc_id) -``` - -Contributed by [James Thorniley](https://github.com/jthorniley) via [PR #2431](https://github.com/strawberry-graphql/strawberry/pull/2431/) - - -0.151.3 - 2023-01-09 --------------------- - -This release adds support for FastAPI 0.89.0 - -Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #2440](https://github.com/strawberry-graphql/strawberry/pull/2440/) - - -0.151.2 - 2022-12-23 --------------------- - -This release fixes `@strawberry.experimental.pydantic.type` and adds support for the metadata attribute on fields. - -Example: -```python -@strawberry.experimental.pydantic.type(model=User) -class UserType: - private: strawberry.auto = strawberry.field(metadata={"admin_only": True}) - public: strawberry.auto -``` - -Contributed by [Huy Z](https://github.com/huyz) via [PR #2415](https://github.com/strawberry-graphql/strawberry/pull/2415/) - - -0.151.1 - 2022-12-20 --------------------- - -This release fixes an issue that prevented using generic -that had a field of type enum. The following works now: - -```python -@strawberry.enum -class EstimatedValueEnum(Enum): - test = "test" - testtest = "testtest" - - -@strawberry.type -class EstimatedValue(Generic[T]): - value: T - type: EstimatedValueEnum - - -@strawberry.type -class Query: - @strawberry.field - def estimated_value(self) -> Optional[EstimatedValue[int]]: - return EstimatedValue(value=1, type=EstimatedValueEnum.test) -``` - -Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #2411](https://github.com/strawberry-graphql/strawberry/pull/2411/) - - -0.151.0 - 2022-12-13 --------------------- - -This PR adds a new `graphql_type` parameter to strawberry.field that allows you -to explicitly set the field type. This parameter will take preference over the -resolver return type and the class field type. - -For example: - -```python -@strawberry.type -class Query: - a: float = strawberry.field(graphql_type=str) - b = strawberry.field(graphql_type=int) - - @strawberry.field(graphql_type=float) - def c(self) -> str: - return "3.4" - - -schema = strawberry.Schema(Query) - -str( - schema -) == """ - type Query { - a: String! - b: Int! - c: Float! - } -""" -``` - -Contributed by [Jonathan Kim](https://github.com/jkimbo) via [PR #2313](https://github.com/strawberry-graphql/strawberry/pull/2313/) - - -0.150.1 - 2022-12-13 --------------------- - -Fixed field resolvers with nested generic return types -(e.g. `list`, `Optional`, `Union` etc) raising TypeErrors. -This means resolver factory methods can now be correctly type hinted. - -For example the below would previously error unless you ommited all the -type hints on `resolver_factory` and `actual_resolver` functions. -```python -from typing import Callable, Optional, Type, TypeVar - -import strawberry - - -@strawberry.type -class Cat: - name: str - - -T = TypeVar("T") - - -def resolver_factory(type_: Type[T]) -> Callable[[], Optional[T]]: - def actual_resolver() -> Optional[T]: - # load rows from database and cast to type etc - ... - - return actual_resolver - - -@strawberry.type -class Query: - cat: Cat = strawberry.field(resolver_factory(Cat)) - - -schema = strawberry.Schema(query=Query) -``` - -Contributed by [Tim OSullivan](https://github.com/invokermain) via [PR #1900](https://github.com/strawberry-graphql/strawberry/pull/1900/) - - -0.150.0 - 2022-12-13 --------------------- - -This release implements the ability to use custom caching for dataloaders. -It also allows to provide a `cache_key_fn` to the dataloader. This function -is used to generate the cache key for the dataloader. This is useful when -you want to use a custom hashing function for the cache key. - -Contributed by [Aman Choudhary](https://github.com/Techno-Tut) via [PR #2394](https://github.com/strawberry-graphql/strawberry/pull/2394/) - - -0.149.2 - 2022-12-09 --------------------- - -This release fixes support for generics in arguments, see the following example: - - ```python - T = TypeVar("T") - - - @strawberry.type - class Node(Generic[T]): - @strawberry.field - def data(self, arg: T) -> T: # `arg` is also generic - return arg - ``` - -Contributed by [A. Coady](https://github.com/coady) via [PR #2316](https://github.com/strawberry-graphql/strawberry/pull/2316/) - - -0.149.1 - 2022-12-09 --------------------- - -This release improves the performance of rich exceptions on custom scalars -by changing how frames are fetched from the call stack. -Before the change, custom scalars were using a CPU intensive call to the -`inspect` module to fetch frame info which could lead to serious CPU spikes. - -Contributed by [Paulo Amaral](https://github.com/paulopaixaoamaral) via [PR #2390](https://github.com/strawberry-graphql/strawberry/pull/2390/) - - -0.149.0 - 2022-12-09 --------------------- - -This release does some internal refactoring of the HTTP views, hopefully it -doesn't affect anyone. It mostly changes the status codes returned in case of -errors (e.g. bad JSON, missing queries and so on). - -It also improves the testing, and adds an entirely new test suite for the HTTP -views, this means in future we'll be able to keep all the HTTP views in sync -feature-wise. - -Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #1840](https://github.com/strawberry-graphql/strawberry/pull/1840/) - - -0.148.0 - 2022-12-08 --------------------- - -This release changes the `get_context`, `get_root_value` and `process_result` -methods of the Flask async view to be async functions. This allows you to use -async code in these methods. - -Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #2388](https://github.com/strawberry-graphql/strawberry/pull/2388/) - - -0.147.0 - 2022-12-08 --------------------- - -This release introduces a `encode_json` method on all the HTTP integrations. -This method allows to customize the encoding of the JSON response. By default we -use `json.dumps` but you can override this method to use a different encoder. - -It also deprecates `json_encoder` and `json_dumps_params` in the Django and -Sanic views, `encode_json` should be used instead. - -Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #2272](https://github.com/strawberry-graphql/strawberry/pull/2272/) - - -0.146.0 - 2022-12-05 --------------------- - -This release updates the Sanic integration and includes some breaking changes. -You might need to update your code if you are customizing `get_context` or -`process_result` - -## `get_context` - -`get_context` now receives the request as the first argument and the response as -the second argument. - -## `process_result` - -`process_result` is now async and receives the request and the GraphQL execution -result. - -This change is needed to align all the HTTP integrations and reduce the amount -of code needed to maintain. It also makes the errors consistent with other -integrations. - -It also brings a **new feature** and it allows to customize the HTTP status code -by using `info.context["response"].status_code = YOUR_CODE`. - -It also removes the upper bound on the Sanic version, so you can use the latest -version of Sanic with Strawberry. - -Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #2273](https://github.com/strawberry-graphql/strawberry/pull/2273/) - - -0.145.0 - 2022-12-04 --------------------- - -This release introduced improved errors! Now, when you have a syntax error in -your code, you'll get a nice error message with a line number and a pointer to -the exact location of the error. โœจ - -This is a huge improvement over the previous behavior, which was providing a -stack trace with no clear indication of where the error was. ๐Ÿ™ˆ - -You can enable rich errors by installing Strawberry with the `cli` extra: - -```bash -pip install "strawberry-graphql[cli]" -``` - -Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #2027](https://github.com/strawberry-graphql/strawberry/pull/2027/) - - -0.144.3 - 2022-12-04 --------------------- - -This release fixes an issue with type duplication of generics. - -You can now use a lazy type with a generic even if -the original type was already used with that generic in the schema. - -Example: - -```python3 -@strawberry.type -class Query: - regular: Edge[User] - lazy: Edge[Annotated["User", strawberry.lazy(".user")]] -``` - -Contributed by [Dmitry Semenov](https://github.com/lonelyteapot) via [PR #2381](https://github.com/strawberry-graphql/strawberry/pull/2381/) - - -0.144.2 - 2022-12-02 --------------------- - -Generic types are now allowed in the schema's extra types. -```python -T = TypeVar("T") - - -@strawberry.type -class Node(Generic[T]): - field: T - - -@strawberry.type -class Query: - name: str - - -schema = strawberry.Schema(Query, types=[Node[int]]) -``` - -Contributed by [A. Coady](https://github.com/coady) via [PR #2294](https://github.com/strawberry-graphql/strawberry/pull/2294/) - - -0.144.1 - 2022-12-02 --------------------- - -This release fixes a regression that prevented Generic types -from being used multiple types. - -Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #2378](https://github.com/strawberry-graphql/strawberry/pull/2378/) - - -0.144.0 - 2022-12-02 --------------------- - -Added extra validation that types used in a schema are unique. -Strawberry starts to throw an exception `DuplicatedTypeName` when two types defined in a schema have the same name. - -Contributed by [Bartosz Polnik](https://github.com/bartekbp) via [PR #2356](https://github.com/strawberry-graphql/strawberry/pull/2356/) - - -0.143.0 - 2022-12-01 --------------------- - -Added an error to be used when overriding GraphQLError in custom extensions and added a guide on how to use it. -Exposing GraphQLError from the strawberry namespace brings a better experience and will be useful in the future (when we move to something else). - -Contributed by [Niten Nashiki](https://github.com/nnashiki) via [PR #2360](https://github.com/strawberry-graphql/strawberry/pull/2360/) - - -0.142.3 - 2022-11-29 --------------------- - -This release updates GraphiQL to 2.2.0 and fixes an issue with the websocket URL -being incorrectly set when navigating to GraphiQL with an URL with a hash. - -Contributed by [Shen Li](https://github.com/ericls) via [PR #2363](https://github.com/strawberry-graphql/strawberry/pull/2363/) - - -0.142.2 - 2022-11-15 --------------------- - -This release changes the dataloader batch resolution to avoid resolving -futures that were canceled, and also from reusing them from the cache. -Trying to resolve a future that was canceled would raise `asyncio.InvalidStateError` - -Contributed by [Thiago Bellini Ribeiro](https://github.com/bellini666) via [PR #2339](https://github.com/strawberry-graphql/strawberry/pull/2339/) - - -0.142.1 - 2022-11-11 --------------------- - -This release fixes a bug where using a custom scalar in a union would result -in an unclear exception. Instead, when using a custom scalar in a union, -the `InvalidUnionType` exception is raised with a clear message that you -cannot use that type in a union. - -Contributed by [Jonathan Kim](https://github.com/jkimbo) via [PR #2336](https://github.com/strawberry-graphql/strawberry/pull/2336/) - - -0.142.0 - 2022-11-11 --------------------- - -This release adds support for `typing.Self` and `typing_extensions.Self` for types and interfaces. - -```python -from typing_extensions import Self - - -@strawberry.type -class Node: - @strawberry.field - def field(self) -> Self: - return self -``` - -Contributed by [A. Coady](https://github.com/coady) via [PR #2295](https://github.com/strawberry-graphql/strawberry/pull/2295/) - - -0.141.0 - 2022-11-10 --------------------- - -This release adds support for an implicit `resolve_reference` method -on Federation type. This method will automatically create a Strawberry -instance for a federation type based on the input data received, for -example, the following: - -```python -@strawberry.federation.type(keys=["id"]) -class Something: - id: str - - -@strawberry.federation.type(keys=["upc"]) -class Product: - upc: str - something: Something - - @staticmethod - def resolve_reference(**data): - return Product(upc=data["upc"], something=Something(id=data["something_id"])) -``` - -doesn't need the resolve_reference method anymore. - -Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #2332](https://github.com/strawberry-graphql/strawberry/pull/2332/) - - -0.140.3 - 2022-11-09 --------------------- - -[Internal] Update StrawberryField so that `type_annotation` is always an instance of StrawberryAnnotation. - -Contributed by [Jonathan Kim](https://github.com/jkimbo) via [PR #2319](https://github.com/strawberry-graphql/strawberry/pull/2319/) - - -0.140.2 - 2022-11-08 --------------------- - -This release fixes an issue that prevented using enums that -were using strawberry.enum_value, like the following example: - -```python -from enum import Enum -import strawberry - - -@strawberry.enum -class TestEnum(Enum): - A = strawberry.enum_value("A") - B = "B" - - -@strawberry.type -class Query: - @strawberry.field - def receive_enum(self, test: TestEnum) -> int: - return 0 - - -schema = strawberry.Schema(query=Query) -``` - -Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #2306](https://github.com/strawberry-graphql/strawberry/pull/2306/) - - -0.140.1 - 2022-11-08 --------------------- - -This release adds logging back for parsing and validation errors that was -accidentally removed in v0.135.0. - -Contributed by [Jonathan Kim](https://github.com/jkimbo) via [PR #2323](https://github.com/strawberry-graphql/strawberry/pull/2323/) - - -0.140.0 - 2022-11-07 --------------------- - -This release allows to disable operation logging when running the debug server. - -``` -strawberry server demo --log-operations False -``` - -Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #2310](https://github.com/strawberry-graphql/strawberry/pull/2310/) - - -0.139.0 - 2022-11-04 --------------------- - -This release changes the type resolution priority to prefer the field annotation over the resolver return type. - -```python -def my_resolver() -> str: - return "1.33" - - -@strawberry.type -class Query: - a: float = strawberry.field(resolver=my_resolver) - - -schema = strawberry.Schema(Query) - -# Before: -str( - schema -) == """ -type Query { - a: String! -} -""" - -# After: -str( - schema -) == """ -type Query { - a: Float! -} -""" -``` - -Contributed by [Jonathan Kim](https://github.com/jkimbo) via [PR #2312](https://github.com/strawberry-graphql/strawberry/pull/2312/) - - -0.138.2 - 2022-11-04 --------------------- - -Fix Pydantic integration for Python 3.10.0 (which was missing the `kw_only` -parameter for `dataclasses.make_dataclass()`). - -Contributed by [Jonathan Kim](https://github.com/jkimbo) via [PR #2309](https://github.com/strawberry-graphql/strawberry/pull/2309/) - - -0.138.1 - 2022-10-31 --------------------- - -This release changes an internal implementation for FastAPI's -GraphQL router. This should reduce overhead when using the context, -and it shouldn't affect your code. - -Contributed by [Kristjรกn Valur Jรณnsson](https://github.com/kristjanvalur) via [PR #2278](https://github.com/strawberry-graphql/strawberry/pull/2278/) - - -0.138.0 - 2022-10-31 --------------------- - -This release adds support for generic in arguments, see the following example: - -```python -T = TypeVar("T") - - -@strawberry.type -class Node(Generic[T]): - @strawberry.field - def data(self, arg: T) -> T: # `arg` is also generic - return arg -``` - -Contributed by [A. Coady](https://github.com/coady) via [PR #2293](https://github.com/strawberry-graphql/strawberry/pull/2293/) - - -0.137.1 - 2022-10-24 --------------------- - -Allowed `CustomScalar | None` syntax for python >= 3.10. - -Contributed by [Guillaume Andreu Sabater](https://github.com/g-as) via [PR #2279](https://github.com/strawberry-graphql/strawberry/pull/2279/) - - -0.137.0 - 2022-10-21 --------------------- - -This release fixes errors when using Union-of-lazy-types - -Contributed by [Paulo Costa](https://github.com/paulo-raca) via [PR #2271](https://github.com/strawberry-graphql/strawberry/pull/2271/) - - -0.136.0 - 2022-10-21 --------------------- - -This release refactors the chalice integration in order to keep it consistent with -the other integrations. - -## Deprecation: - -Passing `render_graphiql` is now deprecated, please use `graphiql` instead. - -## New features: - -- You can now return a custom status by using `info.context["response"].status_code = 418` -- You can enabled/disable queries via get using `allow_queries_via_get` (defaults to `True`) - -## Changes: - -Trying to access /graphql via a browser and with `graphiql` set to `False` will return a 404. - -Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #2266](https://github.com/strawberry-graphql/strawberry/pull/2266/) - - -0.135.0 - 2022-10-21 --------------------- - -This release adds a new `MaskErrors` extension that can be used to hide error -messages from the client to prevent exposing sensitive details. By default it -masks all errors raised in any field resolver. - -```python -import strawberry -from strawberry.extensions import MaskErrors - -schema = strawberry.Schema( - Query, - extensions=[ - MaskErrors(), - ], -) -``` - -Contributed by [Jonathan Kim](https://github.com/jkimbo) via [PR #2248](https://github.com/strawberry-graphql/strawberry/pull/2248/) - - -0.134.5 - 2022-10-20 --------------------- - -This release improves the error message that you get when trying -to use an enum that hasn't been decorated with `@strawberry.enum` -inside a type's field. - -Contributed by [Rise Riyo](https://github.com/riseriyo) via [PR #2267](https://github.com/strawberry-graphql/strawberry/pull/2267/) - - -0.134.4 - 2022-10-20 --------------------- - -This release adds support for printing schema directives on an input type object, for example the following schema: - -```python -@strawberry.schema_directive(locations=[Location.INPUT_FIELD_DEFINITION]) -class RangeInput: - min: int - max: int - - -@strawberry.input -class CreateUserInput: - name: str - age: int = strawberry.field(directives=[RangeInput(min=1, max=100)]) -``` - -prints the following: - -```graphql -directive @rangeInput(min: Int!, max: Int!) on INPUT_FIELD_DEFINITION - -input Input @sensitiveInput(reason: "GDPR") { - firstName: String! - age: Int! @rangeInput(min: 1, max: 100) -} -``` - -Contributed by [Etty](https://github.com/estyxx) via [PR #2233](https://github.com/strawberry-graphql/strawberry/pull/2233/) - - -0.134.3 - 2022-10-16 --------------------- - -This release fixes an issue that prevented using strawberry.lazy with relative paths. - -The following should work now: - -```python -@strawberry.type -class TypeA: - b: Annotated["TypeB", strawberry.lazy(".type_b")] -``` - -Contributed by [Paulo Costa](https://github.com/paulo-raca) via [PR #2244](https://github.com/strawberry-graphql/strawberry/pull/2244/) - - -0.134.2 - 2022-10-16 --------------------- - -This release adds pyupgrade to our CI and includes some minor changes to keep our codebase modern. - -Contributed by [Liel Fridman](https://github.com/lielfr) via [PR #2255](https://github.com/strawberry-graphql/strawberry/pull/2255/) - - -0.134.1 - 2022-10-14 --------------------- - -This release fixes an issue that prevented using lazy types inside -generic types. - -The following is now allowed: - -```python -T = TypeVar("T") - -TypeAType = Annotated["TypeA", strawberry.lazy("tests.schema.test_lazy.type_a")] - - -@strawberry.type -class Edge(Generic[T]): - node: T - - -@strawberry.type -class Query: - users: Edge[TypeAType] -``` - -Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #2254](https://github.com/strawberry-graphql/strawberry/pull/2254/) - - -0.134.0 - 2022-10-14 --------------------- - -These release allow you to define a different `url` in the `GraphQLTestClient`, the default is "/graphql/". - -Here's an example with Starlette client: -```python -import pytest - -from starlette.testclient import TestClient -from strawberry.asgi.test import GraphQLTestClient - - -@pytest.fixture -def graphql_client() -> GraphQLTestClient: - return GraphQLTestClient( - TestClient(app, base_url="http://localhost:8000"), url="/api/" - ) -``` - -Contributed by [Etty](https://github.com/estyxx) via [PR #2238](https://github.com/strawberry-graphql/strawberry/pull/2238/) - - -0.133.7 - 2022-10-14 --------------------- - -This release fixes a type issue when passing `scalar_overrides` to `strawberry.Schema` - -Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #2251](https://github.com/strawberry-graphql/strawberry/pull/2251/) - - -0.133.6 - 2022-10-13 --------------------- - -Fix support for arguments where `arg.type=LazyType["EnumType"]` - -Contributed by [Paulo Costa](https://github.com/paulo-raca) via [PR #2245](https://github.com/strawberry-graphql/strawberry/pull/2245/) - - -0.133.5 - 2022-10-03 --------------------- - -Updated `unset` import, from `strawberry.arguments` to `strawberry.unset` in codebase. - -This will prevent strawberry from triggering its own warning on deprecated imports. - -Contributed by [dependabot](https://github.com/dependabot) via [PR #2219](https://github.com/strawberry-graphql/strawberry/pull/2219/) - - -0.133.4 - 2022-10-03 --------------------- - -This release fixes the type of strawberry.federation.field, -this will prevent errors from mypy and pyright when doing the following: - -```python -@strawberry.federation.type(keys=["id"]) -class Location: - id: strawberry.ID - - # the following field was reporting an error in mypy and pylance - celestial_body: CelestialBody = strawberry.federation.field( - resolver=resolve_celestial_body - ) -``` - -Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #2222](https://github.com/strawberry-graphql/strawberry/pull/2222/) - - -0.133.3 - 2022-10-03 --------------------- - -This release allows to create a federation schema without having to pass a -`Query` type. This is useful when your schema only extends some types without -adding any additional root field. - -```python -@strawberry.federation.type(keys=["id"]) -class Location: - id: strawberry.ID - name: str = strawberry.federation.field(override="start") - - -schema = strawberry.federation.Schema(types=[Location]) -``` - -Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #2220](https://github.com/strawberry-graphql/strawberry/pull/2220/) - - -0.133.2 - 2022-09-30 --------------------- - -This release fixes an issue with `strawberry.federation.field` that -prevented instantiating field when passing a resolver function. - -Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #2218](https://github.com/strawberry-graphql/strawberry/pull/2218/) - - -0.133.1 - 2022-09-28 --------------------- - -This release fixes an issue that prevented using `strawberry.field` with -`UNSET` as the default value. - -Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #2128](https://github.com/strawberry-graphql/strawberry/pull/2128/) - - -0.133.0 - 2022-09-27 --------------------- - -Reduce the number of required dependencies, by marking Pygments and python-multipart as optional. These dependencies are still necessary for some functionality, and so users of that functionality need to ensure they're installed, either explicitly or via an extra: - -- Pygments is still necessary when using Strawberry in debug mode, and is included in the `strawberry[debug-server]` extra. -- python-multipart is still necessary when using `strawberry.file_uploads.Upload` with FastAPI or Starlette, and is included in the `strawberry[fastapi]` and `strawberry[asgi]` extras, respectively. - -There is now also the `strawberry[cli]` extra to support commands like `strawberry codegen` and `strawberry export-schema`. - -Contributed by [Huon Wilson](https://github.com/huonw) via [PR #2205](https://github.com/strawberry-graphql/strawberry/pull/2205/) - - -0.132.1 - 2022-09-23 --------------------- - -Improve resolving performance by avoiding extra calls for basic fields. - -This change improves performance of resolving a query by skipping `Info` -creation and permission checking for fields that don't have a resolver -or permission classes. In local benchmarks it improves performance of large -results by ~14%. - -Contributed by [Jonathan Kim](https://github.com/jkimbo) via [PR #2194](https://github.com/strawberry-graphql/strawberry/pull/2194/) - - -0.132.0 - 2022-09-23 --------------------- - -Support storing metadata in strawberry fields. - -Contributed by [Paulo Costa](https://github.com/paulo-raca) via [PR #2190](https://github.com/strawberry-graphql/strawberry/pull/2190/) - - -0.131.5 - 2022-09-22 --------------------- - -Fixes false positives with the mypy plugin. -Happened when `to_pydantic` was called on a type that was converted -pydantic with all_fields=True. - -Also fixes the type signature when `to_pydantic` is defined by the user. - -```python -from pydantic import BaseModel -from typing import Optional -import strawberry - - -class MyModel(BaseModel): - email: str - password: Optional[str] - - -@strawberry.experimental.pydantic.input(model=MyModel, all_fields=True) -class MyModelStrawberry: - ... - - -MyModelStrawberry(email="").to_pydantic() -# previously would complain wrongly about missing email and password -``` - -Contributed by [James Chua](https://github.com/thejaminator) via [PR #2017](https://github.com/strawberry-graphql/strawberry/pull/2017/) - - -0.131.4 - 2022-09-22 --------------------- - -This release updates the mypy plugin and the typing for Pyright to treat all -strawberry fields as keyword-only arguments. This reflects a previous change to -the Strawberry API. - -Contributed by [Paulo Costa](https://github.com/paulo-raca) via [PR #2191](https://github.com/strawberry-graphql/strawberry/pull/2191/) - - -0.131.3 - 2022-09-22 --------------------- - -Bug fix: Do not force kw-only=False in fields specified with strawberry.field() - -Contributed by [Paulo Costa](https://github.com/paulo-raca) via [PR #2189](https://github.com/strawberry-graphql/strawberry/pull/2189/) - - -0.131.2 - 2022-09-22 --------------------- - -This release fixes a small issue that might happen when -uploading files and not passing the operations object. - -Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #2192](https://github.com/strawberry-graphql/strawberry/pull/2192/) - - -0.131.1 - 2022-09-16 --------------------- - -Fix warnings during unit tests for Sanic's upload. - -Otherwise running unit tests results in a bunch of warning like this: - -``` -DeprecationWarning: Use 'content=<...>' to upload raw bytes/text content. -``` - -Contributed by [Paulo Costa](https://github.com/paulo-raca) via [PR #2178](https://github.com/strawberry-graphql/strawberry/pull/2178/) - - -0.131.0 - 2022-09-15 --------------------- - -This release improves the dataloader class with new features: - -- Explicitly cache invalidation, prevents old data from being fetched after a mutation -- Importing data into the cache, prevents unnecessary load calls if the data has already been fetched by other means. - -Contributed by [Paulo Costa](https://github.com/paulo-raca) via [PR #2149](https://github.com/strawberry-graphql/strawberry/pull/2149/) - - -0.130.4 - 2022-09-14 --------------------- - -This release adds improved support for Pyright and Pylance, VSCode default -language server for Python. - -Using `strawberry.type`, `strawberry.field`, `strawberry.input` and -`strawberry.enum` will now be correctly recognized by Pyright and Pylance and -won't show errors. - -Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #2172](https://github.com/strawberry-graphql/strawberry/pull/2172/) - - -0.130.3 - 2022-09-12 --------------------- - -Fix invalid deprecation warning issued on arguments annotated -by a subclassed `strawberry.types.Info`. - -Thanks to @ThirVondukr for the bug report! - -Example: - -```python -class MyInfo(Info): - pass - - -@strawberry.type -class Query: - @strawberry.field - def is_tasty(self, info: MyInfo) -> bool: - """Subclassed ``info`` argument no longer raises deprecation warning.""" -``` - -Contributed by [San Kilkis](https://github.com/skilkis) via [PR #2137](https://github.com/strawberry-graphql/strawberry/pull/2137/) - - -0.130.2 - 2022-09-12 --------------------- - -This release fixes the conversion of generic aliases when -using pydantic. - -Contributed by [Silas Sewell](https://github.com/silas) via [PR #2152](https://github.com/strawberry-graphql/strawberry/pull/2152/) - - -0.130.1 - 2022-09-12 --------------------- - -Fix version parsing issue related to dev builds of Mypy in `strawberry.ext.mypy_plugin` - -Contributed by [San Kilkis](https://github.com/skilkis) via [PR #2157](https://github.com/strawberry-graphql/strawberry/pull/2157/) - - -0.130.0 - 2022-09-12 --------------------- - -Convert Tuple and Sequence types to GraphQL list types. - -Example: - -```python -from collections.abc import Sequence -from typing import Tuple - - -@strawberry.type -class User: - pets: Sequence[Pet] - favourite_ice_cream_flavours: Tuple[IceCreamFlavour] -``` - -Contributed by [Jonathan Kim](https://github.com/jkimbo) via [PR #2164](https://github.com/strawberry-graphql/strawberry/pull/2164/) - - -0.129.0 - 2022-09-11 --------------------- - -This release adds `strawberry.lazy` which allows you to define the type of the -field and its path. This is useful when you want to define a field with a type -that has a circular dependency. - -For example, let's say we have a `User` type that has a list of `Post` and a -`Post` type that has a `User`: - -```python -# posts.py -from typing import TYPE_CHECKING, Annotated - -import strawberry - -if TYPE_CHECKING: - from .users import User - - -@strawberry.type -class Post: - title: str - author: Annotated["User", strawberry.lazy(".users")] -``` - -```python -# users.py -from typing import TYPE_CHECKING, Annotated, List - -import strawberry - -if TYPE_CHECKING: - from .posts import Post - - -@strawberry.type -class User: - name: str - posts: List[Annotated["Post", strawberry.lazy(".posts")]] -``` - -Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #2158](https://github.com/strawberry-graphql/strawberry/pull/2158/) - - -0.128.0 - 2022-09-05 --------------------- - -This release changes how dataclasses are created to make use of the new -`kw_only` argument in Python 3.10 so that fields without a default value can now -follow a field with a default value. This feature is also backported to all other -supported Python versions. - -More info: https://docs.python.org/3/library/dataclasses.html#dataclasses.dataclass - -For example: - -```python -# This no longer raises a TypeError - - -@strawberry.type -class MyType: - a: str = "Hi" - b: int -``` - -โš ๏ธ This is a breaking change! Whenever instantiating a Strawberry type make sure -that you only pass values are keyword arguments: - -```python -# Before: - -MyType("foo", 3) - -# After: - -MyType(a="foo", b=3) -``` - -Contributed by [Jonathan Kim](https://github.com/jkimbo) via [PR #1187](https://github.com/strawberry-graphql/strawberry/pull/1187/) - - -0.127.4 - 2022-08-31 --------------------- - -This release fixes a bug in the subscription clean up when subscribing using the -graphql-transport-ws protocol, which could occasionally cause a 'finally' -statement within the task to not get run, leading to leaked resources. - -Contributed by [rjwills28](https://github.com/rjwills28) via [PR #2141](https://github.com/strawberry-graphql/strawberry/pull/2141/) - - -0.127.3 - 2022-08-30 --------------------- - -This release fixes a couple of small styling issues with -the GraphiQL explorer - -Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #2143](https://github.com/strawberry-graphql/strawberry/pull/2143/) - - -0.127.2 - 2022-08-30 --------------------- - -This release adds support for passing schema directives to -`Schema(..., types=[])`. This can be useful if using a built-inschema directive -that's not supported by a gateway. - -For example the following: - -```python -import strawberry -from strawberry.scalars import JSON -from strawberry.schema_directive import Location - - -@strawberry.type -class Query: - example: JSON - - -@strawberry.schema_directive(locations=[Location.SCALAR], name="specifiedBy") -class SpecifiedBy: - name: str - - -schema = strawberry.Schema(query=Query, types=[SpecifiedBy]) -``` - -will print the following SDL: - -```graphql -directive @specifiedBy(name: String!) on SCALAR - -""" -The `JSON` scalar type represents JSON values as specified by [ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf). -""" -scalar JSON - @specifiedBy( - url: "http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf" - ) - -type Query { - example: JSON! -} -``` - -Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #2140](https://github.com/strawberry-graphql/strawberry/pull/2140/) - - -0.127.1 - 2022-08-30 --------------------- - -This release fixes an issue with the updated GraphiQL -interface. - -Contributed by [Doctor](https://github.com/ThirVondukr) via [PR #2138](https://github.com/strawberry-graphql/strawberry/pull/2138/) - - -0.127.0 - 2022-08-29 --------------------- - -This release updates the built-in GraphiQL version to version 2.0, -which means you can now enjoy all the new features that come with the latest version! - -Contributed by [Matt Exact](https://github.com/MattExact) via [PR #1889](https://github.com/strawberry-graphql/strawberry/pull/1889/) - - -0.126.2 - 2022-08-23 --------------------- - -This release restricts the `backports.cached_property` dependency to only be -installed when Python < 3.8. Since version 3.8 `cached_property` is included -in the builtin `functools`. The code is updated to use the builtin version -when Python >= 3.8. - -Contributed by [ljnsn](https://github.com/ljnsn) via [PR #2114](https://github.com/strawberry-graphql/strawberry/pull/2114/) - - -0.126.1 - 2022-08-22 --------------------- - -Keep extra discovered types sorted so that each schema printing is -always the same. - -Contributed by [Thiago Bellini Ribeiro](https://github.com/bellini666) via [PR #2115](https://github.com/strawberry-graphql/strawberry/pull/2115/) - - -0.126.0 - 2022-08-18 --------------------- - -This release adds support for adding descriptions to enum values. - -### Example - - -```python -@strawberry.enum -class IceCreamFlavour(Enum): - VANILLA = strawberry.enum_value("vanilla") - STRAWBERRY = strawberry.enum_value( - "strawberry", - description="Our favourite", - ) - CHOCOLATE = "chocolate" - - -@strawberry.type -class Query: - favorite_ice_cream: IceCreamFlavour = IceCreamFlavour.STRAWBERRY - - -schema = strawberry.Schema(query=Query) -``` - -This produces the following schema - -```graphql -enum IceCreamFlavour { - VANILLA - - """Our favourite.""" - STRAWBERRY - CHOCOLATE -} - -type Query { - favoriteIceCream: IceCreamFlavour! -} -``` - -Contributed by [Felipe Gonzalez](https://github.com/gonzalezzfelipe) via [PR #2106](https://github.com/strawberry-graphql/strawberry/pull/2106/) - - -0.125.1 - 2022-08-16 --------------------- - -This release hides `resolvable: True` in @keys directives -when using Apollo Federation 1, to preserve compatibility -with older Gateways. - -Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #2099](https://github.com/strawberry-graphql/strawberry/pull/2099/) - - -0.125.0 - 2022-08-12 --------------------- - -This release adds an integration with Django Channels. The integration will -allow you to use GraphQL subscriptions via Django Channels. - -Contributed by [Dan Sloan](https://github.com/LucidDan) via [PR #1407](https://github.com/strawberry-graphql/strawberry/pull/1407/) - - -0.124.0 - 2022-08-08 --------------------- - -This release adds full support for Apollo Federation 2.0. To opt-in you need to -pass `enable_federation_2=True` to `strawberry.federation.Schema`, like in the -following example: - -```python -@strawberry.federation.type(keys=["id"]) -class User: - id: strawberry.ID - - -@strawberry.type -class Query: - user: User - - -schema = strawberry.federation.Schema(query=Query, enable_federation_2=True) -``` - -This release also improves type checker support for the federation. - -Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #2047](https://github.com/strawberry-graphql/strawberry/pull/2047/) - - -0.123.3 - 2022-08-02 --------------------- - -This release fixes a regression introduced in version 0.118.2 which was -preventing using circular dependencies in Strawberry django and Strawberry -django plus. - -Contributed by [Thiago Bellini Ribeiro](https://github.com/bellini666) via [PR #2062](https://github.com/strawberry-graphql/strawberry/pull/2062/) - - -0.123.2 - 2022-08-01 --------------------- - -This release adds support for priting custom enums used only on -schema directives, for example the following schema: - -```python -@strawberry.enum -class Reason(str, Enum): - EXAMPLE = "example" - - -@strawberry.schema_directive(locations=[Location.FIELD_DEFINITION]) -class Sensitive: - reason: Reason - - -@strawberry.type -class Query: - first_name: str = strawberry.field(directives=[Sensitive(reason=Reason.EXAMPLE)]) -``` - -prints the following: - -```graphql -directive @sensitive(reason: Reason!) on FIELD_DEFINITION - -type Query { - firstName: String! @sensitive(reason: EXAMPLE) -} - -enum Reason { - EXAMPLE -} -``` - -while previously it would omit the definition of the enum. - -Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #2059](https://github.com/strawberry-graphql/strawberry/pull/2059/) - - -0.123.1 - 2022-08-01 --------------------- - -This release adds support for priting custom scalar used only on -schema directives, for example the following schema: - -```python -SensitiveConfiguration = strawberry.scalar(str, name="SensitiveConfiguration") - - -@strawberry.schema_directive(locations=[Location.FIELD_DEFINITION]) -class Sensitive: - config: SensitiveConfiguration - - -@strawberry.type -class Query: - first_name: str = strawberry.field(directives=[Sensitive(config="Some config")]) -``` - -prints the following: - -```graphql -directive @sensitive(config: SensitiveConfiguration!) on FIELD_DEFINITION - -type Query { - firstName: String! @sensitive(config: "Some config") -} - -scalar SensitiveConfiguration -``` - -while previously it would omit the definition of the scalar. - -Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #2058](https://github.com/strawberry-graphql/strawberry/pull/2058/) - - -0.123.0 - 2022-08-01 --------------------- - -This PR adds support for adding schema directives to the schema of -your GraphQL API. For printing the following schema: - -```python -@strawberry.schema_directive(locations=[Location.SCHEMA]) -class Tag: - name: str - - -@strawberry.type -class Query: - first_name: str = strawberry.field(directives=[Tag(name="team-1")]) - - -schema = strawberry.Schema(query=Query, schema_directives=[Tag(name="team-1")]) -``` - -will print the following: - -```graphql -directive @tag(name: String!) on SCHEMA - -schema @tag(name: "team-1") { - query: Query -} - -type Query { - firstName: String! -} -""" -``` - -Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #2054](https://github.com/strawberry-graphql/strawberry/pull/2054/) - - -0.122.1 - 2022-07-31 --------------------- - -This release fixes that the AIOHTTP integration ignored the `operationName` of query -operations. This behaviour is a regression introduced in version 0.107.0. - -Contributed by [Jonathan Ehwald](https://github.com/DoctorJohn) via [PR #2055](https://github.com/strawberry-graphql/strawberry/pull/2055/) - - -0.122.0 - 2022-07-29 --------------------- - -This release adds support for printing default values for scalars like JSON. - -For example the following: - -```python -import strawberry -from strawberry.scalars import JSON - - -@strawberry.input -class MyInput: - j: JSON = strawberry.field(default_factory=dict) - j2: JSON = strawberry.field(default_factory=lambda: {"hello": "world"}) -``` - -will print the following schema: - -```graphql -input MyInput { - j: JSON! = {} - j2: JSON! = {hello: "world"} -} -``` - -Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #2048](https://github.com/strawberry-graphql/strawberry/pull/2048/) - - -0.121.1 - 2022-07-27 --------------------- - -This release adds a backward compatibility layer with libraries -that specify a custom `get_result`. - -Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #2038](https://github.com/strawberry-graphql/strawberry/pull/2038/) - - -0.121.0 - 2022-07-23 --------------------- - -This release adds support for overriding the default resolver for fields. - -Currently the default resolver is `getattr`, but now you can change it to any -function you like, for example you can allow returning dictionaries: - -```python -@strawberry.type -class User: - name: str - - -@strawberry.type -class Query: - @strawberry.field - def user(self) -> User: - return {"name": "Patrick"} # type: ignore - - -schema = strawberry.Schema( - query=Query, - config=StrawberryConfig(default_resolver=getitem), -) - -query = "{ user { name } }" - -result = schema.execute_sync(query) -``` - -Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #2037](https://github.com/strawberry-graphql/strawberry/pull/2037/) - - -0.120.0 - 2022-07-23 --------------------- - -This release add a new `DatadogTracingExtension` that can be used to instrument -your application with Datadog. - -```python -import strawberry - -from strawberry.extensions.tracing import DatadogTracingExtension - -schema = strawberry.Schema( - Query, - extensions=[ - DatadogTracingExtension, - ], -) -``` - -Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #2001](https://github.com/strawberry-graphql/strawberry/pull/2001/) - - -0.119.2 - 2022-07-23 --------------------- - -Fixed edge case where `Union` types raised an `UnallowedReturnTypeForUnion` -error when returning the correct type from the resolver. This also improves -performance of StrawberryUnion's `_resolve_union_type` from `O(n)` to `O(1)` in -the majority of cases where `n` is the number of types in the schema. - -For -[example the below](https://play.strawberry.rocks/?gist=f7d88898d127e65b12140fdd763f9ef2)) -would previously raise the error when querying `two` as `StrawberryUnion` would -incorrectly determine that the resolver returns `Container[TypeOne]`. - -```python -import strawberry -from typing import TypeVar, Generic, Union, List, Type - -T = TypeVar("T") - - -@strawberry.type -class Container(Generic[T]): - items: List[T] - - -@strawberry.type -class TypeOne: - attr: str - - -@strawberry.type -class TypeTwo: - attr: str - - -def resolver_one(): - return Container(items=[TypeOne("one")]) - - -def resolver_two(): - return Container(items=[TypeTwo("two")]) - - -@strawberry.type -class Query: - one: Union[Container[TypeOne], TypeOne] = strawberry.field(resolver_one) - two: Union[Container[TypeTwo], TypeTwo] = strawberry.field(resolver_two) - - -schema = strawberry.Schema(query=Query) -``` - -Contributed by [Tim OSullivan](https://github.com/invokermain) via [PR #2029](https://github.com/strawberry-graphql/strawberry/pull/2029/) - - -0.119.1 - 2022-07-18 --------------------- - -An explanatory custom exception is raised when union of GraphQL input types is attempted. - -Contributed by [Dhanshree Arora](https://github.com/DhanshreeA) via [PR #2019](https://github.com/strawberry-graphql/strawberry/pull/2019/) - - -0.119.0 - 2022-07-14 --------------------- - -This release changes when we add the custom directives extension, previously -the extension was always enabled, now it is only enabled if you pass custom -directives to `strawberry.Schema`. - -Contributed by [bomtall](https://github.com/bomtall) via [PR #2020](https://github.com/strawberry-graphql/strawberry/pull/2020/) - - -0.118.2 - 2022-07-14 --------------------- - -This release adds an initial fix to make `strawberry.auto` work when using -`from __future__ import annotations`. - -Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #1994](https://github.com/strawberry-graphql/strawberry/pull/1994/) - - -0.118.1 - 2022-07-14 --------------------- - -Fixes issue where users without pydantic were not able to use the mypy plugin. - -Contributed by [James Chua](https://github.com/thejaminator) via [PR #2016](https://github.com/strawberry-graphql/strawberry/pull/2016/) - - -0.118.0 - 2022-07-13 --------------------- - -You can now pass keyword arguments to `to_pydantic` -```python -from pydantic import BaseModel -import strawberry - - -class MyModel(BaseModel): - email: str - password: str - - -@strawberry.experimental.pydantic.input(model=MyModel) -class MyModelStrawberry: - email: strawberry.auto - # no password field here - - -MyModelStrawberry(email="").to_pydantic(password="hunter") -``` - -Also if you forget to pass password, mypy will complain - -```python -MyModelStrawberry(email="").to_pydantic() -# error: Missing named argument "password" for "to_pydantic" of "MyModelStrawberry" -``` - -Contributed by [James Chua](https://github.com/thejaminator) via [PR #2012](https://github.com/strawberry-graphql/strawberry/pull/2012/) - - -0.117.1 - 2022-07-07 --------------------- - -Allow to add alias to fields generated from pydantic with `strawberry.field(name="ageAlias")`. - -``` -class User(pydantic.BaseModel): - age: int - -@strawberry.experimental.pydantic.type(User) -class UserType: - age: strawberry.auto = strawberry.field(name="ageAlias") -``` - -Contributed by [Alex](https://github.com/benzolium) via [PR #1986](https://github.com/strawberry-graphql/strawberry/pull/1986/) - - -0.117.0 - 2022-07-06 --------------------- - -This release fixes an issue that required installing opentelemetry when -trying to use the ApolloTracing extension - -Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #1977](https://github.com/strawberry-graphql/strawberry/pull/1977/) - - -0.116.4 - 2022-07-04 --------------------- - -Fix regression caused by the new resolver argument handling mechanism -introduced in v0.115.0. This release restores the ability to use unhashable -default values in resolvers such as dict and list. See example below: - -```python -@strawberry.type -class Query: - @strawberry.field - def field(self, x: List[str] = ["foo"], y: JSON = {"foo": 42}) -> str: # noqa: B006 - return f"{x} {y}" -``` - -Thanks to @coady for the regression report! - -Contributed by [San Kilkis](https://github.com/skilkis) via [PR #1985](https://github.com/strawberry-graphql/strawberry/pull/1985/) - - -0.116.3 - 2022-07-04 --------------------- - -This release fixes the following error when trying to use Strawberry -with Apollo Federation: - -``` -Error: A valid schema couldn't be composed. The following composition errors were found: - [burro-api] Unknown type _FieldSet -``` - -Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #1988](https://github.com/strawberry-graphql/strawberry/pull/1988/) - - -0.116.2 - 2022-07-03 --------------------- - -Reimplement `StrawberryResolver.annotations` property after removal in v0.115. - -Library authors who previously relied on the public `annotations` property -can continue to do so after this fix. - -Contributed by [San Kilkis](https://github.com/skilkis) via [PR #1990](https://github.com/strawberry-graphql/strawberry/pull/1990/) - - -0.116.1 - 2022-07-03 --------------------- - -This release fixes a breaking internal error in mypy plugin for the following case. -- using positional arguments to pass a resolver for `strawberry.field()` or `strawberry.mutation()` - -```python -failed: str = strawberry.field(resolver) -successed: str = strawberry.field(resolver=resolver) -``` - -now mypy returns an error with `"field()" or "mutation()" only takes keyword arguments` message -rather than an internal error. - -Contributed by [cake-monotone](https://github.com/cake-monotone) via [PR #1987](https://github.com/strawberry-graphql/strawberry/pull/1987/) - - -0.116.0 - 2022-07-03 --------------------- - -This release adds a link from generated GraphQLCore types to the Strawberry type -that generated them. - -From a GraphQLCore type you can now access the Strawberry type by doing: - -```python -strawberry_type: TypeDefinition = graphql_core_type.extensions[ - GraphQLCoreConverter.DEFINITION_BACKREF -] -``` - -Contributed by [Paulo Costa](https://github.com/paulo-raca) via [PR #1766](https://github.com/strawberry-graphql/strawberry/pull/1766/) - - -0.115.0 - 2022-07-01 --------------------- - -This release changes how we declare the `info` argument in resolvers and the -`value` argument in directives. - -Previously we'd use the name of the argument to determine its value. Now we use -the type annotation of the argument to determine its value. - -Here's an example of how the old syntax works: - -```python -def some_resolver(info) -> str: - return info.context.get("some_key", "default") - - -@strawberry.type -class Example: - a_field: str = strawberry.resolver(some_resolver) -``` - -and here's an example of how the new syntax works: - -```python -from strawberry.types import Info - - -def some_resolver(info: Info) -> str: - return info.context.get("some_key", "default") - - -@strawberry.type -class Example: - a_field: str = strawberry.resolver(some_resolver) -``` - -This means that you can now use a different name for the `info` argument in your -resolver and the `value` argument in your directive. - -Here's an example that uses a custom name for both the value and the info -parameter in directives: - -```python -from strawberry.types import Info -from strawberry.directive import DirectiveLocation, DirectiveValue - - -@strawberry.type -class Cake: - frosting: Optional[str] = None - flavor: str = "Chocolate" - - -@strawberry.type -class Query: - @strawberry.field - def cake(self) -> Cake: - return Cake() - - -@strawberry.directive( - locations=[DirectiveLocation.FIELD], - description="Add frosting with ``value`` to a cake.", -) -def add_frosting(value: str, v: DirectiveValue[Cake], my_info: Info): - # Arbitrary argument name when using `DirectiveValue` is supported! - assert isinstance(v, Cake) - if ( - value in my_info.context["allergies"] - ): # Info can now be accessed from directives! - raise AllergyError("You are allergic to this frosting!") - else: - v.frosting = value # Value can now be used as a GraphQL argument name! - return v -``` - -**Note:** the old way of passing arguments by name is deprecated and will be -removed in future releases of Strawberry. - -Contributed by [San Kilkis](https://github.com/skilkis) via [PR #1713](https://github.com/strawberry-graphql/strawberry/pull/1713/) - - -0.114.7 - 2022-07-01 --------------------- - -Allow use of implicit `Any` in `strawberry.Private` annotated Generic types. - -For example the following is now supported: - -```python -from __future__ import annotations - -from typing import Generic, Sequence, TypeVar - -import strawberry - - -T = TypeVar("T") - - -@strawberry.type -class Foo(Generic[T]): - private_field: strawberry.Private[Sequence] # instead of Sequence[Any] - - -@strawberry.type -class Query: - @strawberry.field - def foo(self) -> Foo[str]: - return Foo(private_field=[1, 2, 3]) -``` - -See Issue [#1938](https://github.com/strawberry-graphql/strawberry/issues/1938) -for details. - -Contributed by [San Kilkis](https://github.com/skilkis) via [PR #1939](https://github.com/strawberry-graphql/strawberry/pull/1939/) - - -0.114.6 - 2022-06-30 --------------------- - -The federation decorator now allows for a list of additional arbitrary schema -directives extending the key/shareable directives used for federation. - -Example Python: - -```python -import strawberry -from strawberry.schema.config import StrawberryConfig -from strawberry.schema_directive import Location - - -@strawberry.schema_directive(locations=[Location.OBJECT]) -class CacheControl: - max_age: int - - -@strawberry.federation.type( - keys=["id"], shareable=True, extend=True, directives=[CacheControl(max_age=42)] -) -class FederatedType: - id: strawberry.ID - - -schema = strawberry.Schema(query=Query, config=StrawberryConfig(auto_camel_case=False)) -``` - -Resulting GQL Schema: - -```graphql -directive @CacheControl(max_age: Int!) on OBJECT -directive @key(fields: _FieldSet!, resolvable: Boolean) on OBJECT | INTERFACE -directive @shareable on FIELD_DEFINITION | OBJECT - -extend type FederatedType - @key(fields: "id") - @shareable - @CacheControl(max_age: 42) { - id: ID! -} - -type Query { - federatedType: FederatedType! -} -``` - -Contributed by [Jeffrey DeFond](https://github.com/defond0) via [PR #1945](https://github.com/strawberry-graphql/strawberry/pull/1945/) - - -0.114.5 - 2022-06-23 --------------------- - -This release adds support in Mypy for using strawberry.mutation -while passing a resolver, the following now doesn't make Mypy return -an error: - -```python -import strawberry - - -def set_name(self, name: str) -> None: - self.name = name - - -@strawberry.type -class Mutation: - set_name: None = strawberry.mutation(resolver=set_name) -``` - -Contributed by [Etty](https://github.com/estyxx) via [PR #1966](https://github.com/strawberry-graphql/strawberry/pull/1966/) - - -0.114.4 - 2022-06-23 --------------------- - -This release fixes the type annotation of `Response.errors` used in the `GraphQLTestClient` to be a `List` of `GraphQLFormattedError`. - -Contributed by [Etty](https://github.com/estyxx) via [PR #1961](https://github.com/strawberry-graphql/strawberry/pull/1961/) - - -0.114.3 - 2022-06-21 --------------------- - -This release fixes the type annotation of `Response.errors` used in the `GraphQLTestClient` to be a `List` of `GraphQLError`. - -Contributed by [Etty](https://github.com/estyxx) via [PR #1959](https://github.com/strawberry-graphql/strawberry/pull/1959/) - - -0.114.2 - 2022-06-15 --------------------- - -This release fixes an issue in the `GraphQLTestClient` when using both variables and files together. - -Contributed by [Etty](https://github.com/estyxx) via [PR #1576](https://github.com/strawberry-graphql/strawberry/pull/1576/) - - -0.114.1 - 2022-06-09 --------------------- - -Fix crash in Django's `HttpResponse.__repr__` by handling `status_code=None` in `TemporalHttpResponse.__repr__`. - -Contributed by [Daniel Hahler](https://github.com/blueyed) via [PR #1950](https://github.com/strawberry-graphql/strawberry/pull/1950/) - - -0.114.0 - 2022-05-27 --------------------- - -Improve schema directives typing and printing after latest refactor. - -- Support for printing schema directives for non-scalars (e.g. types) and null values. -- Also print the schema directive itself and any extra types defined in it -- Fix typing for apis expecting directives (e.g. `strawberry.field`, `strawberry.type`, etc) - to expect an object instead of a `StrawberrySchemaDirective`, which is now an internal type. - -Contributed by [Thiago Bellini Ribeiro](https://github.com/bellini666) via [PR #1723](https://github.com/strawberry-graphql/strawberry/pull/1723/) - - -0.113.0 - 2022-05-19 --------------------- - -This release adds support for Starlette 0.18 to 0.20 - -It also removes upper bound dependencies limit for starlette, -allowing you to install the latest version without having to -wait for a new release of Strawberry - -Contributed by [Timothy Pansino](https://github.com/TimPansino) via [PR #1594](https://github.com/strawberry-graphql/strawberry/pull/1594/) - - -0.112.0 - 2022-05-15 --------------------- - -This release adds a new flask view to allow for aysnc dispatching of requests. - -This is especially useful when using dataloaders with flask. - -```python -from strawberry.flask.views import AsyncGraphQLView - -... - -app.add_url_rule( - "/graphql", - view_func=AsyncGraphQLView.as_view("graphql_view", schema=schema, **kwargs), -) -``` - -Contributed by [Scott Weitzner](https://github.com/scottweitzner) via [PR #1907](https://github.com/strawberry-graphql/strawberry/pull/1907/) - - -0.111.2 - 2022-05-09 --------------------- - -This release fixes resolvers using functions with generic type variables raising a `MissingTypesForGenericError` error. - -For example a resolver factory like the below can now be used: - -```python -import strawberry -from typing import Type, TypeVar - -T = TypeVar("T") # or TypeVar("T", bound=StrawberryType) etc - - -def resolver_factory(strawberry_type: Type[T]): - def resolver(id: strawberry.ID) -> T: - # some actual logic here - return strawberry_type(...) - - return resolver -``` - -Contributed by [Tim OSullivan](https://github.com/invokermain) via [PR #1891](https://github.com/strawberry-graphql/strawberry/pull/1891/) - - -0.111.1 - 2022-05-03 --------------------- - -Rename internal variable `custom_getter` in FastAPI router implementation. - -Contributed by [Gary Donovan](https://github.com/garyd203) via [PR #1875](https://github.com/strawberry-graphql/strawberry/pull/1875/) - - -0.111.0 - 2022-05-02 --------------------- - -This release adds support for Apollo Federation 2 directives: -- @shareable -- @tag -- @override -- @inaccessible - -This release does **not** add support for the @link directive. - -This release updates the @key directive to align with Apollo Federation 2 updates. - -See the below code snippet and/or the newly-added test cases for examples on how to use the new directives. -The below snippet demonstrates the @override directive. -```python -import strawberry -from typing import List - - -@strawberry.interface -class SomeInterface: - id: strawberry.ID - - -@strawberry.federation.type(keys=["upc"], extend=True) -class Product(SomeInterface): - upc: str = strawberry.federation.field(external=True, override=["mySubGraph"]) - - -@strawberry.federation.type -class Query: - @strawberry.field - def top_products(self, first: int) -> List[Product]: - return [] - - -schema = strawberry.federation.Schema(query=Query) -``` - -should return: - -```graphql -extend type Product implements SomeInterface @key(fields: "upc", resolvable: "True") { - id: ID! - upc: String! @external @override(from: "mySubGraph") -} - -type Query { - _service: _Service! - _entities(representations: [_Any!]!): [_Entity]! - topProducts(first: Int!): [Product!]! -} - -interface SomeInterface { - id: ID! -} - -scalar _Any - -union _Entity = Product - -type _Service { - sdl: String! -} -``` - -Contributed by [Matt Skillman](https://github.com/mtskillman) via [PR #1874](https://github.com/strawberry-graphql/strawberry/pull/1874/) - - -0.110.0 - 2022-05-02 --------------------- - -This release adds support for passing a custom name to schema directives fields, -by using `strawberry.directive_field`. - -```python -import strawberry - - -@strawberry.schema_directive(locations=[Location.FIELD_DEFINITION]) -class Sensitive: - reason: str = strawberry.directive_field(name="as") - real_age_2: str = strawberry.directive_field(name="real_age") - - -@strawberry.type -class Query: - first_name: str = strawberry.field( - directives=[Sensitive(reason="GDPR", real_age_2="42")] - ) -``` - -should return: - -```graphql -type Query { - firstName: String! @sensitive(as: "GDPR", real_age: "42") -} -``` - -Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #1871](https://github.com/strawberry-graphql/strawberry/pull/1871/) - - -0.109.1 - 2022-04-28 --------------------- - -This release adds support for Mypy 0.950 - -Contributed by [dependabot](https://github.com/dependabot) via [PR #1855](https://github.com/strawberry-graphql/strawberry/pull/1855/) - - -0.109.0 - 2022-04-23 --------------------- - -Changed the location of `UNSET` from `arguments.py` to `unset.py`. `UNSET` can now also be imported directly from `strawberry`. Deprecated the `is_unset` method in favor of the builtin `is` operator: - -```python -from strawberry import UNSET -from strawberry.arguments import is_unset # old - -a = UNSET - -assert a is UNSET # new -assert is_unset(a) # old -``` -Further more a new subsection to the docs was added explaining this. - -Contributed by [Dominique Garmier](https://github.com/DominiqueGarmier) via [PR #1813](https://github.com/strawberry-graphql/strawberry/pull/1813/) - - -0.108.3 - 2022-04-22 --------------------- - -Fixes a bug when converting pydantic models with NewTypes in a List. -This no longers causes an exception. - - ```python - from typing import List, NewType - from pydantic import BaseModel - import strawberry - - password = NewType("password", str) - - - class User(BaseModel): - passwords: List[password] - - - @strawberry.experimental.pydantic.type(User) - class UserType: - passwords: strawberry.auto - ``` - -Contributed by [James Chua](https://github.com/thejaminator) via [PR #1770](https://github.com/strawberry-graphql/strawberry/pull/1770/) - - -0.108.2 - 2022-04-21 --------------------- - -Fixes mypy type inference when using @strawberry.experimental.pydantic.input - and @strawberry.experimental.pydantic.interface decorators - -Contributed by [James Chua](https://github.com/thejaminator) via [PR #1832](https://github.com/strawberry-graphql/strawberry/pull/1832/) - - -0.108.1 - 2022-04-20 --------------------- - -Refactoring: Move enum deserialization logic from convert_arguments to CustomGraphQLEnumType - -Contributed by [Paulo Costa](https://github.com/paulo-raca) via [PR #1765](https://github.com/strawberry-graphql/strawberry/pull/1765/) - - -0.108.0 - 2022-04-19 --------------------- - -Added support for deprecating Enum values with `deprecation_reason` while using `strawberry.enum_value` instead of string definition. - -```python -@strawberry.enum -class IceCreamFlavour(Enum): - VANILLA = strawberry.enum_value("vanilla") - STRAWBERRY = strawberry.enum_value("strawberry", deprecation_reason="We ran out") - CHOCOLATE = "chocolate" -``` - -Contributed by [Mateusz Sobas](https://github.com/msobas) via [PR #1720](https://github.com/strawberry-graphql/strawberry/pull/1720/) - - -0.107.1 - 2022-04-18 --------------------- - -This release fixes an issue in the previous release where requests using query params did not support passing variable values. Variables passed by query params are now parsed from a string to a dictionary. - -Contributed by [Matt Exact](https://github.com/MattExact) via [PR #1820](https://github.com/strawberry-graphql/strawberry/pull/1820/) - - -0.107.0 - 2022-04-18 --------------------- - -This release adds support in all our integration for queries via GET requests. -This behavior is enabled by default, but you can disable it by passing -`allow_queries_via_get=False` to the constructor of the integration of your -choice. - -For security reason only queries are allowed via `GET` requests. - -Contributed by [Matt Exact](https://github.com/MattExact) via [PR #1686](https://github.com/strawberry-graphql/strawberry/pull/1686/) - - -0.106.3 - 2022-04-15 --------------------- - -Correctly parse Decimal scalar types to avoid floating point errors - -Contributed by [Marco Acierno](https://github.com/marcoacierno) via [PR #1811](https://github.com/strawberry-graphql/strawberry/pull/1811/) - - -0.106.2 - 2022-04-14 --------------------- - -Allow all data types in `Schema(types=[...])` - -Contributed by [Paulo Costa](https://github.com/paulo-raca) via [PR #1714](https://github.com/strawberry-graphql/strawberry/pull/1714/) - - -0.106.1 - 2022-04-14 --------------------- - -This release fixes a number of problems with single-result-operations over -`graphql-transport-ws` protocol - -- operation **IDs** now share the same namespace as streaming operations - meaning that they cannot be reused while the others are in operation - -- single-result-operations now run as *tasks* meaning that messages related - to them can be overlapped with other messages on the websocket. - -- single-result-operations can be cancelled with the `complete` message. - -- IDs for single result and streaming result operations are now released - once the operation is done, allowing them to be re-used later, as well as - freeing up resources related to previous requests. - -Contributed by [Kristjรกn Valur Jรณnsson](https://github.com/kristjanvalur) via [PR #1792](https://github.com/strawberry-graphql/strawberry/pull/1792/) - - -0.106.0 - 2022-04-14 --------------------- - -This release adds an implementation of the `GraphQLTestClient` for the `aiohttp` integration (in addition to the existing `asgi` and `Django` support). It hides the HTTP request's details and verifies that there are no errors in the response (this behavior can be disabled by passing `asserts_errors=False`). This makes it easier to test queries and makes your tests cleaner. - -If you are using `pytest` you can add a fixture in `conftest.py` - -```python -import pytest - -from strawberry.aiohttp.test.client import GraphQLTestClient - - -@pytest.fixture -def graphql_client(aiohttp_client, myapp): - yield GraphQLTestClient(aiohttp_client(myapp)) -``` - -And use it everywhere in your tests - -```python -def test_strawberry(graphql_client): - query = """ - query Hi($name: String!) { - hi(name: $name) - } - """ - - result = graphql_client.query(query, variables={"name": "๐Ÿ“"}) - - assert result.data == {"hi": "Hi ๐Ÿ“!"} -``` - -Contributed by [Etty](https://github.com/estyxx) via [PR #1604](https://github.com/strawberry-graphql/strawberry/pull/1604/) - - -0.105.1 - 2022-04-12 --------------------- - -This release fixes a bug in the codegen that marked optional unions -as non optional. - -Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #1806](https://github.com/strawberry-graphql/strawberry/pull/1806/) - - -0.105.0 - 2022-04-05 --------------------- - -This release adds support for passing `json_encoder` and `json_dumps_params` to Sanic's view. - - -```python -from strawberry.sanic.views import GraphQLView - -from api.schema import Schema - -app = Sanic(__name__) - -app.add_route( - GraphQLView.as_view( - schema=schema, - graphiql=True, - json_encoder=CustomEncoder, - json_dumps_params={}, - ), - "/graphql", -) -``` - -Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #1797](https://github.com/strawberry-graphql/strawberry/pull/1797/) - - -0.104.4 - 2022-04-05 --------------------- - -Allow use of `AsyncIterator` and `AsyncIterable` generics to annotate return -type of subscription resolvers. - -Contributed by [San Kilkis](https://github.com/skilkis) via [PR #1771](https://github.com/strawberry-graphql/strawberry/pull/1771/) - - -0.104.3 - 2022-04-03 --------------------- - -Exeptions from handler functions in graphql_transport_ws are no longer -incorrectly caught and classified as message parsing errors. - -Contributed by [Kristjรกn Valur Jรณnsson](https://github.com/kristjanvalur) via [PR #1761](https://github.com/strawberry-graphql/strawberry/pull/1761/) - - -0.104.2 - 2022-04-02 --------------------- - -Drop support for Django < 3.2. - -Contributed by [Guillaume Andreu Sabater](https://github.com/g-as) via [PR #1787](https://github.com/strawberry-graphql/strawberry/pull/1787/) - - -0.104.1 - 2022-03-28 --------------------- - -This release adds support for aliased fields when doing codegen. - -Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #1772](https://github.com/strawberry-graphql/strawberry/pull/1772/) - - -0.104.0 - 2022-03-28 --------------------- - -Add `is_auto` utility for checking if a type is `strawberry.auto`, -considering the possibility of it being a `StrawberryAnnotation` or -even being used inside `Annotated`. - -Contributed by [Thiago Bellini Ribeiro](https://github.com/bellini666) via [PR #1721](https://github.com/strawberry-graphql/strawberry/pull/1721/) - - -0.103.9 - 2022-03-23 --------------------- - -This release moves the console plugin for the codegen command -to be last one, allowing to run code before writing files to -disk. - -Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #1760](https://github.com/strawberry-graphql/strawberry/pull/1760/) - - -0.103.8 - 2022-03-18 --------------------- - -This release adds a `python_type` to the codegen `GraphQLEnum` class -to allow access to the original python enum when generating code - -Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #1752](https://github.com/strawberry-graphql/strawberry/pull/1752/) - - -0.103.7 - 2022-03-18 --------------------- - -Fix an issue where there was no clean way to mark a Pydantic field as deprecated, add permission classes, or add directives. Now you can use the short field syntax to do all three. - -```python -import pydantic -import strawberry - - -class MyModel(pydantic.BaseModel): - age: int - name: str - - -@strawberry.experimental.pydantic.type(MyModel) -class MyType: - age: strawberry.auto - name: strawberry.auto = strawberry.field( - deprecation_reason="Because", - permission_classes=[MyPermission], - directives=[MyDirective], - ) -``` - -Contributed by [Matt Allen](https://github.com/Matt343) via [PR #1748](https://github.com/strawberry-graphql/strawberry/pull/1748/) - - -0.103.6 - 2022-03-18 --------------------- - -This release adds a missing `__init__.py` inside `cli/commands` - -Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #1751](https://github.com/strawberry-graphql/strawberry/pull/1751/) - - -0.103.5 - 2022-03-18 --------------------- - -This release fixes an issue that prevented using generic types -with interfaces. - -Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #1701](https://github.com/strawberry-graphql/strawberry/pull/1701/) - - -0.103.4 - 2022-03-18 --------------------- - -This release fixes a couple of more issues with codegen: - -1. Adds support for boolean values in input fields -2. Changes how we unwrap types in order to add full support for LazyTypes, Optionals and Lists -3. Improve also how we generate types for unions, now we don't generate a Union type if the selection is for only one type - -Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #1746](https://github.com/strawberry-graphql/strawberry/pull/1746/) - - -0.103.3 - 2022-03-17 --------------------- - -The return type annotation for `DataLoader.load` and `load_many` no longer -includes any exceptions directly returned by the `load_fn`. The ability to -handle errors by returning them as elements from `load_fn` is now documented too. - -Contributed by [Huon Wilson](https://github.com/huonw) via [PR #1737](https://github.com/strawberry-graphql/strawberry/pull/1737/) - - -0.103.2 - 2022-03-17 --------------------- - -This release add supports for `LazyType`s in the codegen command - -Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #1745](https://github.com/strawberry-graphql/strawberry/pull/1745/) - - -0.103.1 - 2022-03-15 --------------------- - -This release adds support for MyPy 0.941 under Python 3.10 - -Contributed by [dependabot](https://github.com/dependabot) via [PR #1728](https://github.com/strawberry-graphql/strawberry/pull/1728/) - - -0.103.0 - 2022-03-14 --------------------- - -This release adds an experimental codegen feature for queries. -It allows to combine a graphql query and Strawberry schema to generate -Python types or TypeScript types. - -You can use the following command: - -```bash -strawberry codegen --schema schema --output-dir ./output -p python query.graphql -``` - -to generate python types that correspond to your GraphQL query. - -Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #1655](https://github.com/strawberry-graphql/strawberry/pull/1655/) - - -0.102.3 - 2022-03-14 --------------------- - -This release makes StrawberryOptional and StrawberryList hashable, -allowing to use strawberry types with libraries like dacite and -dataclasses_json. - -Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #1726](https://github.com/strawberry-graphql/strawberry/pull/1726/) - - -0.102.2 - 2022-03-08 --------------------- - -Add support for postponed evaluation of annotations -([PEP-563](https://www.python.org/dev/peps/pep-0563/)) to `strawberry.Private` -annotated fields. - -## Example - -This release fixes Issue #1586 using schema-conversion time filtering of -`strawberry.Private` fields for PEP-563. This means the following is now -supported: - -```python -@strawberry.type -class Query: - foo: "strawberry.Private[int]" -``` - -Forward references are supported as well: - -```python -from __future__ import annotations - -from dataclasses import dataclass - - -@strawberry.type -class Query: - private_foo: strawberry.Private[SensitiveData] - - @strawberry.field - def foo(self) -> int: - return self.private_foo.visible - - -@dataclass -class SensitiveData: - visible: int - not_visible: int -``` - -Contributed by [San Kilkis](https://github.com/skilkis) via [PR #1684](https://github.com/strawberry-graphql/strawberry/pull/1684/) - - -0.102.1 - 2022-03-07 --------------------- - -This PR improves the support for scalars when using MyPy. - -Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #1205](https://github.com/strawberry-graphql/strawberry/pull/1205/) - - -0.102.0 - 2022-03-07 --------------------- - -Added the response object to `get_context` on the `flask` view. This means that in fields, something like this can be used; - -```python -@strawberry.field -def response_check(self, info: Info) -> bool: - response: Response = info.context["response"] - response.status_code = 401 - - return True -``` - -0.101.0 - 2022-03-06 --------------------- - -This release adds support for `graphql-transport-ws` single result operations. - -Single result operations allow clients to execute queries and mutations over an existing WebSocket connection. - -Contributed by [Jonathan Ehwald](https://github.com/DoctorJohn) [PR #1698](https://github.com/strawberry-graphql/strawberry/pull/1698/) - - -0.100.0 - 2022-03-05 --------------------- - -Change `strawberry.auto` to be a type instead of a sentinel. -This not only removes the dependency on sentinel from the project, but also fixes -some related issues, like the fact that only types can be used with `Annotated`. - -Also, custom scalars will now trick static type checkers into thinking they -returned their wrapped type. This should fix issues with pyright 1.1.224+ where -it doesn't allow non-type objects to be used as annotations for dataclasses and -dataclass-alike classes (which is strawberry's case). The change to `strawberry.auto` -also fixes this issue for it. - -Contributed by [Thiago Bellini Ribeiro](https://github.com/bellini666) [PR #1690](https://github.com/strawberry-graphql/strawberry/pull/1690/) - - -0.99.3 - 2022-03-05 -------------------- - -This release adds support for flask 2.x and also relaxes the requirements for Django, allowing to install newer version of Django without having to wait for Strawberry to update its supported dependencies list. - -Contributed by [Guillaume Andreu Sabater](https://github.com/g-as) [PR #1687](https://github.com/strawberry-graphql/strawberry/pull/1687/) - - -0.99.2 - 2022-03-04 -------------------- - -This fixes the schema printer to add support for schema -directives on input types. - -Contributed by [Patrick Arminio](https://github.com/patrick91) [PR #1697](https://github.com/strawberry-graphql/strawberry/pull/1697/) - - -0.99.1 - 2022-03-02 -------------------- - -This release fixed a false positive deprecation warning related to our AIOHTTP class based view. - -Contributed by [Jonathan Ehwald](https://github.com/DoctorJohn) [PR #1691](https://github.com/strawberry-graphql/strawberry/pull/1691/) - - -0.99.0 - 2022-02-28 -------------------- - -This release adds the following scalar types: - -- `JSON` -- `Base16` -- `Base32` -- `Base64` - -they can be used like so: - -```python -from strawberry.scalar import Base16, Base32, Base64, JSON - - -@strawberry.type -class Example: - a: Base16 - b: Base32 - c: Base64 - d: JSON -``` - -Contributed by [Paulo Costa](https://github.com/paulo-raca) [PR #1647](https://github.com/strawberry-graphql/strawberry/pull/1647/) - - -0.98.2 - 2022-02-24 -------------------- - -Adds support for converting pydantic conlist. -Note that constraint is not enforced in the graphql type. -Thus, we recommend always working on the pydantic type such that the validation is enforced. - -```python -import strawberry -from pydantic import BaseModel, conlist - - -class Example(BaseModel): - friends: conlist(str, min_items=1) - - -@strawberry.experimental.pydantic.input(model=Example, all_fields=True) -class ExampleGQL: - ... - - -@strawberry.type -class Query: - @strawberry.field() - def test(self, example: ExampleGQL) -> None: - # friends may be an empty list here - print(example.friends) - # calling to_pydantic() runs the validation and raises - # an error if friends is empty - print(example.to_pydantic().friends) - - -schema = strawberry.Schema(query=Query) -``` - -The converted graphql type is -``` -input ExampleGQL { - friends: [String!]! -} -``` - -Contributed by [James Chua](https://github.com/thejaminator) [PR #1656](https://github.com/strawberry-graphql/strawberry/pull/1656/) - -0.98.1 - 2022-02-24 -------------------- - -This release wasn't published on PyPI - -0.98.0 - 2022-02-23 -------------------- - -This release updates `graphql-core` to `3.2.0` - -Make sure you take a look at [`graphql-core`'s release notes](https://github.com/graphql-python/graphql-core/releases/tag/v3.2.0) -for any potential breaking change that might affect you if you're importing things -from the `graphql` package directly. - -Contributed by [Patrick Arminio](https://github.com/patrick91) [PR #1601](https://github.com/strawberry-graphql/strawberry/pull/1601/) - - 0.97.0 - 2022-02-17 ------------------- @@ -2956,12 +56,12 @@ Decides if the all the GraphQL field names for the generated type should use the from pydantic import BaseModel, Field import strawberry - class UserModel(BaseModel): - id: int = Field(..., alias="my_alias_name") - + id: int = Field(..., alias="my_alias_name") -@strawberry.experimental.pydantic.type(UserModel, use_pydantic_alias=False) +@strawberry.experimental.pydantic.type( + UserModel, use_pydantic_alias=False +) class User: id: strawberry.auto ``` @@ -3012,18 +112,15 @@ T = TypeVar("T") K = TypeVar("K") V = TypeVar("V") - @strawberry.type class Value(Generic[T]): value: T - @strawberry.type class DictItem(Generic[K, V]): key: K value: V - @strawberry.type class Query: d: Value[List[DictItem[int, str]]] @@ -3069,16 +166,13 @@ Adds mypy extension support as well. from pydantic import BaseModel import strawberry - class UserPydantic(BaseModel): age: int - @strawberry.experimental.pydantic.type(UserPydantic) class UserStrawberry: age: strawberry.auto - reveal_type(UserStrawberry(age=123).to_pydantic()) ``` Mypy will infer the type as "UserPydantic". Previously it would be "Any" @@ -3151,7 +245,6 @@ the particular field defined. class User(BaseModel): age: int - @strawberry.experimental.pydantic.type(User) class UserType: age: strawberry.auto @@ -3199,18 +292,15 @@ For example: class Cat: name: str - @strawberry.type class Dog: name: str - Animal = strawberry.union("Animal", (Cat, Dog)) - @strawberry.type class Query: - animal: Animal | None # This line no longer triggers a TypeError + animal: Animal | None # This line no longer triggers a TypeError ``` Contributed by [Yossi Rozantsev](https://github.com/Apakottur) [PR #1540](https://github.com/strawberry-graphql/strawberry/pull/1540/) @@ -3381,7 +471,6 @@ urlpatterns = [ ), ] - # โ€ฆ or set them in a custom view class CustomAsyncGraphQLView(AsyncGraphQLView): json_encoder = JSONEncoder @@ -3442,7 +531,7 @@ from django.test.client import Client from strawberry.django.test import GraphQLTestClient -@pytest.fixture +@pytest.fixture() def graphql_client(): yield GraphQLTestClient(Client()) ``` @@ -3513,7 +602,6 @@ This release fixes an issue that prevented using `classmethod`s and `staticmetho ```python import strawberry - @strawberry.type class Query: @strawberry.field @@ -3541,12 +629,10 @@ to use unions without any type issue, like so: class User: name: str - @strawberry.type class Error: message: str - UserOrError = strawberry.union("UserOrError", (User, Error)) x: UserOrError = User(name="Patrick") @@ -3602,12 +688,10 @@ passing a generic type to another generic, like so: class Edge(Generic[T]): node: T - @strawberry.type class Connection(Generic[T]): edges: List[T] - Connection[Edge[int]] ``` @@ -3739,14 +823,12 @@ import strawberry from fastapi import FastAPI from strawberry.fastapi import GraphQLRouter - @strawberry.type class Query: @strawberry.field def hello(self) -> str: return "Hello World" - schema = strawberry.Schema(Query) graphql_app = GraphQLRouter(schema) @@ -3800,11 +882,9 @@ Pydantic fields' `description` are now copied to the GraphQL schema import pydantic import strawberry - class UserModel(pydantic.BaseModel): age: str = pydantic.Field(..., description="Description") - @strawberry.experimental.pydantic.type(UserModel) class User: age: strawberry.auto @@ -3886,7 +966,6 @@ import pydantic import strawberry from strawberry.experimental.pydantic import auto - class User(pydantic.BaseModel): age: int password: str @@ -3929,17 +1008,14 @@ for the new union syntax, allowing to declare unions like this: ```python import strawberry - @strawberry.type class User: name: str - @strawberry.type class Error: code: str - @strawberry.type class Query: find_user: User | Error @@ -4032,11 +1108,11 @@ import strawberry from strawberry.extensions import ParserCache, ValidationCache schema = strawberry.Schema( - Query, - extensions=[ - ParserCache(), - ValidationCache(), - ], + Query, + extensions=[ + ParserCache(), + ValidationCache(), + ] ) ``` @@ -4103,19 +1179,16 @@ These now function as expected: ```python T = TypeVar("T") - @strawberry.enum class VehicleMake(Enum): - FORD = "ford" - TOYOTA = "toyota" - HONDA = "honda" - + FORD = 'ford' + TOYOTA = 'toyota' + HONDA = 'honda' @strawberry.type class GenericForEnum(Generic[T]): generic_slot: T - @strawberry.type class SomeType: field: GenericForEnum[VehicleMake] @@ -4134,12 +1207,10 @@ class TypeFromAnotherFile: ```python T = TypeVar("T") - @strawberry.type class GenericType(Generic[T]): item: T - @strawberry.type class RealType: lazy: GenericType[LazyType["TypeFromAnotherFile", "another_file.py"]] @@ -4192,23 +1263,19 @@ For example this will raise an exception: ```python import strawberry - @strawberry.type class Noun: text: str - @strawberry.type class Verb: text: str - Word = strawberry.union("Word", types=(Noun, Verb)) - @strawberry.field def add_word(word: Word) -> bool: - ... + ... ``` Contributed by [Mohammad Hossein Yazdani](https://github.com/MAM-SYS) [PR #1222](https://github.com/strawberry-graphql/strawberry/pull/1222/) @@ -4249,14 +1316,7 @@ from strawberry.django.views import AsyncGraphQLView from .schema import schema -urlpatterns = [ - path( - "graphql", - AsyncGraphQLView.as_view( - schema=schema, graphiql=True, subscriptions_enabled=True - ), - ) -] +urlpatterns = [path("graphql", AsyncGraphQLView.as_view(schema=schema, graphiql=True, subscriptions_enabled=True))] ``` Contributed by [lijok](https://github.com/lijok) [PR #1215](https://github.com/strawberry-graphql/strawberry/pull/1215/) @@ -4294,19 +1354,17 @@ EpochDateTime = strawberry.scalar( parse_value=lambda value: datetime.fromtimestamp(int(value), timezone.utc), ) - @strawberry.type class Query: @strawberry.field def current_time(self) -> datetime: return datetime.now() - schema = strawberry.Schema( - Query, - scalar_overrides={ - datetime: EpochDateTime, - }, + Query, + scalar_overrides={ + datetime: EpochDateTime, + } ) result = schema.execute_sync("{ currentTime }") assert result.data == {"currentTime": 1628683200} @@ -4334,14 +1392,12 @@ using directives and async extensions. class Query: name: str = "Banana" - @strawberry.directive( locations=[DirectiveLocation.FIELD], description="Make string uppercase" ) async def uppercase(value: str): return value.upper() - schema = strawberry.Schema(query=Query, directives=[uppercase]) ``` @@ -4366,15 +1422,12 @@ This releases fixes a MyPy issue that prevented from using types created with import strawberry from strawberry.tools import create_type - @strawberry.field def name() -> str: return "foo" - MyType = create_type("MyType", [name]) - class Query(MyType): ... ``` @@ -4414,7 +1467,6 @@ This release allows background tasks to be set with the ASGI integration. Tasks ```python from starlette.background import BackgroundTask - @strawberry.mutation def create_flavour(self, info: Info) -> str: info.context["response"].background = BackgroundTask(...) @@ -4464,7 +1516,6 @@ It's also possible to mix both synchronous and asynchronous hooks within one ext ```python from strawberry.extensions import Extension - class MyExtension(Extension): async def on_request_start(self): print("GraphQL request start") @@ -4519,7 +1570,6 @@ their synchronous counterpart is that the `has_permission` method is asynchronou ```python from strawberry.permission import BasePermission - class IsAuthenticated(BasePermission): message = "User is not authenticated" @@ -4577,7 +1627,9 @@ from strawberry.tools import depth_limit_validator # Add the depth limit validator to the list of default validation rules -validation_rules = default_validation_rules + [depth_limit_validator(3)] +validation_rules = ( + default_validation_rules + [depth_limit_validator(3)] +) result = schema.execute_sync( """ @@ -4594,6 +1646,7 @@ result = schema.execute_sync( } """, validation_rules=validation_rules, + ) ) assert len(result.errors) == 1 assert result.errors[0].message == "'MyQuery' exceeds maximum operation depth of 3" @@ -4651,8 +1704,9 @@ You can use it like so: class Query: example_field: str = "Example" - -schema = strawberry.Schema(query=Query, config=StrawberryConfig(auto_camel_case=False)) +schema = strawberry.Schema( + query=Query, config=StrawberryConfig(auto_camel_case=False) +) ``` Contributed by [Patrick Arminio](https://github.com/patrick91) [PR #798](https://github.com/strawberry-graphql/strawberry/pull/798/) @@ -4733,7 +1787,6 @@ This is now allowed: class SomeInterface: id: strawberry.ID - @strawberry.federation.type(keys=["upc"], extend=True) class Product(SomeInterface): upc: str = strawberry.federation.field(external=True) @@ -4806,7 +1859,8 @@ class HelloInput: class Query: @strawberry.field def hello( - self, input_: Annotated[HelloInput, strawberry.argument(name="input")] + self, + input_: Annotated[HelloInput, strawberry.argument(name="input")] ) -> str: return f"Hi {input_.name}" ``` @@ -4830,9 +1884,10 @@ class IceCreamFlavour(Enum): CHOCOLATE = "chocolate" PISTACHIO = "pistachio" - @strawberry.mutation -def create_flavour(self, flavour: IceCreamFlavour = IceCreamFlavour.STRAWBERRY) -> str: +def create_flavour( + self, flavour: IceCreamFlavour = IceCreamFlavour.STRAWBERRY +) -> str: return f"{flavour.name}" ``` @@ -5012,7 +2067,6 @@ Example: import strawberry from strawberry.extensions import Extension - class MyExtension(Extension): def on_request_end(self): root_value = self.execution_context.root_value @@ -5043,7 +2097,6 @@ from graphql import ExecutionResult as GraphQLExecutionResult from graphql.error.graphql_error import GraphQLError from graphql.language import DocumentNode as GraphQLDocumentNode - @dataclasses.dataclass class ExecutionContext: query: str @@ -5061,13 +2114,11 @@ and can be accessed in any of the extension hooks: ```python from strawberry.extensions import Extension - class MyExtension(Extension): def on_request_end(self): result = self.execution_context.result # Do something with the result - schema = strawberry.Schema(query=Query, extensions=[MyExtension]) ``` @@ -5131,7 +2182,6 @@ You can use `@requires` to specify which fields you need to resolve a field ```python import strawberry - @strawberry.federation.type(keys=["id"], extend=True) class Product: id: strawberry.ID = strawberry.federation.field(external=True) @@ -5252,16 +2302,13 @@ This release adds a function called `create_type` to create a Strawberry type fr import strawberry from strawberry.tools import create_type - @strawberry.field def hello(info) -> str: return "World" - def get_name(info) -> str: return info.context.user.name - my_name = strawberry.field(name="myName", resolver=get_name) Query = create_type("Query", [hello, my_name]) @@ -5278,12 +2325,10 @@ This release fixes an issue when using nested lists, this now works properly: def get_polygons() -> List[List[float]]: return [[2.0, 6.0]] - @strawberry.type class Query: polygons: List[List[float]] = strawberry.field(resolver=get_polygons) - schema = strawberry.Schema(query=Query) query = "{ polygons }" @@ -5299,12 +2344,10 @@ This release fixes support for generic types so that now we can also use generic ```python T = typing.TypeVar("T") - @strawberry.input class Input(typing.Generic[T]): field: T - @strawberry.type class Query: @strawberry.field @@ -5321,7 +2364,6 @@ passing a type, like here: ```python T = typing.TypeVar("T") - @strawberry.interface class Node(typing.Generic[T]): id: strawberry.ID @@ -5329,12 +2371,10 @@ class Node(typing.Generic[T]): def _resolve(self) -> typing.Optional[T]: return None - @strawberry.type class Book(Node[str]): name: str - @strawberry.type class Query: @strawberry.field @@ -5431,7 +2471,6 @@ class Query: def name(self) -> str: return "A" - assert Query(1) == Query(1) assert Query(1) != Query(2) ``` @@ -5605,7 +2644,6 @@ Fix Generic name generation to use the custom name specified in Strawberry if av class EdgeName: node: str - @strawberry.type class Connection(Generic[T]): edge: T @@ -5622,7 +2660,6 @@ This release add the ability to disable query validation by setting ```python import strawberry - @strawberry.type class Query: @strawberry.field @@ -5737,17 +2774,14 @@ supported: class Dog: name: str - @strawberry.type class Cat: name: str - @strawberry.type class Connection(Generic[T]): nodes: List[T] - @strawberry.type class Query: connection: Connection[Union[Dog, Cat]] @@ -5774,14 +2808,15 @@ from pydantic import BaseModel class UserModel(BaseModel): id: int - name = "John Doe" + name = 'John Doe' signup_ts: Optional[datetime] = None friends: List[int] = [] - -@strawberry.experimental.pydantic.type( - model=UserModel, fields=["id", "name", "friends"] -) +@strawberry.experimental.pydantic.type(model=UserModel, fields=[ + 'id', + 'name', + 'friends' +]) class UserType: pass ``` @@ -5800,7 +2835,6 @@ Fix issue preventing reusing the same resolver for multiple fields, like here: def get_name(self) -> str: return "Name" - @strawberry.type class Query: name: str = strawberry.field(resolver=get_name) @@ -5834,7 +2868,6 @@ Bugfix to allow the use of `UNSET` as a default value for arguments. import strawberry from strawberry.arguments import UNSET, is_unset - @strawberry.type class Query: @strawberry.field @@ -5843,7 +2876,6 @@ class Query: return "Hi there" return "Hi {name}" - schema = strawberry.Schema(query=Query) result = schema.execute_async("{ hello }") @@ -5902,6 +2934,7 @@ async def app(): loader.load(3), ) + assert value_a == 1 assert value_b == 2 assert value_c == 3 @@ -5923,13 +2956,11 @@ import strawberry class Error: message: str - @strawberry.interface class FieldError(Error): message: str field: str - @strawberry.type class PasswordTooShort(FieldError): message: str @@ -6048,13 +3079,10 @@ Add support for adding a description to field arguments using the [`Annotated`]( ```python from typing import Annotated - @strawberry.type class Query: @strawberry.field - def user_by_id( - id: Annotated[str, strawberry.argument(description="The ID of the user")] - ) -> User: + def user_by_id(id: Annotated[str, strawberry.argument(description="The ID of the user")]) -> User: ... ``` @@ -6082,13 +3110,11 @@ from enum import Enum import strawberry - class IceCreamFlavour(Enum): VANILLA = "vanilla" STRAWBERRY = "strawberry" CHOCOLATE = "chocolate" - Flavour = strawberry.enum(IceCreamFlavour) ``` @@ -6188,7 +3214,6 @@ Example: ```python import strawberry - @strawberry.type class User: age: strawberry.Private[int] @@ -6211,10 +3236,9 @@ This release fixes an issue with mypy when doing the following: ```python import strawberry - @strawberry.type class User: - name: str = strawberry.field(description="Example") + name: str = strawberry.field(description='Example') ``` 0.34.0 - 2020-09-30 @@ -6234,11 +3258,11 @@ And here's an example of custom extension: ```python from strawberry.extensions import Extension - class MyExtension(Extension): def get_results(self): - return {"example": "this is an example for an extension"} - + return { + "example": "this is an example for an extension" + } schema = strawberry.Schema(query=Query, extensions=[MyExtension]) ``` @@ -6295,6 +3319,7 @@ documented without the keyword. The fix is very straight-forward: replace any ```python @strawberry.type class Query: + my_int: int = strawberry.field(f=lambda: 5) # becomes my_int: int = strawberry.field(resolver=lambda: 5) @@ -6303,6 +3328,7 @@ class Query: @strawberry.field def my_float(self) -> float: return 5.5 + ``` Other (minor) breaking changes @@ -6339,12 +3365,10 @@ from strawberry.django.views import GraphQLView as BaseGraphQLView from strawberry.http import GraphQLHTTPResponse from strawberry.types import ExecutionResult - class GraphQLView(BaseGraphQLView): - def process_result( - self, request: HttpRequest, result: ExecutionResult - ) -> GraphQLHTTPResponse: + def process_result(self, request: HttpRequest, result: ExecutionResult) -> GraphQLHTTPResponse: return {"data": result.data, "errors": result.errors or []} + ``` Flask example: @@ -6355,10 +3379,10 @@ from strawberry.flask.views import GraphQLView as BaseGraphQLView from strawberry.http import GraphQLHTTPResponse from strawberry.types import ExecutionResult - class GraphQLView(BaseGraphQLView): def process_result(self, result: ExecutionResult) -> GraphQLHTTPResponse: return {"data": result.data, "errors": result.errors or []} + ``` ASGI example: @@ -6371,12 +3395,10 @@ from starlette.requests import Request from .schema import schema - class GraphQL(BaseGraphQLView): - async def process_result( - self, request: Request, result: ExecutionResult - ) -> GraphQLHTTPResponse: + async def process_result(self, request: Request, result: ExecutionResult) -> GraphQLHTTPResponse: return {"data": result.data, "errors": result.errors or []} + ``` 0.30.1 - 2020-08-17 @@ -6396,7 +3418,6 @@ Django example: # views.py from strawberry.django.views import GraphQLView as BaseGraphQLView - class GraphQLView(BaseGraphQLView): def get_context(self, request): return { @@ -6427,7 +3448,6 @@ Flask example: # views.py from strawberry.flask.views import GraphQLView as BaseGraphQLView - class GraphQLView(BaseGraphQLView): def get_context(self, request): return { @@ -6464,7 +3484,6 @@ from strawberry.asgi import GraphQL as BaseGraphQL from .schema import schema - class GraphQL(BaseGraphQLView): async def get_context(self, request): return { @@ -6552,15 +3571,14 @@ like the following example: ```python from __future__ import annotations - @strawberry.type class Query: me: MyType = strawberry.field(name="myself") - @strawberry.type class MyType: id: strawberry.ID + ``` 0.28.0 - 2020-07-24 @@ -6605,15 +3623,12 @@ for example using an optional union. This is now properly supported: class A: a: int - @strawberry.type class B: b: int - Result = strawberry.union("Result", (A, B)) - @strawberry.type class Query: ab: Optional[Result] = None @@ -6699,9 +3714,9 @@ schema = strawberry.federation.Schema(query=Query, types=[Campaign]) Default values make input arguments nullable when the default is None. ```python class Query: - @strawberry.field - def hello(self, i: int = 0, s: str = None) -> str: - return s + @strawberry.field + def hello(self, i: int = 0, s: str = None) -> str: + return s ``` ```graphql type Query { @@ -6783,15 +3798,15 @@ to specify root and info arguments: def function_resolver() -> str: return "I'm a function resolver" - def function_resolver_with_params(x: str) -> str: return f"I'm {x}" - @strawberry.type class Query: hello: str = strawberry.field(resolver=function_resolver) - hello_with_params: str = strawberry.field(resolver=function_resolver_with_params) + hello_with_params: str = strawberry.field( + resolver=function_resolver_with_params + ) @strawberry.type @@ -6837,13 +3852,11 @@ to reuse types, here's an example: ```python T = typing.TypeVar("T") - @strawberry.type class Edge(typing.Generic[T]): cursor: strawberry.ID node: T - @strawberry.type class Query: @strawberry.field @@ -6913,12 +3926,11 @@ import strawberry from strawberry.permission import BasePermission - class IsAdmin(BasePermission): message = "You are not authorized" def has_permission(self, source, info): - return source.name.lower() == "Patrick" or _is_admin(info) + return source.name.lower() == "Patrick" or _is_admin(info) @strawberry.type @@ -6931,7 +3943,7 @@ class User: class Query: @strawberry.field(permission_classes=[IsAdmin]) def user(self, info) -> str: - return User(name="Patrick", email="example@email.com") + return User(name="Patrick", email="example@email.com") ``` 0.19.1 - 2019-12-20 @@ -7070,13 +4082,14 @@ Added a Django view that allows you to query the schema and interact with it via Usage: ```python + # Install -# pip install "strawberry-graphql[django]" +$ pip install 'strawberry-graphql[django]' # settings.py INSTALLED_APPS = [ - ..., - "strawberry.django", + ... + 'strawberry.django', ] # urls.py @@ -7084,8 +4097,9 @@ from strawberry.django.views import GraphQLView from your_project.schema import schema urlpatterns = [ - path("graphql/", GraphQLView.as_view(schema=schema)), + path('graphql/', GraphQLView.as_view(schema=schema)), ] + ``` 0.15.0 - 2019-09-04 @@ -7124,19 +4138,16 @@ Added support for defining query directives, example: import strawberry from strawberry.directive import DirectiveLocation - @strawberry.type class Query: cake: str = "made_in_switzerland" - @strawberry.directive( locations=[DirectiveLocation.FIELD], description="Make string uppercase" ) def uppercase(value: str, example: str): return value.upper() - schema = strawberry.Schema(query=Query, directives=[uppercase]) ``` @@ -7166,19 +4177,16 @@ Allow the usage of Union types in the mutations class A: x: int - @strawberry.type class B: y: int - @strawberry.type class Mutation: @strawberry.mutation def hello(self, info) -> Union[A, B]: return B(y=5) - schema = strawberry.Schema(query=A, mutation=Mutation) query = """ @@ -7211,8 +4219,7 @@ class Parent: @strawberry.field def friend(self, info) -> str: - return "food" - + return 'food' @strawberry.type class Schema(Parent): @@ -7229,19 +4236,17 @@ import strawberry from strawberry.permission import BasePermission - class IsAdmin(BasePermission): message = "You are not authorized" def has_permission(self, info): - return False - + return False @strawberry.type class Query: @strawberry.field(permission_classes=[IsAdmin]) def hello(self, info) -> str: - return "Hello" + return "Hello" ``` 0.12.0 - 2019-06-25 @@ -7293,7 +4298,6 @@ class Category: name: str id: InitVar[str] - @strawberry.type class Query: @strawberry.field @@ -7315,7 +4319,6 @@ Added support for passing resolver functions def resolver(root, info, par: str) -> str: return f"hello {par}" - @strawberry.type class Query: example: str = strawberry.field(resolver=resolver) @@ -7330,7 +4333,7 @@ Added support for renaming fields. Example usage: ```python @strawberry.type class Query: - example: str = strawberry.field(name="test") +example: str = strawberry.field(name='test') ``` 0.7.0 - 2019-05-09 @@ -7341,7 +4344,7 @@ Example: ```python @strawberry.interface class Node: - id: strawberry.ID +id: strawberry.ID ``` 0.6.0 - 2019-05-02 diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000000..00d527b88a --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,80 @@ + + +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, sex characteristics, gender identity and +expression, level of experience, education, socio-economic status, nationality, +personal appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +- Using welcoming and inclusive language +- Being respectful of differing viewpoints and experiences +- Gracefully accepting constructive criticism +- Focusing on what is best for the community +- Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +- The use of sexualized language or imagery and unwelcome sexual attention or + advances +- Trolling, insulting/derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or electronic + address, without explicit permission +- Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, or to ban temporarily or permanently any +contributor for other behaviors that they deem inappropriate, threatening, +offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the project team at patrick.arminio@gmail.com. All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an +incident. Further details of specific enforcement policies may be posted +separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 1.4, available at +https://www.contributor-covenant.org/version/1/4/code-of-conduct.html + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see +https://www.contributor-covenant.org/faq diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ea3c68c4e9..de22302be1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -32,11 +32,14 @@ possible. Make sure you include the Python and Strawberry versions. #### Before submitting a bug report -- Check that your issue does not already exist in the issue tracker on GitHub. +- Check that your issue does not already exist in the + [issue tracker](https://github.com/strawberry-graphql/strawberry/issues). #### How do I submit a bug report? -Bugs are tracked on the issue tracker on GitHub where you can create a new one. +Bugs are tracked on the +[official issue tracker](https://github.com/strawberry-graphql/strawberry/issues) +where you can create a new one. Explain the problem and include additional details to help maintainers reproduce the problem: @@ -79,11 +82,13 @@ suggestion, please #### Before submitting an enhancement suggestion -- Check that your issue does not already exist in the issue tracker on GitHub. +- Check that your issue does not already exist in the + [issue tracker](https://github.com/strawberry-graphql/strawberry/issues). #### How do I submit an enhancement suggestion? -Enhancement suggestions are tracked on the project's issue tracker on GitHub +Enhancement suggestions are tracked on the +[official issue tracker](https://github.com/strawberry-graphql/strawberry/issues) where you can create a new one and provide the following information: - **Use a clear and descriptive title** for the issue to identify the @@ -96,9 +101,6 @@ where you can create a new one and provide the following information: ### Contributing to code -> This section is about contributing to -[Strawberry Python library](https://github.com/strawberry-graphql/strawberry). - #### Local development You will need Poetry to start contributing to Strawberry. Refer to the @@ -118,7 +120,7 @@ that the current tests are passing on your machine: ```bash $ poetry install -$ poetry run pytest tests -n auto +$ poetry run pytest tests ``` Strawberry uses the [black](https://github.com/ambv/black) coding style and you diff --git a/README.md b/README.md index 5751b0043a..f79c9a370c 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ The quick start method provides a server and CLI to get going quickly. Install with: ```shell -pip install "strawberry-graphql[debug-server]" +pip install 'strawberry-graphql[debug-server]' ``` ## Getting Started @@ -76,8 +76,8 @@ A Django view is provided for adding a GraphQL endpoint to your application. ```python INSTALLED_APPS = [ - ..., # your other apps - "strawberry.django", + ... + 'strawberry.django', ] ``` @@ -89,7 +89,7 @@ from .schema import schema urlpatterns = [ ..., - path("graphql", GraphQLView.as_view(schema=schema)), + path('graphql', GraphQLView.as_view(schema=schema)), ] ``` @@ -147,5 +147,3 @@ pre-commit install The code in this project is licensed under MIT license. See [LICENSE](./LICENSE) for more information. - -![Recent Activity](https://images.repography.com/0/strawberry-graphql/strawberry/recent-activity/d751713988987e9331980363e24189ce.svg) diff --git a/RELEASE.md b/RELEASE.md index 92ae55746c..a4bf3a5107 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -1,3 +1,95 @@ Release type: minor -Add starlite integration +- Add Full support for deriving nested pydantic models (including when using `List`, `Optional`, `Union` and `ForwardRef`) +- Support for deriving [ormar](https://github.com/collerek/ormar) models with relationships (`ForeignKey`, `ManyToMany`, and reverse relations) +- Support for deriving [SQLModel](https://github.com/tiangolo/sqlmodel) models with `Relationship` fields +- Strawberry type declarations don't have to follow model declarations order (eg: childs can be defined before parents) +- Add a new `exclude` param to the `strawberry.experimental.pydantic.type` decorator, allowing to include all fields while excluding some + +## Pydantic + +GraphQL container types (`List`, `Optional` and `Union`) and `ForwardRef` are supported: + +```python +class User(pydantic.BaseModel): + name: str + hobby: Optional[List["Hobby"]] + +class Hobby(pydantic.BaseModel): + name: str + +@strawberry.experimental.pydantic.type(User, all_fields=True) +class UserType: + pass + +@strawberry.experimental.pydantic.type(Hobby, all_fields=True) +class HobbyType: + pass +``` + +## Ormar + +`ForeignKey`, `ManyToMany` and reverse relations are supported: + +```python +class Hobby(ormar.Model): + name: str + +class User(ormar.Model): + name: str = ormar.String(max_length=255) + hobby: Hobby = ormar.ForeignKey(Hobby, nullable=False) + +@strawberry.experimental.pydantic.type(Hobby, all_fields=True) +class HobbyType: + pass + +@strawberry.experimental.pydantic.type(User, all_fields=True) +class UserType: + pass +``` + +```graphql +type HobbyType { + name: String! + users: [UserType] +} + +type UserType { + name: String! + hobby: HobbyType! +} +``` + +## SLQModel + +SQLModel is another pydantic-based orm, that uses SQLAlchemy to define models. All relations are defined using the `Relationship` field: + +```python +class Hobby(SQLModel, table=True): + name: str + users: List["User"] = Relationship(back_populates="hobby") + +class User(SQLModel, table=True): + name: str = Field() + hobby: Hobby = Relationship(back_populates="users") + +@strawberry.experimental.pydantic.type(Hobby, all_fields=True) +class HobbyType: + pass + +@strawberry.experimental.pydantic.type(User, all_fields=True) +class UserType: + pass +``` + +```graphql +type HobbyType { + name: String! + users: [UserType!]! +} + +type UserType { + name: String! + hobby: HobbyType! +} +``` diff --git a/docs/README.md b/docs/README.md index cc4e0ecc24..73803aa035 100644 --- a/docs/README.md +++ b/docs/README.md @@ -9,13 +9,10 @@ - [Mutations](./general/mutations.md) - [Subscriptions](./general/subscriptions.md) - [Why](./general/why.md) -- [Breaking changes](./breaking-changes.md) -- [FAQ](./faq.md) ## Types - [Schema](./types/schema.md) -- [Schema Directives](./types/schema-directives.md) - [Schema configurations](./types/schema-configurations.md) - [Scalars](./types/scalars.md) - [Object types](./types/object-types.md) @@ -25,13 +22,7 @@ - [Generics](./types/generics.md) - [Resolvers](./types/resolvers.md) - [Union types](./types/union.md) -- [Lazy types](./types/lazy.md) - [Exceptions](./types/exceptions.md) -- [Private/External Fields](./types/private.md) - -## Codegen - -- [Query codegen](./codegen/query-codegen.md) ## [Extensions](./extensions) @@ -41,22 +32,16 @@ - [DataLoaders](./guides/dataloaders.md) - [Dealing with errors](./guides/errors.md) - [Federation](./guides/federation.md) -- [Federation V1](./guides/federation-v1.md) - [Custom extensions](./guides/custom-extensions.md) - [File upload](./guides/file-upload.md) -- [Pagination](./guides/pagination/overview.md) - - [Implementing Offset Pagination](./guides/pagination/offset-based.md) - - [Implementing Cursor Pagination](./guides/pagination/cursor-based.md) - - [Implementing the Connection specification](./guides/pagination/connections.md) +- [Pagination](./guides/pagination.md) - [Permissions](./guides/permissions.md) - [Builtin server](./guides/server.md) - [Tools](./guides/tools.md) - [Schema export](./guides/schema-export.md) -- [Convert to dictionary](./guides/convert-to-dictionary.md) ## Editor integration -- [Mypy](./editors/mypy.md) - [Visual Studio Code](./editors/vscode.md) ## Concepts @@ -69,7 +54,6 @@ - [AIOHTTP](./integrations/aiohttp.md) - [ASGI](./integrations/asgi.md) - [Django](./integrations/django.md) -- [Channels](./integrations/channels.md) - [FastAPI](./integrations/fastapi.md) - [Flask](./integrations/flask.md) - [Sanic](./integrations/sanic.md) diff --git a/docs/_test.md b/docs/_test.md index 4d9da6d841..f28acdc114 100644 --- a/docs/_test.md +++ b/docs/_test.md @@ -35,9 +35,7 @@ class X: This is probably not implemented in the best way, but for now it works: - - -``` +```python import ^[info](strawberry) @strawberry.type diff --git a/docs/breaking-changes.md b/docs/breaking-changes.md deleted file mode 100644 index 0249f55a46..0000000000 --- a/docs/breaking-changes.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -title: List of breaking changes ---- - -# List of breaking changes - -- [Version 0.146.0 - 5 December 2022](./breaking-changes/0.146.0.md) diff --git a/docs/breaking-changes/0.146.0.md b/docs/breaking-changes/0.146.0.md deleted file mode 100644 index 71b6116b11..0000000000 --- a/docs/breaking-changes/0.146.0.md +++ /dev/null @@ -1,64 +0,0 @@ ---- -title: 0.146.0 Breaking Changes ---- - -# v0.146.0 Breaking Changes - 5 December 2022 - -This release introduces a couple of breaking changes to the Sanic integration. - -## `process_result` is now async and accepts the request as the first argument - -If you customized the `process_result` function, you will need to update your -code to make it async and accept the request as the first argument. - -For example: - -```python -from strawberry.sanic.views import GraphQLView -from strawberry.http import GraphQLHTTPResponse, process_result -from strawberry.types import ExecutionResult -from sanic.request import Request -from graphql.error.graphql_error import format_error as format_graphql_error - - -class MyGraphQLView(GraphQLView): - async def process_result( - self, request: Request, result: ExecutionResult - ) -> GraphQLHTTPResponse: - if result.errors: - result.errors = [format_graphql_error(err) for err in result.errors] - - return process_result(data) -``` - -## `get_context` now receives also the response as the second argument - -If you customized the `get_context` function, you will need to update your code -to accept the response as the second argument. The response argument allows you -to set cookies and other headers. - -For example: - -```python -from strawberry.sanic.views import GraphQLView -from strawberry.sanic.context import StrawberrySanicContext -from strawberry.http.temporal_response import TemporalResponse -from sanic.request import Request - - -class MyGraphQLView(GraphQLView): - async def get_context( - self, request: Request, response: TemporalResponse - ) -> StrawberrySanicContext: - return {"request": request, "response": response} -``` - -# Deprecations - -## Context value is now a dictionary - -The context value is now a dictionary instead of a custom class. This means that -you should access the context value using the `["key"]` syntax instead of the -`.key` syntax. - -The `.key` syntax is still supported but will be removed in future releases. diff --git a/docs/codegen/query-codegen.md b/docs/codegen/query-codegen.md deleted file mode 100644 index b8b52e7114..0000000000 --- a/docs/codegen/query-codegen.md +++ /dev/null @@ -1,139 +0,0 @@ ---- -title: Codegen -experimental: true ---- - -# Query codegen - -Strawberry supports code generation for GraphQL queries. - - - -Schema codegen will be supported in future releases. We are testing the query -codegen in order to come up with a nice API. - - - -Let's assume we have the following GraphQL schema built with Strawberry: - -```python -from typing import List - -import strawberry - - -@strawberry.type -class Post: - id: strawberry.ID - title: str - - -@strawberry.type -class User: - id: strawberry.ID - name: str - email: str - - @strawberry.field - def post(self) -> Post: - return Post(id=self.id, title=f"Post for {self.name}") - - -@strawberry.type -class Query: - @strawberry.field - def user(self, info) -> User: - return User(id=strawberry.ID("1"), name="John", email="abc@bac.com") - - @strawberry.field - def all_users(self) -> List[User]: - return [ - User(id=strawberry.ID("1"), name="John", email="abc@bac.com"), - ] - - -schema = strawberry.Schema(query=Query) -``` - -and we want to generate types based on the following query: - -```graphql -query MyQuery { - user { - post { - title - } - } -} -``` - -With the following command: - -```bash -strawberry codegen --schema schema --output-dir ./output -p python query.graphql -``` - -We'll get the following output inside `output/types.py`: - -```python -class MyQueryResultUserPost: - title: str - - -class MyQueryResultUser: - post: MyQueryResultUserPost - - -class MyQueryResult: - user: MyQueryResultUser -``` - -## Why is this useful? - -Query code generation is usually used to generate types for clients using your -GraphQL APIs. - -Tools like [GraphQL Codegen](https://www.graphql-code-generator.com/) exist in -order to create types and code for your clients. Strawberry's codegen feature -aims to address the similar problem without needing to install a separate tool. - -## Plugin system - -Strawberry's codegen supports plugins, in the example above for example, we are -using the `python` plugin. To pass more plugins to the codegen tool, you can use -the `-p` flag, for example: - -```bash -strawberry codegen --schema schema --output-dir ./output -p python -p typescript query.graphql -``` - -the plugin can be specified as a python path. - -### Custom plugins - -The interface for plugins looks like this: - -```python -from strawberry.codegen import CodegenPlugin, CodegenFile, CodegenResult -from strawberry.codegen.types import GraphQLType, GraphQLOperation - - -class QueryCodegenPlugin: - def on_start(self) -> None: - ... - - def on_end(self, result: CodegenResult) -> None: - ... - - def generate_code( - self, types: List[GraphQLType], operation: GraphQLOperation - ) -> List[CodegenFile]: - return [] -``` - -- `on_start` is called before the codegen starts. -- `on_end` is called after the codegen ends and it receives the result of the - codegen. You can use this to format code, or add licenses to files and so on. -- `generated_code` is called when the codegen starts and it receives the types - and the operation. You cans use this to generate code for each type and - operation. diff --git a/docs/concepts/typings.md b/docs/concepts/typings.md index 782625e549..c06b8b3790 100644 --- a/docs/concepts/typings.md +++ b/docs/concepts/typings.md @@ -65,50 +65,41 @@ from typing import List, Union, Optional import strawberry BOOKS_LOOKUP = { - "Frank Herbert": [ - { - "title": "Dune", - "date_published": "1965-08-01", - "price": "5.99", - "isbn": 9780801950773, - } - ], + 'Frank Herbert': [{ + 'title': 'Dune', + 'date_published': '1965-08-01', + 'price': '5.99', + 'isbn': 9780801950773 + }], } - @strawberry.type class Book: title: str - author: "Author" + author: 'Author' date_published: datetime.date price: decimal.Decimal isbn: str - -def get_books_by_author(root: "Author") -> List["Book"]: +def get_books_by_author(root: 'Author') -> List['Book']: stored_books = BOOKS_LOOKUP[root.name] - return [ - Book( - title=book.get("title"), - author=root, - date_published=book.get("date_published"), - price=book.get("price"), - isbn=book.get("isbn"), - ) - for book in stored_books - ] - + return [Book( + title = book.get('title'), + author = root, + date_published = book.get('date_published'), + price = book.get('price'), + isbn = book.get('isbn') + ) for book in stored_books] @strawberry.type class Author: name: str books: List[Book] = strawberry.field(resolver=get_books_by_author) - @strawberry.type class Group: - name: Optional[str] # groups of authors don't necessarily have names + name: Optional[str] # groups of authors don't necessarily have names authors: List[Author] @strawberry.field diff --git a/docs/editors/mypy.md b/docs/editors/mypy.md deleted file mode 100644 index 0d979bcecb..0000000000 --- a/docs/editors/mypy.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -title: Mypy ---- - -# Mypy - -Strawberry comes with support for -[Mypy](https://mypy.readthedocs.io/en/stable/), a popular static type checker -for Python. - -This guide will explain how to configure Mypy to work with Strawberry. - -## Install Mypy - -The first thing we need to do is to install -[Mypy](https://mypy.readthedocs.io/en/stable/), this is the tool that will -perform the type checking. - -Once the tool is installed, we need to configure it to enable type checking and -use the Strawberry plugin. To do so we need to create a `mypy.ini` file in the -root of our project and add the following settings: - -```ini -[mypy] -plugins = strawberry.ext.mypy_plugin -``` - -Once you have configured the settings, you can run `mypy` and you should be -getting type checking errors. diff --git a/docs/errors.md b/docs/errors.md deleted file mode 100644 index 8d1161f1bc..0000000000 --- a/docs/errors.md +++ /dev/null @@ -1,52 +0,0 @@ -# Errors in strawberry - -Strawberry has built-in errors for when something goes wrong with the creation -and usage of the schema. - -It also provides a custom exception handler for improving how errors are printed -and to make it easier to find the exception source, for example the following -code: - -```python -import strawberry - - -@strawberry.type -class Query: - @strawberry.field - def hello_world(self): - return "Hello there!" - - -schema = strawberry.Schema(query=Query) -``` - -will show the following exception on the command line: - -```text - - error: Missing annotation for field `hello_world` - - @ demo.py:7 - - 6 | @strawberry.field - โฑ 7 | def hello_world(self): - ^^^^^^^^^^^ resolver missing annotation - 8 | return "Hello there!" - - - To fix this error you can add an annotation, like so `def hello_world(...) -> str:` - - Read more about this error on https://errors.strawberry.rocks/missing-return-annotation - -``` - -These errors are only enabled when `rich` and `libcst` are installed. You can -install Strawberry with errors enabled by running: - -```bash -pip install "strawberry-graphql[cli]" -``` - -If you want to disable the errors you can do so by setting the -`STRAWBERRY_DISABLE_RICH_ERRORS` environment variable to `1`. diff --git a/docs/errors/_template.md b/docs/errors/_template.md deleted file mode 100644 index c8b44a2aa6..0000000000 --- a/docs/errors/_template.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -title: Some Error ---- - -# Some Error Error - -## Description - -This error is thrown when ... for example the following code will throw this -error: - -```python -import strawberry - -schema = strawberry.Schema(query=Query) -``` diff --git a/docs/errors/duplicated-type-name.md b/docs/errors/duplicated-type-name.md deleted file mode 100644 index d81e655b6e..0000000000 --- a/docs/errors/duplicated-type-name.md +++ /dev/null @@ -1,67 +0,0 @@ ---- -title: Duplicated Type Name Error ---- - -# Duplicated Type Name Error - -## Description - -This error is thrown when you try to register two types with the same name in -the schema. - -For example, the following code will throw this error: - -```python -import strawberry - - -@strawberry.type -class User: - name: str - - -@strawberry.type(name="User") -class UserB: - name: str - - -@strawberry.type -class Query: - user: User - user_b: UserB - - -schema = strawberry.Schema(query=Query) -``` - -## How to fix this error - -To fix this error you need to make sure that all the types in your schema have -unique names. For example in our example above we can fix this error by changing -the `name` argument of the `UserB` type: - -```python -import strawberry - - -@strawberry.type -class User: - name: str - - -# Note: Strawberry will automatically use the name of the class -# if it is not provided, in this case we are passing the name -# to show how it works and how to fix the error -@strawberry.type(name="UserB") -class UserB: - name: str - - -@strawberry.type -class Query: - user: User - user_b: UserB - - -schema = strawberry.Schema(query=Query) -``` diff --git a/docs/errors/invalid-argument-type.md b/docs/errors/invalid-argument-type.md deleted file mode 100644 index 3fb24da83d..0000000000 --- a/docs/errors/invalid-argument-type.md +++ /dev/null @@ -1,41 +0,0 @@ ---- -title: Invalid Argument Type Error ---- - -# Invalid Argument Type Error - -## Description - -This error is thrown when an argument is of the wrong type, it usually happens -when passing **unions** or **interfaces** as an argument, for example the -following code will throw this error: - -```python -import strawberry - - -@strawberry.type -class TypeA: - id: strawberry.ID - - -Example = strawberry.union(name="Example", types=(TypeA,)) - - -@strawberry.type -class Query: - @strawberry.field - def example(self, data: Example) -> str: - return "this is an example" - - -schema = strawberry.Schema(query=Query) -``` - -## Using union types as arguments - -The latest [GraphQL specification](https://spec.graphql.org/October2021/) -doesn't allow using unions as arguments. There's currently an -[RFC for adding a `oneOf` directive](https://github.com/graphql/graphql-spec/pull/825) -that might work for your use case, but it's not yet implemented in the spec and -Strawberry diff --git a/docs/errors/invalid-type-for-union-merge.md b/docs/errors/invalid-type-for-union-merge.md deleted file mode 100644 index 19d3a94642..0000000000 --- a/docs/errors/invalid-type-for-union-merge.md +++ /dev/null @@ -1,66 +0,0 @@ ---- -title: Invalid Type for Union Merge Error ---- - -# Invalid Type for Union Merge Error - -## Description - -This error is thrown when trying to extend an union with a type that's not -allowed in unions, for example the following code will throw this error: - -```python -import strawberry - - -@strawberry.type -class Example: - name: str - - -ExampleUnion = strawberry.union("ExampleUnion", types=(Example,)) - - -@strawberry.type -class Query: - field: ExampleUnion | int - - -schema = strawberry.Schema(query=Query) -``` - -This happens because GraphQL doesn't support scalars as union members. - -## How to fix this error - -At the moment Strawberry doesn't have a proper way to merge unions and types, -but you can still create a union type that combines multiple types manually. -Since GraphQL doesn't allow scalars as union members, a workaround is to create -a wrapper type that contains the scalar value and use that instead. For example -the following code will create a union type between `Example` and `IntWrapper` -which is a wrapper on top of the `int` scalar: - -```python -import strawberry - - -@strawberry.type -class Example: - name: str - - -@strawberry.type -class IntWrapper: - value: int - - -ExampleUnion = strawberry.union("ExampleUnion", types=(Example, IntWrapper)) - - -@strawberry.type -class Query: - field: ExampleUnion - - -schema = strawberry.Schema(query=Query) -``` diff --git a/docs/errors/invalid-union-type.md b/docs/errors/invalid-union-type.md deleted file mode 100644 index 3ee2833c33..0000000000 --- a/docs/errors/invalid-union-type.md +++ /dev/null @@ -1,64 +0,0 @@ ---- -title: Invalid Type for Union Error ---- - -# Invalid Type for Union Error - -## Description - -This error is thrown when trying to create an union with one or more type that's -are allowed in unions, for example the following code will throw this error: - -```python -import strawberry - - -@strawberry.type -class Example: - name: str - - -ExampleUnion = strawberry.union("ExampleUnion", types=(Example, int)) - - -@strawberry.type -class Query: - field: ExampleUnion - - -schema = strawberry.Schema(query=Query) -``` - -This happens because GraphQL doesn't support scalars as union members. - -## How to fix this error - -Since GraphQL doesn't allow scalars as union members, a workaround is to create -a wrapper type that contains the scalar value and use that instead. For example -the following code will create a union type between `Example` and `IntWrapper` -which is a wrapper on top of the `int` scalar: - -```python -import strawberry - - -@strawberry.type -class Example: - name: str - - -@strawberry.type -class IntWrapper: - value: int - - -ExampleUnion = strawberry.union("ExampleUnion", types=(Example, IntWrapper)) - - -@strawberry.type -class Query: - field: ExampleUnion - - -schema = strawberry.Schema(query=Query) -``` diff --git a/docs/errors/missing-arguments-annotations.md b/docs/errors/missing-arguments-annotations.md deleted file mode 100644 index 10179849eb..0000000000 --- a/docs/errors/missing-arguments-annotations.md +++ /dev/null @@ -1,46 +0,0 @@ ---- -title: Missing arguments annotation Error ---- - -# Missing arguments annotation Error - -## Description - -This error is thrown when an argument is missing an annotation, for example the -following code will throw this error: - -```python -import strawberry - - -@strawberry.type -class Query: - @strawberry.field - def hello(self, name) -> str: # <-- note name here is missing an annotation - return f"hello {name}" - - -schema = strawberry.Schema(query=Query) -``` - -This happens because Strawberry needs to know the type of every argument to be able to -generate the correct GraphQL type. - -## How to fix this error - -You can fix this error by adding an annotation to the argument, for example, the -following code will fix this error: - -```python -import strawberry - - -@strawberry.type -class Query: - @strawberry.field - def hello(self, name: str) -> str: - return f"hello {name}" - - -schema = strawberry.Schema(query=Query) -``` diff --git a/docs/errors/missing-field-annotation.md b/docs/errors/missing-field-annotation.md deleted file mode 100644 index e4bbcf3161..0000000000 --- a/docs/errors/missing-field-annotation.md +++ /dev/null @@ -1,46 +0,0 @@ ---- -title: Missing field annotation Error ---- - -# Missing field annotation Error - -## Description - -This error is thrown when a field on a class is missing an annotation, for -example the following code will throw this error: - -```python -import strawberry - - -@strawberry.type -class Query: - name: str - age = strawberry.field( - name="ageInYears" - ) # note that here we don't have a type for this field - - -schema = strawberry.Schema(query=Query) -``` - -This happens because Strawberry needs to know the type of every field for a type -to be able to generate the correct GraphQL type. - -## How to fix this error - -You can fix this error by adding an annotation to the field, for example, the -following code will fix this error: - -```python -import strawberry - - -@strawberry.type -class Query: - name: str - age: int = strawberry.field(name="ageInYears") - - -schema = strawberry.Schema(query=Query) -``` diff --git a/docs/errors/missing-return-annotation.md b/docs/errors/missing-return-annotation.md deleted file mode 100644 index b3e1d0175d..0000000000 --- a/docs/errors/missing-return-annotation.md +++ /dev/null @@ -1,46 +0,0 @@ ---- -title: Missing return annotation Error ---- - -# Missing return annotation Error - -## Description - -This error is thrown when a resolver and it's corresponding field don't have a -return annotation, for example the following code will throw this error: - -```python -import strawberry - - -@strawberry.type -class Query: - @strawberry.field - def example(self): - return "this is an example" - - -schema = strawberry.Schema(query=Query) -``` - -This happens because Strawberry needs to know the return type of the resolver to -be able to generate the correct GraphQL type. - -## How to fix this error - -You can fix this error by adding a return annotation to the resolver, for -example, the following code will fix this error: - -```python -import strawberry - - -@strawberry.type -class Query: - @strawberry.field - def example(self) -> str: - return "this is an example" - - -schema = strawberry.Schema(query=Query) -``` diff --git a/docs/errors/not-a-strawberry-enum.md b/docs/errors/not-a-strawberry-enum.md deleted file mode 100644 index ceaf00c53d..0000000000 --- a/docs/errors/not-a-strawberry-enum.md +++ /dev/null @@ -1,62 +0,0 @@ ---- -title: Not a Strawberry Enum Error ---- - -# Not a Strawberry Enum Error - -## Description - -This error is thrown when trying to use an enum that is not a Strawberry enum, -for example the following code will throw this error: - -```python -import strawberry - - -# note the lack of @strawberry.enum here: -class IceCreamFlavour(Enum): - VANILLA = strawberry.enum_value("vanilla") - STRAWBERRY = strawberry.enum_value( - "strawberry", - description="Our favourite", - ) - CHOCOLATE = "chocolate" - - -@strawberry.type -class Query: - field: IceCreamFlavour - - -schema = strawberry.Schema(query=Query) -``` - -This happens because Strawberry expects all enums to be decorated with -`@strawberry.enum`. - -## How to fix this error - -You can fix this error by making sure the enum you're using is decorated with -`@strawberry.enum`. For example, the following code will fix this error: - -```python -import strawberry - - -@strawberry.enum -class IceCreamFlavour(Enum): - VANILLA = strawberry.enum_value("vanilla") - STRAWBERRY = strawberry.enum_value( - "strawberry", - description="Our favourite", - ) - CHOCOLATE = "chocolate" - - -@strawberry.type -class Query: - field: IceCreamFlavour - - -schema = strawberry.Schema(query=Query) -``` diff --git a/docs/errors/object-is-not-an-enum.md b/docs/errors/object-is-not-an-enum.md deleted file mode 100644 index ce4579c3b5..0000000000 --- a/docs/errors/object-is-not-an-enum.md +++ /dev/null @@ -1,52 +0,0 @@ ---- -title: Object is not an Enum Error ---- - -# Object is not an Enum Error - -## Description - -This error is thrown when applying `@strawberry.enum` to a non-enum object, for -example the following code will throw this error: - -```python -import strawberry - - -@strawberry.enum -class NotAnEnum: - A = "A" - - -@strawberry.type -class Query: - field: NotAnEnum - - -schema = strawberry.Schema(query=Query) -``` - -This happens because Strawberry expects all enums to be subclasses of `Enum`. - -## How to fix this error - -You can fix this error by making sure the class you're applying -`@strawberry.enum` to is a subclass of `Enum`. For example, the following code -will fix this error: - -```python -import strawberry - - -@strawberry.enum -class NotAnEnum: - A = "A" - - -@strawberry.type -class Query: - field: NotAnEnum - - -schema = strawberry.Schema(query=Query) -``` diff --git a/docs/errors/object-is-not-class.md b/docs/errors/object-is-not-class.md deleted file mode 100644 index 02e3406032..0000000000 --- a/docs/errors/object-is-not-class.md +++ /dev/null @@ -1,52 +0,0 @@ ---- -title: Object is not an Class Error ---- - -# Object is not an Class Error - -## Description - -This error is thrown when applying `@strawberry.type/interface/input` to a -non-class object, for example the following code will throw this error: - -```python -import strawberry - - -@strawberry.type -def a_function(): - ... - - -@strawberry.type -class Query: - field: a_function - - -schema = strawberry.Schema(query=Query) -``` - -This happens because Strawberry expects all enums to be subclasses of `Enum`. - -## How to fix this error - -You can fix this error by making sure the class you're applying -`@strawberry.type/interface/input` to is a class. For example, the following -code will fix this error: - -```python -import strawberry - - -@strawberry.type -class AFunction: - field: int - - -@strawberry.type -class Query: - field: AFunction - - -schema = strawberry.Schema(query=Query) -``` diff --git a/docs/errors/private-strawberry-field.md b/docs/errors/private-strawberry-field.md deleted file mode 100644 index 72cbb93e49..0000000000 --- a/docs/errors/private-strawberry-field.md +++ /dev/null @@ -1,56 +0,0 @@ ---- -title: Private Strawberry Field Error ---- - -# Private Strawberry Field Error - -## Description - -This error is thrown when using both `strawberry.Private[Type]` and -`strawberry.field` on the same field, for example the following code will throw -this error: - -```python -import strawberry - - -@strawberry.type -class Query: - name: str - age: strawberry.Private[int] = strawberry.field(name="ageInYears") - - -schema = strawberry.Schema(query=Query) -``` - -This happens because a `strawberry.Private` field is not going to be exposed in -the GraphQL schema, so using `strawberry.field` on that field won't be useful, -since it is meant to be used to change information about a field that is exposed -in the GraphQL schema. - - - -## How to fix this error - -You can fix this error by either removing the `strawberry.Private` annotation or -by removing the `strawberry.field` usage. If you need to specify a default value -using `default_factory` you can use `dataclasses.field` instead of -`strawberry.field`. For example: - -```python -import strawberry -import dataclasses - - -@strawberry.type -class Query: - name: str - tags: strawberry.Private[str] = dataclasses.field(default_factory=list) - - -schema = strawberry.Schema(query=Query) -``` diff --git a/docs/errors/scalar-already-registered.md b/docs/errors/scalar-already-registered.md deleted file mode 100644 index 0835801684..0000000000 --- a/docs/errors/scalar-already-registered.md +++ /dev/null @@ -1,70 +0,0 @@ ---- -title: Scalar already registered Error ---- - -# Scalar already registered Error - -## Description - -This error is thrown when trying to use a scalar that is already registered. -This usually happens when using the same name for different scalars, for example -the following code will throw this error: - -```python -import strawberry - -MyCustomScalar = strawberry.scalar( - str, - name="MyCustomScalar", -) - -MyCustomScalar2 = strawberry.scalar( - int, - name="MyCustomScalar", -) - - -@strawberry.type -class Query: - scalar_1: MyCustomScalar - scalar_2: MyCustomScalar2 - - -strawberry.Schema(Query) -``` - -This happens because different types in Strawberry (and GraphQL) cannot have the -same name. - - - -## How to fix this error - -You can fix this error by either reusing the existing scalar, or by changing the -name of one of them, for example in this code we renamed the second scalar: - -```python -import strawberry - -MyCustomScalar = strawberry.scalar( - str, - name="MyCustomScalar", -) - -MyCustomScalar2 = strawberry.scalar( - int, - name="MyCustomScalar2", -) - - -@strawberry.type -class Query: - scalar_1: MyCustomScalar - scalar_2: MyCustomScalar2 - - -strawberry.Schema(Query) -``` diff --git a/docs/errors/unresolved-field-type.md b/docs/errors/unresolved-field-type.md deleted file mode 100644 index bcb781896c..0000000000 --- a/docs/errors/unresolved-field-type.md +++ /dev/null @@ -1,54 +0,0 @@ ---- -title: Unresolved Field Type Error ---- - -# Unresolved Field Type Error - -## Description - -This error is thrown when Strawberry is unable to resolve a field type. This -happens when the type of a field is not accessible in the current scope. For -example the following code will throw this error: - -```python -import strawberry - - -@strawberry.type -class Query: - user: "User" - - -schema = strawberry.Schema(query=Query) -``` - - - -Note that we are using the forward reference syntax to define the type of the -field. This is because the `User` type is not yet defined when the `Query` type -is defined. - -This would also happen when using `from __future__ import annotations`. - - - -To fix this error you need to import the type that you are using in the field, -for example: - -```python -import strawberry -from .user import User - - -@strawberry.type -class Query: - user: "User" - - -schema = strawberry.Schema(query=Query) -``` - -Unfortunately, this won't work in cases where there's a circular dependency -between types. In this case, you can use `strawberry.LazyType`. - - diff --git a/docs/errors/unsupported-type.md b/docs/errors/unsupported-type.md deleted file mode 100644 index 6783c3a36c..0000000000 --- a/docs/errors/unsupported-type.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: Unsupported Type Error ---- - -# Unsupported Type Error - -## Description - -This error is thrown when trying to convert arguments with a type that -Strawberry doesn't know about. It shouldn't happen with normal usage of -Strawberry. diff --git a/docs/extensions/_template.md b/docs/extensions/_template.md index 568761e48a..e39bf087d6 100644 --- a/docs/extensions/_template.md +++ b/docs/extensions/_template.md @@ -18,15 +18,14 @@ schema = strawberry.Schema( Query, extensions=[ ExtensionName(), - ], + ] ) ``` ## API reference: ```python -class ExtensionName(an_argument=None): - ... +class ExtensionName(an_argument=None)` ``` #### `an_argument: Optional[str] = None` @@ -46,7 +45,7 @@ schema = strawberry.Schema( Query, extensions=[ ExtensionName(an_argument="something"), - ], + ] ) ``` diff --git a/docs/extensions/add-validation-rules.md b/docs/extensions/add-validation-rules.md index 32b802b885..0b582b3335 100644 --- a/docs/extensions/add-validation-rules.md +++ b/docs/extensions/add-validation-rules.md @@ -17,24 +17,21 @@ import strawberry from strawberry.extensions import AddValidationRules from graphql import ValidationRule - class MyCustomRule(ValidationRule): ... - schema = strawberry.Schema( Query, extensions=[ AddValidationRules(MyCustomRule), - ], + ] ) ``` ## API reference: ```python -class AddValidationRules(validation_rules): - ... +class AddValidationRules(validation_rules) ``` #### `validation_rules: List[Type[ASTValidationRule]]` @@ -51,18 +48,16 @@ import strawberry from strawberry.extensions import AddValidationRules from graphql import ValidationRule - class CustomRule(ValidationRule): def enter_field(self, node, *args) -> None: if node.name.value == "example": self.report_error(GraphQLError("Can't query field 'example'")) - schema = strawberry.Schema( Query, extensions=[ AddValidationRules([CustomRule]), - ], + ] ) result = schema.execute_sync("{ example }") @@ -84,7 +79,7 @@ schema = strawberry.Schema( Query, extensions=[ AddValidationRules([NoDeprecatedCustomRule]), - ], + ] ) ``` @@ -102,7 +97,7 @@ schema = strawberry.Schema( Query, extensions=[ AddValidationRules([NoSchemaIntrospectionCustomRule]), - ], + ] ) ``` diff --git a/docs/extensions/apollo-tracing.md b/docs/extensions/apollo-tracing.md index a29fc51a90..ca479ad6b7 100644 --- a/docs/extensions/apollo-tracing.md +++ b/docs/extensions/apollo-tracing.md @@ -18,7 +18,7 @@ schema = strawberry.Schema( Query, extensions=[ ApolloTracingExtension, - ], + ] ) ``` @@ -34,7 +34,7 @@ schema = strawberry.Schema( Query, extensions=[ ApolloTracingExtensionSync, - ], + ] ) ``` diff --git a/docs/extensions/datadog.md b/docs/extensions/datadog.md deleted file mode 100644 index 90fb1377dd..0000000000 --- a/docs/extensions/datadog.md +++ /dev/null @@ -1,52 +0,0 @@ ---- -title: DatadogExtension -summary: Add Datadog tracing to your GraphQL server. -tags: tracing ---- - -# `DatadogExtension` - -This extension adds support for tracing with Datadog. - - - -Make sure you have `ddtrace` installed before using this extension. - -``` -pip install ddtrace -``` - - - -## Usage example: - -```python -import strawberry -from strawberry.extensions.tracing import DatadogTracingExtension - -schema = strawberry.Schema( - Query, - extensions=[ - DatadogTracingExtension, - ], -) -``` - - - -If you are not running in an Async context then you'll need to use the sync -version: - -```python -import strawberry -from strawberry.extensions.tracing import DatadogTracingExtensionSync - -schema = strawberry.Schema( - Query, - extensions=[ - DatadogTracingExtensionSync, - ], -) -``` - - diff --git a/docs/extensions/disable-validation.md b/docs/extensions/disable-validation.md index 4b2ecc7711..98b8490c10 100644 --- a/docs/extensions/disable-validation.md +++ b/docs/extensions/disable-validation.md @@ -26,7 +26,7 @@ schema = strawberry.Schema( Query, extensions=[ DisableValidation(), - ], + ] ) ``` diff --git a/docs/extensions/mask-errors.md b/docs/extensions/mask-errors.md deleted file mode 100644 index 7f6910eb90..0000000000 --- a/docs/extensions/mask-errors.md +++ /dev/null @@ -1,99 +0,0 @@ ---- -title: MaskErrors -summary: Hide error messages from the client. -tags: security ---- - -# `MaskErrors` - -This extension hides error messages from the client to prevent exposing -sensitive details. By default it masks all errors raised in any field resolver. - -## Usage example: - -```python -import strawberry -from strawberry.extensions import MaskErrors - -schema = strawberry.Schema( - Query, - extensions=[ - MaskErrors(), - ], -) -``` - -## API reference: - -```python -class MaskErrors( - should_mask_error=default_should_mask_error, error_message="Unexpected error." -): - ... -``` - -#### `should_mask_error: Callable[[GraphQLError], bool] = default_should_mask_error` - -Predicate function to check if a GraphQLError should be masked or not. Use the -`original_error` attribute to access the original error that was raised in the -resolver. - - - -The `default_should_mask_error` function always returns `True`. - - - -#### `error_message: str = "Unexpected error."` - -The error message to display to the client when there is an error. - -## More examples: - -
- Hide some exceptions - -```python -import strawberry -from strawberry.extensions import MaskErrors -from graphql.error import GraphQLError - - -class VisibleError(Exception): - pass - - -def should_mask_error(error: GraphQLError) -> bool: - original_error = error.original_error - if original_error and isinstance(original_error, VisibleError): - return False - - return True - - -schema = strawberry.Schema( - Query, - extensions=[ - MaskErrors(should_mask_error=should_mask_error), - ], -) -``` - -
- -
- Change error message - -```python -import strawberry -from strawberry.extensions import MaskErrors - -schema = strawberry.Schema( - Query, - extensions=[ - MaskErrors(error_message="Oh no! An error occured. Very sorry about that."), - ], -) -``` - -
diff --git a/docs/extensions/opentelemetry.md b/docs/extensions/opentelemetry.md index 6333ddced9..3508dc8eea 100644 --- a/docs/extensions/opentelemetry.md +++ b/docs/extensions/opentelemetry.md @@ -6,8 +6,7 @@ tags: tracing # `OpenTelemetryExtension` -This extension adds tracing information that is compatible with -[Open Telemetry](https://opentelemetry.io/). +This extension adds tracing information that is compatible with [Open Telemetry](https://opentelemetry.io/). @@ -29,14 +28,13 @@ schema = strawberry.Schema( Query, extensions=[ OpenTelemetryExtension, - ], + ] ) ``` -If you are not running in an Async context then you'll need to use the sync -version: +If you are not running in an Async context then you'll need to use the sync version: ```python import strawberry @@ -46,7 +44,7 @@ schema = strawberry.Schema( Query, extensions=[ OpenTelemetryExtensionSync, - ], + ] ) ``` @@ -55,8 +53,7 @@ schema = strawberry.Schema( ## API reference: ```python -class OpenTelemetryExtension(arg_filter=None): - ... +class OpenTelemetryExtension(arg_filter=None) ``` #### `arg_filter: Optional[ArgFilter]` @@ -65,7 +62,10 @@ A function to filter certain field arguments from being included in the tracing data. ```python -ArgFilter = Callable[[Dict[str, Any], GraphQLResolveInfo], Dict[str, Any]] +ArgFilter = Callable[ + [Dict[str, Any], GraphQLResolveInfo], + Dict[str, Any] +] ``` ## More examples: @@ -77,7 +77,6 @@ ArgFilter = Callable[[Dict[str, Any], GraphQLResolveInfo], Dict[str, Any]] import strawberry from strawberry.extensions.tracing import OpenTelemetryExtensionSync - def arg_filter(kwargs, info): filtered_kwargs = {} for name, value in kwargs: @@ -88,14 +87,13 @@ def arg_filter(kwargs, info): return filtered_kwargs - schema = strawberry.Schema( Query, extensions=[ OpenTelemetryExtensionSync( - arg_filter=arg_filter, + arg_filter=arg_filter, ), - ], + ] ) ``` diff --git a/docs/extensions/parser-cache.md b/docs/extensions/parser-cache.md index 61e7370577..b8a8502506 100644 --- a/docs/extensions/parser-cache.md +++ b/docs/extensions/parser-cache.md @@ -6,8 +6,7 @@ tags: performance,caching,parsing # `ParserCache` -This extension adds LRU caching to the parsing step of query execution to -improve performance by caching the parsed result in memory. +This extension adds LRU caching to the parsing step of query execution to improve performance by caching the parsed result in memory. ## Usage example: @@ -19,21 +18,19 @@ schema = strawberry.Schema( Query, extensions=[ ParserCache(), - ], + ] ) ``` ## API reference: ```python -class ParserCache(maxsize=None): - ... +class ParserCache(maxsize=None) ``` #### `maxsize: Optional[int] = None` -Set the maxsize of the cache. If `maxsize` is set to `None` then the cache will -grow without bound. +Set the maxsize of the cache. If `maxsize` is set to `None` then the cache will grow without bound. More info: https://docs.python.org/3/library/functools.html#functools.lru_cache @@ -50,7 +47,7 @@ schema = strawberry.Schema( Query, extensions=[ ParserCache(maxsize=100), - ], + ] ) ``` diff --git a/docs/extensions/query-depth-limiter.md b/docs/extensions/query-depth-limiter.md index a159cc43d6..46159e8650 100644 --- a/docs/extensions/query-depth-limiter.md +++ b/docs/extensions/query-depth-limiter.md @@ -18,15 +18,14 @@ schema = strawberry.Schema( Query, extensions=[ QueryDepthLimiter(max_depth=10), - ], + ] ) ``` ## API reference: ```python -class QueryDepthLimiter(max_depth, ignore=None, callback=None): - ... +class QueryDepthLimiter(max_depth, ignore=None, callback=None) ``` #### `max_depth: int` @@ -56,13 +55,15 @@ from strawberry.extensions import QueryDepthLimiter schema = strawberry.Schema( Query, extensions=[ - QueryDepthLimiter(max_depth=2, ignore=["user"]), - ], + QueryDepthLimiter( + max_depth=2, + ignore=["user"] + ), + ] ) # This query fails -schema.execute( - """ +schema.execute(""" query TooDeep { book { author { @@ -72,12 +73,10 @@ schema.execute( } } } -""" -) +""") # This query succeeds because the `user` field is ignored -schema.execute( - """ +schema.execute(""" query NotTooDeep { user { favouriteBooks { @@ -89,8 +88,7 @@ schema.execute( } } } -""" -) +""") ``` @@ -106,13 +104,15 @@ from strawberry.extensions import QueryDepthLimiter schema = strawberry.Schema( Query, extensions=[ - QueryDepthLimiter(max_depth=2, ignore=[re.compile(r".*favourite.*")]), - ], + QueryDepthLimiter( + max_depth=2, + ignore=[re.compile(r".*favourite.*"] + ), + ] ) # This query succeeds because an field that contains `favourite` is ignored -schema.execute( - """ +schema.execute(""" query NotTooDeep { user { favouriteBooks { @@ -124,8 +124,7 @@ schema.execute( } } } -""" -) +""") ``` @@ -141,13 +140,13 @@ schema = strawberry.Schema( Query, extensions=[ QueryDepthLimiter( - max_depth=2, ignore=[lambda field_name: field_name == "user"] + max_depth=2, + ignore=[lambda field_name: field_name == "user"] ), - ], + ] ) -schema.execute( - """ +schema.execute(""" query NotTooDeep { user { favouriteBooks { @@ -159,8 +158,7 @@ schema.execute( } } } -""" -) +""") ``` diff --git a/docs/extensions/validation-cache.md b/docs/extensions/validation-cache.md index 42254fed80..fd73f75472 100644 --- a/docs/extensions/validation-cache.md +++ b/docs/extensions/validation-cache.md @@ -18,15 +18,14 @@ schema = strawberry.Schema( Query, extensions=[ ValidationCache(), - ], + ] ) ``` ## API reference: ```python -class ValidationCache(maxsize=None): - ... +class ValidationCache(maxsize=None) ``` #### `maxsize: Optional[int] = None` @@ -48,7 +47,7 @@ schema = strawberry.Schema( Query, extensions=[ ValidationCache(maxsize=100), - ], + ] ) ``` diff --git a/docs/faq.md b/docs/faq.md deleted file mode 100644 index 37be476dd8..0000000000 --- a/docs/faq.md +++ /dev/null @@ -1,91 +0,0 @@ ---- -title: FAQ -faq: true ---- - -# Frequently Asked Questions - -## How can I hide a field from GraphQL? - -Strawberry provides a `Private` type that can be used to hide fields from -GraphQL, for example, the following code: - -```python -import strawberry - - -@strawberry.type -class User: - name: str - age: int - password: strawberry.Private[str] - - -@strawberry.type -class Query: - @strawberry.field - def user(self) -> User: - return User(name="Patrick", age=100, password="This is fake") - - -schema = strawberry.Schema(query=Query) -``` - -will result in the following schema: - -```graphql -type Query { - user: User! -} - -type User { - name: String! - age: Int! -} -``` - -## How can I deal with circular imports? - -In cases where you have circular imports, you can use `strawberry.lazy` to -resolve the circular imports, for example: - -```python -# posts.py -from typing import TYPE_CHECKING, Annotated - -import strawberry - -if TYPE_CHECKING: - from .users import User - - -@strawberry.type -class Post: - title: str - author: Annotated["User", strawberry.lazy(".users")] -``` - -For more information, see the [Lazy types](./types/lazy.md) documentation. - -## Can I reuse Object Types with Input Objects? - -Unfortunately not because, as the -[GraphQL spec](https://spec.graphql.org/June2018/#sec-Input-Objects) specifies, -there is a difference between Objects Types and Inputs types: - -> The GraphQL Object type (ObjectTypeDefinition) defined above is inappropriate -> for reโ€use here, because Object types can contain fields that define arguments -> or contain references to interfaces and unions, neither of which is -> appropriate for use as an input argument. For this reason, input objects have -> a separate type in the system. - -And this is also true for Input types' fields: you can only use Strawberry Input -types or scalar. - -See our [Input Types](./types/input-types.md) docs. - -## Can I use asyncio with Strawberry and Django? - -Yes, Strawberry provides an async view that can be used with Django, you can -Check [Async Django](./integrations/django.md#async-django) for more -information. diff --git a/docs/general/mutations.md b/docs/general/mutations.md index df4149838d..39dbf292c5 100644 --- a/docs/general/mutations.md +++ b/docs/general/mutations.md @@ -17,7 +17,6 @@ implement a mutation that is supposed to add a book: ```python import strawberry - # Reader, you can safely ignore Query in this example, it is required by # strawberry.Schema so it is included here for completeness @strawberry.type @@ -26,16 +25,14 @@ class Query: def hello() -> str: return "world" - @strawberry.type class Mutation: @strawberry.mutation def add_book(self, title: str, author: str) -> Book: - print(f"Adding {title} by {author}") + print(f'Adding {title} by {author}') return Book(title=title, author=author) - schema = strawberry.Schema(query=Query, mutation=Mutation) ``` diff --git a/docs/general/queries.md b/docs/general/queries.md index 97d01349fb..c638d30c91 100644 --- a/docs/general/queries.md +++ b/docs/general/queries.md @@ -16,7 +16,6 @@ This is how you define a root query type in Strawberry: class Query: name: str - schema = strawberry.Schema(query=Query) ``` @@ -34,12 +33,10 @@ name: def get_name() -> str: return "Strawberry" - @strawberry.type class Query: name: str = strawberry.field(resolver=get_name) - schema = strawberry.Schema(query=Query) ``` diff --git a/docs/general/schema-basics.md b/docs/general/schema-basics.md index 25ca8e2992..307f1f7190 100644 --- a/docs/general/schema-basics.md +++ b/docs/general/schema-basics.md @@ -60,17 +60,15 @@ look like this in Strawberry import typing import strawberry - @strawberry.type class Book: - title: str - author: "Author" - + title: str + author: 'Author' @strawberry.type class Author: - name: str - books: typing.List["Book"] + name: str + books: typing.List['Book'] ``` As you can see the code maps almost one to one with the schema, thanks to @@ -129,73 +127,17 @@ Object types can refer to each other, as we had in our schema earlier: import typing import strawberry - @strawberry.type class Book: - title: str - author: "Author" - + title: str + author: 'Author' @strawberry.type class Author: - name: str - books: typing.List[Book] + name: str + books: typing.List[Book] ``` -## Providing data to fields - -In the above schema, a `Book` has an `author` field and an `Author` has a `books` -field, yet we do not know how our data can be mapped to fulfil the structure of -the promised schema. - -To achieve this, we introduce the concept of the [_resolver_](../types/resolvers.md) that provides some -data to a field through a function. - -Continuing with this example of books and authors, resolvers can be defined -to provides values to the fields: - -```python -def get_author_for_book(root) -> "Author": - return Author(name="Michael Crichton") - - -@strawberry.type -class Book: - title: str - author: "Author" = strawberry.field(resolver=get_author_for_book) - - -def get_books_for_author(root): - return [Book(title="Jurassic Park")] - - -@strawberry.type -class Author: - name: str - books: typing.List[Book] = strawberry.field(resolver=get_books_for_author) - - -def get_authors(root) -> typing.List[Author]: - return [Author(name="Michael Crichton")] - - -@strawberry.type -class Query: - authors: typing.List[Author] = strawberry.field(resolver=get_authors) - books: typing.List[Book] = strawberry.field(resolver=get_books_for_author) -``` - -These functions provide the `strawberry.field` with the ability to render data -to the GraphQL query upon request and are the backbone of all GraphQL APIs. - -This example is trivial since the resolved data is entirely static. However, -when building more complex APIs, these resolvers can be written to map data -from databases, e.g. making SQL queries using SQLAlchemy, and other APIs, -e.g. making HTTP requests using aiohttp. - -For more information and detail on the different ways to write resolvers, -see the [resolvers section](../types/resolvers.md). - ## The Query type The `Query` type defines exactly which GraphQL queries (i.e., read operations) @@ -296,9 +238,9 @@ the following: ```python @strawberry.type class Mutation: - @strawberry.field - def add_book(self, title: str, author: str) -> Book: - ... + @strawberry.field + def add_book(self, title: str, author: str) -> Book: + ... ``` This Mutation type defines a single available mutation, `addBook`. The mutation @@ -357,9 +299,9 @@ Consider our previous mutation to add a book: ```python @strawberry.type class Mutation: - @strawberry.field - def add_book(self, title: str, author: str) -> Book: - ... + @strawberry.field + def add_book(self, title: str, author: str) -> Book: + ... ``` Instead of accepting two arguments, this mutation could accept a single input @@ -372,15 +314,15 @@ keyword: ```python @strawberry.input class AddBookInput: - title: str - author: str + title: str + author: str @strawberry.type class Mutation: - @strawberry.field - def add_book(self, book: AddBookInput) -> Book: - ... + @strawberry.field + def add_book(self, book: AddBookInput) -> Book: + ... ``` Not only does this facilitate passing the AddBookInput type around within @@ -390,8 +332,8 @@ that are automatically exposed by GraphQL-enabled tools: ```python @strawberry.input class AddBookInput: - title: str = strawberry.field(description="The title of the book") - author: str = strawberry.field(description="The name of the author") + title: str = strawberry.field(description="The title of the book") + author: str = strawberry.field(description="The name of the author") ``` Input types can sometimes be useful when multiple operations require the exact diff --git a/docs/general/subscriptions.md b/docs/general/subscriptions.md index 28e1f165e5..5899668962 100644 --- a/docs/general/subscriptions.md +++ b/docs/general/subscriptions.md @@ -12,27 +12,23 @@ This is how you define a subscription-capable resolver: ```python import asyncio -from typing import AsyncGenerator import strawberry - @strawberry.type class Query: @strawberry.field - def hello(self) -> str: + def hello() -> str: return "world" - @strawberry.type class Subscription: @strawberry.subscription - async def count(self, target: int = 100) -> AsyncGenerator[int, None]: + async def count(self, target: int = 100) -> int: for i in range(target): yield i await asyncio.sleep(0.5) - schema = strawberry.Schema(query=Query, subscription=Subscription) ``` @@ -40,14 +36,6 @@ Like queries and mutations, subscriptions are defined in a class and passed to the Schema function. Here we create a rudimentary counting function which counts from 0 to the target sleeping between each loop iteration. - - -The return type of `count` is `AsyncGenerator` where the first generic -argument is the actual type of the response, in most cases the second argument -should be left as `None` (more about Generator typing [here](https://docs.python.org/3/library/typing.html#typing.AsyncGenerator)). - - - We would send the following GraphQL document to our server to subscribe to this data stream: @@ -65,83 +53,6 @@ This is a very short example of what is possible. Like with queries and mutations the subscription can return any GraphQL type, not only scalars as demonstrated here. -## Authenticating Subscriptions - -Without going into detail on [why](https://github.com/websockets/ws/issues/467), custom headers cannot be set on websocket -requests that originate in browsers. -Therefore, when making any GraphQL requests that rely on a websocket connection, header-based authentication is impossible. - -Other popular GraphQL solutions, like Apollo for example, -implement functionality to pass information from the client to the server at the point of websocket -connection initialisation. In this way, information that is relevant to the websocket connection -initialisation and to the lifetime of the connection overall -can be passed to the server before any data is streamed back by the server. As such, it is not limited to only authentication credentials! - -Strawberry's implementation follows that of Apollo's, which as documentation for -[client](https://www.apollographql.com/docs/react/data/subscriptions/#5-authenticate-over-websocket-optional) -and [server](https://www.apollographql.com/docs/apollo-server/data/subscriptions/#operation-context) -implementations, by reading the contents of the initial -websocket connection message into the `info.context` object. - -With Apollo-client as an example of how to send this initial connection information, one defines a `ws-link` as: - -```javascript -import { GraphQLWsLink } from "@apollo/client/link/subscriptions"; -import { createClient } from "graphql-ws"; - -const wsLink = new GraphQLWsLink( - createClient({ - url: "ws://localhost:4000/subscriptions", - connectionParams: { - authToken: "Bearer I_AM_A_VALID_AUTH_TOKEN", - }, - }), -); -``` - -and then, upon the establishment of the Susbcription request and underlying websocket connection, -Strawberry injects this `connectionParams` object as follows: - -```python -import asyncio -from typing import AsyncGenerator - -import strawberry -from strawberry.types import Info - -from .auth import authenticate_token - - -@strawberry.type -class Query: - @strawberry.field - def hello(self) -> str: - return "world" - - -@strawberry.type -class Subscription: - @strawberry.subscription - async def count(self, info: Info, target: int = 100) -> AsyncGenerator[int, None]: - connection_params: dict = info.context.get("connection_params") - token: str = connection_params.get( - "authToken" - ) # equal to "Bearer I_AM_A_VALID_AUTH_TOKEN" - if not authenticate_token(token): - raise Exception("Forbidden!") - for i in range(target): - yield i - await asyncio.sleep(0.5) - - -schema = strawberry.Schema(query=Query, subscription=Subscription) -``` - -Strawberry expects the `connection_params` object to be any type, so the client is free to send -any valid JSON object as the initial message of the websocket connection, which is abstracted -as `connectionParams` in Apollo-client, and it will be successfully -injected into the `info.context` object. It is then up to you to handle it correctly! - ## Advanced Subscription Patterns Typically a GraphQL subscription is streaming something more interesting back. @@ -172,7 +83,7 @@ from typing import Any, AsyncGenerator, AsyncIterator, Coroutine, Optional async def wait_for_call(coro: Coroutine[Any, Any, bytes]) -> Optional[bytes]: """ - wait_for_call calls the supplied coroutine in a wait_for block. + wait_for_call calls the the supplied coroutine in a wait_for block. This mitigates cases where the coroutine doesn't yield until it has completed its task. In this case, reading a line from a StreamReader; if @@ -225,14 +136,12 @@ async def tail(proc: subprocess.Process) -> AsyncGenerator[str, None]: async for l in lines(proc.stdout): yield l - @strawberry.type class Query: @strawberry.field def hello() -> str: return "world" - @strawberry.type class Subscription: @strawberry.subscription @@ -254,14 +163,6 @@ the newer recommended [graphql-transport-ws](https://github.com/enisdenjo/graphql-ws) WebSocket sub-protocols. - - -The `graphql-transport-ws` protocols repository is called `graphql-ws`. -However, `graphql-ws` is also the name of the legacy protocol. -This documentation always refers to the protocol names. - - - Note that the `graphql-ws` sub-protocol is mainly supported for backwards compatibility. Read the [graphql-ws-transport protocols announcement](https://the-guild.dev/blog/graphql-over-websockets) @@ -279,9 +180,10 @@ from strawberry.subscriptions import GRAPHQL_TRANSPORT_WS_PROTOCOL, GRAPHQL_WS_P from api.schema import schema -view = GraphQLView( - schema, subscription_protocols=[GRAPHQL_TRANSPORT_WS_PROTOCOL, GRAPHQL_WS_PROTOCOL] -) +view = GraphQLView(schema, subscription_protocols=[ + GRAPHQL_TRANSPORT_WS_PROTOCOL, + GRAPHQL_WS_PROTOCOL +]) ``` ##### ASGI @@ -292,67 +194,8 @@ from strawberry.subscriptions import GRAPHQL_TRANSPORT_WS_PROTOCOL, GRAPHQL_WS_P from api.schema import schema -app = GraphQL( - schema, - subscription_protocols=[ - GRAPHQL_TRANSPORT_WS_PROTOCOL, - GRAPHQL_WS_PROTOCOL, - ], -) -``` - -##### Django + Channels - -```python -import os - -from django.core.asgi import get_asgi_application -from strawberry.channels import GraphQLProtocolTypeRouter - -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mysite.settings") -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 mysite.graphql import schema - - -application = GraphQLProtocolTypeRouter( - schema, - django_application=django_asgi_app, -) +app = GraphQL(schema, subscription_protocols=[ + GRAPHQL_TRANSPORT_WS_PROTOCOL, + GRAPHQL_WS_PROTOCOL, +]) ``` - -Note: Check the [channels integraton](/docs/integrations/channels.md) page for more information -regarding it. - -#### FastAPI - -```python -from strawberry.fastapi import GraphQLRouter -from fastapi import FastAPI -from api.schema import schema - -graphql_router = GraphQLRouter( - schema, - subscription_protocols=[ - GRAPHQL_TRANSPORT_WS_PROTOCOL, - GRAPHQL_WS_PROTOCOL, - ], -) -app = FastAPI() -app.include_router(graphql_router, prefix="/graphql") -``` - -### Single result operations - -In addition to _streaming operations_ (i.e. subscriptions), -the `graphql-transport-ws` protocol supports so called _single result operations_ (i.e. queries and mutations). - -This enables clients to use one protocol and one connection for queries, mutations and subscriptions. -Take a look at the [protocols repository](https://github.com/enisdenjo/graphql-ws) -to learn how to correctly set up the graphql client of your choice. - -Strawberry supports single result operations out of the box when the `graphql-transport-ws` protocol is enabled. -Single result operations are normal queries and mutations, so there is no need to adjust any resolvers. diff --git a/docs/guides/authentication.md b/docs/guides/authentication.md index c7a6c9351d..3af41ac97d 100644 --- a/docs/guides/authentication.md +++ b/docs/guides/authentication.md @@ -39,6 +39,7 @@ LoginResult = strawberry.union("LoginResult", (LoginSuccess, LoginError)) class Mutation: @strawberry.field def login(self, username: str, password: str) -> LoginResult: + # Your domain-specific authentication logic would go here user = ... @@ -47,60 +48,3 @@ class Mutation: return LoginSuccess(user=User(username=username)) ``` - -### Access authenticated user in resolver - -Its fairly common to require user information within a resolver. We can do that in a type safe way with a custom context dataclass. - -For example, in FastAPI this might look like this: - -```python -from functools import cached_property - -import strawberry -from fastapi import FastAPI -from strawberry.fastapi import BaseContext, GraphQLRouter -from strawberry.types import Info as _Info -from strawberry.types.info import RootValueType - - -@strawberry.type -class User: - ... # This is just a stub for an actual user object - - -class Context(BaseContext): - @cached_property - def user(self) -> User | None: - if not self.request: - return None - - authorization = self.request.headers.get("Authorization", None) - return authorization_service.authorize(authorization) - - -Info = _Info[Context, RootValueType] - - -@strawberry.type -class Query: - @strawberry.field - def get_authenticated_user(self, info: Info) -> User | None: - return info.context.user - - -async def get_context() -> Context: - return Context() - - -schema = strawberry.Schema(Query) - - -graphql_app = GraphQLRouter( - schema, - context_getter=get_context, -) - -app = FastAPI() -app.include_router(graphql_app, prefix="/graphql") -``` diff --git a/docs/guides/convert-to-dictionary.md b/docs/guides/convert-to-dictionary.md deleted file mode 100644 index 876fad5466..0000000000 --- a/docs/guides/convert-to-dictionary.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -title: Convert to Dictionary ---- - -# Convert to Dictionary - -Strawberry provides a utility function to convert a Strawberry object to a -dictionary. - -You can use `strawberry.asdict(...)` function: - -```python -@strawberry.type -class User: - name: str - age: int - - -# should be {"name": "Lorem", "age": 25} -user_dict = strawberry.asdict(User(name="Lorem", age=25)) -``` diff --git a/docs/guides/custom-extensions.md b/docs/guides/custom-extensions.md index 86ee408480..254682c6e4 100644 --- a/docs/guides/custom-extensions.md +++ b/docs/guides/custom-extensions.md @@ -15,11 +15,11 @@ class: import strawberry from strawberry.extensions import Extension - class MyExtension(Extension): def get_results(self): - return {"example": "this is an example for an extension"} - + return { + "example": "this is an example for an extension" + } schema = strawberry.Schema(query=Query, extensions=[MyExtension]) ``` @@ -34,49 +34,14 @@ starts and ends. Both methods can alternatively be implemented asynchronously. ```python from strawberry.extensions import Extension - class MyExtension(Extension): def on_request_start(self): - print("GraphQL request start") - - def on_request_end(self): - print("GraphQL request end") -``` - -
- Extend error response format + print('GraphQL request start') -```python -class ExtendErrorFormat(Extension): def on_request_end(self): - result = self.execution_context.result - if getattr(result, "errors", None): - result.errors = [ - StrawberryGraphQLError( - extensions={"additional_key": "additional_value"}, - nodes=error.nodes, - source=error.source, - positions=error.positions, - path=error.path, - original_error=error.original_error, - message=error.message, - ) - for error in result.errors - ] - - -@strawberry.type -class Query: - @strawberry.field - def ping(self) -> str: - raise Exception("This error occurred while querying the ping field") - - -schema = strawberry.Schema(query=Query, extensions=[ExtendErrorFormat]) + print('GraphQL request end') ``` -
- ### Resolve `resolve` can be used to run code before or after the execution of resolvers, this @@ -89,7 +54,6 @@ Note that `resolve` can also be implemented asynchronously. from strawberry.types import Info from strawberry.extensions import Extension - class MyExtension(Extension): def resolve(self, _next, root, info: Info, *args, **kwargs): return _next(root, info, *args, **kwargs) @@ -104,7 +68,6 @@ resolving to a dictionary of data that will be included in the GraphQL response. from typing import Any, Dict from strawberry.extensions import Extension - class MyExtension(Extension): def get_results(self) -> Dict[str, Any]: return {} @@ -118,13 +81,12 @@ step of the GraphQL execution. Both methods can be implemented asynchronously. ```python from strawberry.extensions import Extension - class MyExtension(Extension): def on_validation_start(self): - print("GraphQL validation start") + print('GraphQL validation start') def on_validation_end(self): - print("GraphQL validation end") + print('GraphQL validation end') ``` ### Parsing @@ -135,13 +97,12 @@ the GraphQL execution. Both methods can be implemented asynchronously. ```python from strawberry.extensions import Extension - class MyExtension(Extension): def on_parsing_start(self): - print("GraphQL parsing start") + print('GraphQL parsing start') def on_parsing_end(self): - print("GraphQL parsing end") + print('GraphQL parsing end') ``` ### Execution @@ -152,13 +113,12 @@ the GraphQL execution. Both methods can be implemented asynchronously. ```python from strawberry.extensions import Extension - class MyExtension(Extension): def on_executing_start(self): - print("GraphQL execution start") + print('GraphQL execution start') def on_executing_end(self): - print("GraphQL execution end") + print('GraphQL execution end') ``` #### Examples: @@ -174,7 +134,6 @@ from strawberry.extensions import Extension # Use an actual cache in production so that this doesn't grow unbounded response_cache = {} - class ExecutionCache(Extension): def on_executing_start(self): # Check if we've come across this query before @@ -195,7 +154,7 @@ schema = strawberry.Schema( Query, extensions=[ ExecutionCache, - ], + ] ) ``` @@ -208,7 +167,6 @@ schema = strawberry.Schema( import strawberry from strawberry.extensions import Extension - class RejectSomeQueries(Extension): def on_executing_start(self): # Reject all operations called "RejectMe" @@ -224,7 +182,7 @@ schema = strawberry.Schema( Query, extensions=[ RejectSomeQueries, - ], + ] ) ``` @@ -244,7 +202,6 @@ from strawberry.extensions import Extension from mydb import get_db_session - class MyExtension(Extension): def on_request_start(self): self.execution_context.context["db"] = get_db_session() diff --git a/docs/guides/dataloaders.md b/docs/guides/dataloaders.md index 80f08f6a2b..2a57404e42 100644 --- a/docs/guides/dataloaders.md +++ b/docs/guides/dataloaders.md @@ -14,10 +14,6 @@ DataLoaders provide an async API, so they only work in async context
-Refer the official DataLoaders -[specification](https://github.com/graphql/dataloader) for an advanced guide on -DataLoaders. - ## Basic usage Here's how you'd use a DataLoader, first we need to define a function that @@ -27,7 +23,6 @@ only an id: ```python import strawberry - @strawberry.type class User: id: strawberry.ID @@ -39,7 +34,6 @@ keys passed: ```python from typing import List - async def load_users(keys: List[int]) -> List[User]: return [User(id=key) for key in keys] ``` @@ -87,223 +81,6 @@ cases we can use the `load_many` method. [user_a, user_b, user_c] = await loader.load_many([1, 2, 3]) ``` -### Errors - -An error associated with a particular key can be indicated by including an -exception value in the corresponding position in the returned list. This -exception will be thrown by the `load` call for that key. With the same `User` -class from above: - -```python -from typing import List, Union -from strawberry.dataloader import DataLoader - -users_database = { - 1: User(id=1), - 2: User(id=2), -} - - -async def load_users(keys: List[int]) -> List[Union[User, ValueError]]: - def lookup(key: int) -> Union[User, ValueError]: - if user := users_database.get(key): - return user - - return ValueError("not found") - - return [lookup(key) for key in keys] - - -loader = DataLoader(load_fn=load_users) -``` - -For this loader, calls like `await loader.load(1)` will return `User(id=1)`, -while `await loader.load(3)` will raise `ValueError("not found")`. - -It's important that the `load_users` function returns exception values within -the list for each incorrect key. A call with `keys == [1, 3]` returns -`[User(id=1), ValueError("not found")]`, and doesn't raise the `ValueError` -directly. If the `load_users` function raises an exception, even `load`s with an -otherwise valid key, like `await loader.load(1)`, will raise that exception. - -### Overriding Cache Key - -By default, the input is used as cache key. In the above examples, the cache key -is always a scalar (int, float, string, etc.) and uniquely resolves the data for -the input. - -In practical applications there are situations where it requires combination of -fields to uniquely identify the data. By providing `cache_key_fn` argument to -the `DataLoader` the behaviour of generating key is changed. It is also useful -when objects are keys and two objects should be considered equivalent. The -function definition takes an input parameter and returns a `Hashable` type. - -```python -from typing import List, Union -from strawberry.dataloader import DataLoader - - -class User: - def __init__(self, custom_id: int, name: str): - self.id: int = custom_id - self.name: str = name - - -async def loader_fn(keys): - return keys - - -def custom_cache_key(key): - return key.id - - -loader = DataLoader(load_fn=loader_fn, cache_key_fn=custom_cache_key) -data1 = await loader.load(User(1, "Nick")) -data2 = await loader.load(User(1, "Nick")) -assert data1 == data2 # returns true -``` - -`loader.load(User(1, "Nick"))` will call `custom_cache_key` internally, passing -the object as parameter to the function which will return `User.id` as key that -is `1`. The second call will check the cache for the key returned by -`custom_cache_key` and will return the cache object from the loader cache. - -The implementation relies on users to handle conflicts while generating the -cache key. In case of conflict the data will be overriden for the key. - -### Cache invalidation - -By default DataLoaders use an internal cache. It is great for performance, -however it can cause problems when the data is modified (i.e., a mutation), as -the cached data is no longer be valid! ๐Ÿ˜ฎ - -To fix it, you can explicitly invalidate the data in the cache, using one of -these ways: - -- Specifying a key with `loader.clear(id)`, -- Specifying several keys with `loader.clear_many([id1, id2, id3, ...])`, -- Invalidating the whole cache with `loader.clear_all()` - -### Importing data into cache - -While dataloaders are powerful and efficient, they do not support complex -queries. - -If your app needs them, you'll probably mix dataloaders and direct database -calls. - -In these scenarios, it is useful to import the data retrieved externally into -the dataloader, in order to avoid reloading data afterwards. - -For example: - -```python+graphql -@strawberry.type -class Person: - id: strawberry.ID - friends_ids: strawberry.Private[List[strawberry.ID]] - - @strawberry.field - async def friends(self) -> List[Person]: - return await loader.load_many(self.friends_ids) - -@strawberry.type -class Query: - @strawberry.field - async def get_all_people(self) -> List[User]: - # Fetch all people from the database, without going through the dataloader abstraction - people = await database.get_all_people() - - # Insert the people we fetched in the dataloader cache - # Since "all people" are now in the cache, accessing `Person.friends` will not - # trigger any extra database access - loader.prime_many({person.id: person for person in people}) - - return people ---- -{ - getAllPeople { - id - friends { - id - } - } -} -``` - -### Custom Cache - -DataLoader's default cache is per-request and it caches data in memory. This -strategy might not be optimal or safe for all use cases. For example, if you are -using DataLoader in a distributed environment, you might want to use a -distributed cache. DataLoader let you override the custom caching logic, which -can get data from other persistent caches (e.g Redis) - -`DataLoader` provides an argument `cache_map`. It takes an instance of a class -which implements an abstract interface `AbstractCache`. The interface methods -are `get`, `set`, `delete` and `clear` - -The `cache_map` parameter overrides the `cache_key_fn` if both arguments are -provided. - -```python -from typing import List, Union, Any, Optional - -import strawberry -from strawberry.types import Info -from strawberry.asgi import GraphQL -from strawberry.dataloader import DataLoader, AbstractCache - -from starlette.requests import Request -from starlette.websockets import WebSocket -from starlette.responses import Response - - -class UserCache(AbstractCache): - def __init__(self): - self.cache = {} - - def get(self, key: Any) -> Union[Any, None]: - return self.cache.get(key) # fetch data from persistent cache - - def set(self, key: Any, value: Any) -> None: - self.cache[key] = value # store data in the cache - - def delete(self, key: Any) -> None: - del self.cache[key] # delete key from the cache - - def clear(self) -> None: - self.cache.clear() # clear the cache - - -@strawberry.type -class User: - id: strawberry.ID - name: str - - -async def load_users(keys) -> List[User]: - return [User(id=key, name="Jane Doe") for key in keys] - - -class MyGraphQL(GraphQL): - async def get_context( - self, request: Union[Request, WebSocket], response: Optional[Response] - ) -> Any: - return {"user_loader": DataLoader(load_fn=load_users, cache_map=UserCache())} - - -@strawberry.type -class Query: - @strawberry.field - async def get_user(self, info: Info, id: strawberry.ID) -> User: - return await info.context["user_loader"].load(id) - - -schema = strawberry.Schema(query=Query) -app = MyGraphQL(schema, graphiql=True) -``` - ## Usage with GraphQL Let's see an example of how you can use DataLoaders with GraphQL: @@ -314,26 +91,22 @@ from typing import List from strawberry.dataloader import DataLoader import strawberry - @strawberry.type class User: id: strawberry.ID - async def load_users(keys) -> List[User]: return [User(id=key) for key in keys] loader = DataLoader(load_fn=load_users) - @strawberry.type class Query: @strawberry.field async def get_user(self, id: strawberry.ID) -> User: return await loader.load(id) - schema = strawberry.Schema(query=Query) ``` @@ -372,7 +145,7 @@ Even if this query is fetching two users, it still results in one call to As you have seen in the code above, the dataloader is instantiated outside the resolver, since we need to share it between multiple resolvers or even between multiple resolver calls. However this is a not a recommended pattern when using -your schema inside a server because the dataloader will cache results for as +your schema inside a server because the dataloader will so cache results for as long as the server is running. Instead a common pattern is to create the dataloader when creating the GraphQL @@ -402,10 +175,10 @@ async def load_users(keys) -> List[User]: class MyGraphQL(GraphQL): - async def get_context( - self, request: Union[Request, WebSocket], response: Optional[Response] - ) -> Any: - return {"user_loader": DataLoader(load_fn=load_users)} + async def get_context(self, request: Union[Request, WebSocket], response: Optional[Response]) -> Any: + return { + "user_loader": DataLoader(load_fn=load_users) + } @strawberry.type @@ -419,15 +192,15 @@ schema = strawberry.Schema(query=Query) app = MyGraphQL(schema) ``` -You can now run the example above with any ASGI server, you can read -[ASGI](../integrations/asgi.md)) to get more details on how to run the app. In -case you choose uvicorn you can install it wih +You can now run the example above with any ASGI server, you can read [ASGI](../integrations/asgi.md)) to +get more details on how to run the app. +In case you choose uvicorn you can install it wih ```bash pip install uvicorn ``` -and then, assuming we named our file above `schema.py` we start the app with +and then, asumming we named our file above `schema.py` we start the app with ``` uvicorn schema:app diff --git a/docs/guides/errors.md b/docs/guides/errors.md index f01bab75fd..43339cff8f 100644 --- a/docs/guides/errors.md +++ b/docs/guides/errors.md @@ -48,14 +48,12 @@ When a query is executed each field must resolve to the correct type. For exampl ```python import strawberry - @strawberry.type class Query: @strawberry.field def hello() -> str: return None - schema = strawberry.Schema(query=Query) ``` @@ -92,19 +90,16 @@ Sometimes a resolver will throw an unexpected error due to a programming error o ```python import strawberry - @strawberry.type class User: name: str - @strawberry.type class Query: @strawberry.field def user() -> User: raise Exception("Can't find user") - schema = strawberry.Schema(query=Query) ``` @@ -144,13 +139,12 @@ This could be achieved by making the field optional when there is a possibility from typing import Optional import strawberry - @strawberry.type class Query: @strawberry.field def get_user(self, id: str) -> Optional[User]: try: - user = get_a_user_by_their_ID + user = # get a user by their ID return user except UserDoesNotExist: return None @@ -163,34 +157,33 @@ For example, say you have a `registerUser` mutation where you need to deal with ```python import strawberry - @strawberry.type class RegisterUserSuccess: user: User - @strawberry.type class UsernameAlreadyExistsError: username: str alternative_username: str - # Create a Union type to represent the 2 results from the mutation Response = strawberry.union( - "RegisterUserResponse", [RegisterUserSuccess, UsernameAlreadyExistsError] + "RegisterUserResponse", + [RegisterUserSuccess, UsernameAlreadyExistsError] ) - @strawberry.mutation def register_user(username: str, password: str) -> Response: if username_already_exists(username): return UsernameAlreadyExistsError( username=username, - alternative_username=generate_username_suggestion(username), + alternative_username=generate_username_suggestion(username) ) user = create_user(username, password) - return RegisterUserSuccess(user=user) + return RegisterUserSuccess( + user=user + ) ``` Then your client can look at the `__typename` of the result to determine what to do next: diff --git a/docs/guides/federation-v1.md b/docs/guides/federation-v1.md deleted file mode 100644 index 458112b9b9..0000000000 --- a/docs/guides/federation-v1.md +++ /dev/null @@ -1,213 +0,0 @@ ---- -title: Federation V1 ---- - - - -This guide refers to Apollo Federation 1, if you're looking for the 2.0 guide, -please see the [federation v2](federation.md) guide. - -You can also see the -[What's new in federation 2](https://www.apollographql.com/docs/federation/federation-2/new-in-federation-2/) -for more details. - - - -# Apollo Federation V1 Guide - -Apollo Federation allows you to combine multiple GraphQL APIs into one. This can -be extremely useful when working with a service oriented architecture. - -Strawberry supports -[Apollo Federation](https://www.apollographql.com/docs/federation) out of the -box, that means that you can create services using Strawberry and federate them -via Apollo Gateway. - - - -We donโ€™t have a gateway server, youโ€™ll still need to use the Apollo Gateway for -this. - - - -## Federated schema example - -Letโ€™s look at an example on how to implement Apollo Federation using Strawberry. -Let's assume we have an application with two services that each expose a GraphQL -API: - -1. `books`: a service to manage all the books we have -2. `reviews`: a service to manage book reviews - -### Books service - -Our `book` service might look something like this: - -```python -@strawberry.federation.type(keys=["id"]) -class Book: - id: strawberry.ID - title: str - - -def get_all_books() -> List[Book]: - return [Book(id=1, title="The Dark Tower")] - - -@strawberry.type -class Query: - all_books: List[Book] = strawberry.field(resolver=get_all_books) - - -schema = strawberry.federation.Schema(query=Query) -``` - -We defined two types: `Book` and `Query`, where `Query` has only one field that -allows us to fetch all the books. - -Notice that the `Book` type is using the `strawberry.federation.type` decorator, -as opposed to the normal `strawberry.type`, this new decorator extends the base -one and allows us to define federation-specific attributes on the type. - -Here, we are telling the federation system that the `Book`'s `id` field is its -uniquely-identifying key. - - - -Federation keys can be thought of as primary keys. They are used by the gateway -to query types between multiple services and then join them into the augmented -type. - - - -### Reviews service - -Now, letโ€™s take a look at our review service: we want to define a type for a -review but also extend the `Book` type to have a list of reviews. - -```python -@strawberry.type -class Review: - id: int - body: str - - -def get_reviews(root: "Book") -> List[Review]: - return [ - Review(id=id_, body=f"A review for {root.id}") - for id_ in range(root.reviews_count) - ] - - -@strawberry.federation.type(extend=True, keys=["id"]) -class Book: - id: strawberry.ID = strawberry.federation.field(external=True) - reviews_count: int - reviews: List[Review] = strawberry.field(resolver=get_reviews) - - @classmethod - def resolve_reference(cls, id: strawberry.ID): - # here we could fetch the book from the database - # or even from an API - return Book(id=id, reviews_count=3) -``` - -Now things are looking more interesting; the `Review` type is a GraphQL type -that holds the contents of the review. - -We've also been able to extend the `Book` type by using again -`strawberry.federation.type`, this time passing `extend=True` as an argument. -This is important because we need to tell federation that we are extending a -type that already exists, not creating a new one. - -We have also declared three fields on `Book`, one of which is `id` which is -marked as `external` with `strawberry.federation.field(external=True)`. This -tells federation that this field is not available in this service, and that it -comes from another service. - -The other fields are `reviews` (the list of `Reviews` for this book) and -`reviews_count` (the number of reviews for this book). - -Finally, we also have a class method, `resolve_reference`, that allows us to -instantiate types when they are referred to by other services. The -`resolve_reference` method is called when a GraphQL operation references an -entity across multiple services. For example, when making this query: - -```graphql -{ - # query defined in the books service - books { - title - # field defined in the reviews service - reviews { - body - } - } -} -``` - -`resolve_reference` is called with the `id` of the book for each book returned -by the books service. Recall that above we defined the `id` field as the `key` -for the `Book` type. In this example we are creating an instance of `Book` with -the requested `id` and a fixed number of reviews. - -If we were to add more fields to `Book` that were stored in a database, this -would be where we could perform queries for these fields' values. - -Now we need to do is to define a `Query` type, even if our service only has one -type that is not used directly in any GraphQL query. This is because the GraphQL -spec mandates that a GraphQL server defines a Query type, even if it ends up -being empty/unused. Finally we also need to let Strawberry know about our Book -and Review types. Since they are not reachable from the `Query` field itself, -Strawberry won't be able to find them by default. - -```python -@strawberry.type -class Query: - _hi: str = strawberry.field(resolver=lambda: "Hello world!") - - -schema = strawberry.federation.Schema(query=Query, types=[Book, Review]) -``` - -## The gateway - -Now we have our services up and running, we need to configure a gateway to -consume our services. Apollo Gateway is the official gateway server for Apollo -Federation. Here's an example on how to configure the gateway: - -```js -const { ApolloServer } = require("apollo-server"); -const { ApolloGateway } = require("@apollo/gateway"); - -const gateway = new ApolloGateway({ - serviceList: [ - { name: "books", url: "http://localhost:8000" }, - { name: "reviews", url: "http://localhost:8080" }, - ], -}); - -const server = new ApolloServer({ gateway }); - -server.listen().then(({ url }) => { - console.log(`๐Ÿš€ Server ready at ${url}`); -}); -``` - -When running this example you'll be able to run query like the following: - -```graphql -{ - allBooks { - id - reviewsCount - reviews { - body - } - } -} -``` - -We have provided a full example that you can run and tweak to play with -Strawberry and Federation. The repo is available here: -[https://github.com/strawberry-graphql/federation-demo](https://github.com/strawberry-graphql/federation-demo) diff --git a/docs/guides/federation.md b/docs/guides/federation.md index d3ef09798c..18c510d64c 100644 --- a/docs/guides/federation.md +++ b/docs/guides/federation.md @@ -1,23 +1,23 @@ --- -title: Federation 2 +title: Federation --- -# Apollo Federation 2 Guide - - - -This guide refers to Apollo Federation 2, if you're looking for the 1.0 guide, -please see the [federation v1](federation-v1.md) guide. - - +# Apollo Federation Guide Apollo Federation allows you to combine multiple GraphQL APIs into one. This can be extremely useful when working with a service oriented architecture. Strawberry supports -[Apollo Federation 2](https://www.apollographql.com/docs/federation/federation-2/new-in-federation-2/) -out of the box, that means that you can create services using Strawberry and -federate them via Apollo Gateway or Apollo Router. +[Apollo Federation](https://www.apollographql.com/docs/federation) out of the +box, that means that you can create services using Strawberry and federate them +via Apollo Gateway. + + + +We donโ€™t have a gateway server, youโ€™ll still need to use the Apollo +Gateway for this. + + ## Federated schema example @@ -28,62 +28,30 @@ API: 1. `books`: a service to manage all the books we have 2. `reviews`: a service to manage book reviews -Our folder structure will look something like this: - -```text -my-app/ -โ”œโ”€ books/ -โ”‚ โ”œโ”€ app.py -โ”œโ”€ reviews/ -โ”‚ โ”œโ”€ app.py -``` - - - -This guide assumes you've installed strawberry in both the books and reviews -service - - - ### Books service -Let's create the `books` service, copy the following inside `books/app.py` +Our `book` service might look something like this: ```python -from typing import List - -import strawberry - - @strawberry.federation.type(keys=["id"]) class Book: id: strawberry.ID title: str - def get_all_books() -> List[Book]: - return [Book(id=strawberry.ID("1"), title="The Dark Tower")] - + return [Book(id=1, title="The Dark Tower")] @strawberry.type class Query: all_books: List[Book] = strawberry.field(resolver=get_all_books) - -schema = strawberry.federation.Schema(query=Query, enable_federation_2=True) +schema = strawberry.federation.Schema(query=Query) ``` - - -`enable_federation_2=True` is used to enable Apollo Federation 2 and currently -defaults to `False`. This will change in a future version of Strawberry. - - - We defined two types: `Book` and `Query`, where `Query` has only one field that allows us to fetch all the books. -Notice that the `Book` type is using the `strawberry.federation.type` decorator, +Notice that the `Book` type is used the `strawberry.federation.type` decorator, as opposed to the normal `strawberry.type`, this new decorator extends the base one and allows us to define federation-specific attributes on the type. @@ -92,9 +60,9 @@ uniquely-identifying key. -Federation keys can be thought of as primary keys. They are used by the gateway -to query types between multiple services and then join them into the augmented -type. +Federation keys can be thought of as primary keys. They are used by the +gateway to query types between multiple services and then join them into the +augmented type. @@ -103,30 +71,21 @@ type. Now, letโ€™s take a look at our review service: we want to define a type for a review but also extend the `Book` type to have a list of reviews. -Copy the following inside `reviews/app.py`: - ```python -from typing import List - -import strawberry - - @strawberry.type class Review: id: int body: str - def get_reviews(root: "Book") -> List[Review]: return [ - Review(id=id_, body=f"A review for {root.id}") - for id_ in range(root.reviews_count) + Review(id=id_, body=f"A review for {root.id}") + for id_ in range(root.reviews_count) ] - -@strawberry.federation.type(keys=["id"]) +@strawberry.federation.type(extend=True, keys=["id"]) class Book: - id: strawberry.ID + id: strawberry.ID = strawberry.federation.field(external=True) reviews_count: int reviews: List[Review] = strawberry.field(resolver=get_reviews) @@ -135,31 +94,23 @@ class Book: # here we could fetch the book from the database # or even from an API return Book(id=id, reviews_count=3) - - -@strawberry.type -class Query: - _hi: str = strawberry.field(resolver=lambda: "Hello World!") - - -schema = strawberry.federation.Schema( - query=Query, types=[Book, Review], enable_federation_2=True -) ``` Now things are looking more interesting; the `Review` type is a GraphQL type that holds the contents of the review. -But we also have a `Book` which has 3 fields, `id`, `reviews_count` and -`reviews`. +We've also been able to extend the `Book` type by using again +`strawberry.federation.type`, this time passing `extend=True` as an argument. +This is important because we need to tell federation that we are extending a +type that already exists, not creating a new one. - - -In Apollo Federation 1 we'd need to mark the `Book` type as an extension and -also we'd need to mark `id` as an external field, this is not the case in Apollo -Federation 2. +We have also declared three fields on `Book`, one of which is `id` which is +marked as `external` with `strawberry.federation.field(external=True)`. This +tells federation that this field is not available in this service, and that it +comes from another service. - +The other fields are `reviews` (the list of `Reviews` for this book) and +`reviews_count` (the number of reviews for this book). Finally, we also have a class method, `resolve_reference`, that allows us to instantiate types when they are referred to by other services. The @@ -187,103 +138,50 @@ the requested `id` and a fixed number of reviews. If we were to add more fields to `Book` that were stored in a database, this would be where we could perform queries for these fields' values. -We also defined a `Query` type that has a single field, `_hi`, which returns a -string. This is required because the GraphQL spec mandates that a GraphQL server -defines a Query type, even if it ends up being empty/unused. - -Finally we also need to let Strawberry know about our Book and Review types. -Since they are not reachable from the `Query` field itself, Strawberry won't be -able to find them. - - - -If you don't need any custom logic for your resolve_reference, you can omit it -and Strawberry will automatically instanciate the type for you. For example, -if we had a `Book` type with only an `id` field, Strawberry would be able to -instanciate it for us based on the data returned by the gateway. +Now we need to do is to define a `Query` type, even if our service only has one +type that is not used directly in any GraphQL query. This is because the GraphQL +spec mandates that a GraphQL server defines a Query type, even if it ends up +being empty/unused. Finally we also need to let Strawberry know about our Book +and Review types. Since they are not reachable from the `Query` field itself, +Strawberry won't be able to find them by default. ```python -@strawberry.federation.type(keys=["id"]) -class Book: - id: strawberry.ID - reviews: List[Review] = strawberry.field(resolver=get_reviews) -``` - - - -## Let's run our services - -Before starting Apollo Router to compose our schemas we need to run the -services. - -In two terminal windows, run the following commands: - -```bash -cd books -strawberry server --port 3500 app -``` +@strawberry.type +class Query: + _service: Optional[str] -```bash -cd reviews -strawberry server --port 3000 app +schema = strawberry.federation.Schema(query=Query, types=[Book, Review]) ``` -## Apollo Router +## The gateway Now we have our services up and running, we need to configure a gateway to -consume our services. Apollo provides a router that can be used for this. +consume our services. Apollo Gateway is the official gateway server for Apollo +Federation. Here's an example on how to configure the gateway: -Before continuing we'll need to install Apollo Router by following -[their installation guide](https://www.apollographql.com/docs/router/quickstart/) -and we'll need to -[install Apollo's CLI](https://www.apollographql.com/docs/rover/getting-started) -to compose the schema. +```js +const { ApolloServer } = require("apollo-server"); +const { ApolloGateway } = require("@apollo/gateway"); - - -Composing the schema means combining all our service's schemas into a single -schema. The composed schema will be used by the router to route requests to the -appropriate services. - - - -Create a file called `supergraph.yaml` with the following contents: - -```yaml -federation_version: 2 -subgraphs: - reviews: - routing_url: http://localhost:3000 - schema: - subgraph_url: http://localhost:3000 - - books: - routing_url: http://localhost:3500 - schema: - subgraph_url: http://localhost:3500 -``` - -This file will be used by rover to compose the schema, which can be done with -the following command: - -```bash -# Creates prod-schema.graphql or overwrites if it already exists -rover supergraph compose --config ./supergraph.yaml > supergraph-schema.graphql -``` +const gateway = new ApolloGateway({ + serviceList: [ + { name: "books", url: "http://localhost:8000" }, + { name: "reviews", url: "http://localhost:8080" }, + ], +}); -Now that we have the composed schema, we can start the router. +const server = new ApolloServer({ gateway }); -```bash -./router --supergraph supergraph-schema.graphql +server.listen().then(({ url }) => { + console.log(`๐Ÿš€ Server ready at ${url}`); +}); ``` -Now that router is running we can go to -[http://localhost:4000](http://localhost:4000) and try to run the following -query: +When running this example you'll be able to run query like the following: ```graphql { - allBooks { + books { id reviewsCount reviews { @@ -293,36 +191,6 @@ query: } ``` -if everything went well we should get the following result: - -```json -{ - "data": { - "allBooks": [ - { - "id": "1", - "reviewsCount": 3, - "reviews": [ - { - "body": "A review for 1" - }, - { - "body": "A review for 1" - }, - { - "body": "A review for 1" - } - ] - } - ] - } -} -``` - We have provided a full example that you can run and tweak to play with Strawberry and Federation. The repo is available here: [https://github.com/strawberry-graphql/federation-demo](https://github.com/strawberry-graphql/federation-demo) - -## Additional resources - -[Apollo Federation Quickstart](https://www.apollographql.com/docs/federation/quickstart/setup/) diff --git a/docs/guides/file-upload.md b/docs/guides/file-upload.md index 3979073b78..8d5eb5322b 100644 --- a/docs/guides/file-upload.md +++ b/docs/guides/file-upload.md @@ -21,14 +21,9 @@ The type passed at runtime depends on the integration: | [Sanic](/docs/integrations/sanic) | [`sanic.request.File`](https://sanic.readthedocs.io/en/stable/sanic/api/core.html#sanic.request.File) | | [Starlette](/docs/integrations/starlette) | [`starlette.datastructures.UploadFile`](https://www.starlette.io/requests/#request-files) | -## ASGI / FastAPI / Starlette +## ASGI -Since these integrations use asyncio for communication, the resolver _must_ be async. - -Additionally, these servers rely on the `python-multipart` package, which is not included by Strawberry by default. It can be installed directly, or, for convenience, it is included in extras: `strawberry[asgi]` (for ASGI/Starlette) or `strawberry[fastapi]` (for FastAPI). For example: - -- if using Pip, `pip install 'strawberry[fastapi]'` -- if using Poetry, `strawberry = { version = "...", extras = ["fastapi"] }` in `pyproject.toml`. +Since ASGI uses asyncio for communication the resolver _must_ be async as well. Example: @@ -38,30 +33,17 @@ import strawberry from strawberry.file_uploads import Upload -@strawberry.input -class FolderInput: - files: typing.List[Upload] - - @strawberry.type class Mutation: @strawberry.mutation async def read_file(self, file: Upload) -> str: - return (await file.read()).decode("utf-8") + return await file.read() @strawberry.mutation async def read_files(self, files: typing.List[Upload]) -> typing.List[str]: contents = [] for file in files: - content = (await file.read()).decode("utf-8") - contents.append(content) - return contents - - @strawberry.mutation - async def read_folder(self, folder: FolderInput) -> typing.List[str]: - contents = [] - for file in folder.files: - content = (await file.read()).decode("utf-8") + content = (await file.read()).decode() contents.append(content) return contents ``` @@ -76,70 +58,17 @@ import strawberry from strawberry.file_uploads import Upload -@strawberry.input -class FolderInput: - files: typing.List[Upload] - - @strawberry.type class Mutation: @strawberry.mutation def read_file(self, file: Upload) -> str: - return file.read().decode("utf-8") + return file.read().decode() @strawberry.mutation def read_files(self, files: typing.List[Upload]) -> typing.List[str]: contents = [] for file in files: - content = file.read().decode("utf-8") + content = file.read().decode() contents.append(content) return contents - - @strawberry.mutation - def read_folder(self, folder: FolderInput) -> typing.List[str]: - contents = [] - for file in folder.files: - contents.append(file.read().decode("utf-8")) - return contents -``` - -## Sending file upload requests - -The tricky part is sending the HTTP request from the client because it must follow the GraphQL multipart request specifications mentioned above. - -The `multipart/form-data` POST request's data must include: - -- `operations` key for GraphQL request with query and variables -- `map` key with mapping some multipart-data to exact GraphQL variable -- and other keys for multipart-data which contains binary data of files - -Assuming you have your schema up and running, here there are some requests examples: - -### Sending one file - -```bash -curl localhost:8000/graphql \ - -F operations='{ "query": "mutation($file: Upload!){ readFile(file: $file) }", "variables": { "file": null } }' \ - -F map='{ "file": ["variables.file"] }' \ - -F file=@a.txt -``` - -### Sending a list of files - -```bash -curl localhost:8000/graphql \ - -F operations='{ "query": "mutation($files: [Upload!]!) { readFiles(files: $files) }", "variables": { "files": [null, null] } }' \ - -F map='{"file1": ["variables.files.0"], "file2": ["variables.files.1"]}' \ - -F file1=@b.txt \ - -F file2=@c.txt -``` - -### Sending nested files - -```bash -curl localhost:8000/graphql \ - -F operations='{ "query": "mutation($folder: FolderInput!) { readFolder(folder: $folder) }", "variables": {"folder": {"files": [null, null]}} }' \ - -F map='{"file1": ["variables.folder.files.0"], "file2": ["variables.folder.files.1"]}' \ - -F file1=@b.txt \ - -F file2=@c.txt ``` diff --git a/docs/guides/pagination.md b/docs/guides/pagination.md new file mode 100644 index 0000000000..f2719692fa --- /dev/null +++ b/docs/guides/pagination.md @@ -0,0 +1,129 @@ +--- +title: Pagination +--- + +# Pagination + +APIs commonly use pagination to efficiently return a portion of a result instead +of every single item, which can have inefficient performance. + +The GraphQL spec [recommends cursor-based pagination](https://graphql.org/learn/pagination/) +and refers to [Relay's Connection Spec](https://relay.dev/graphql/connections.htm) +for specific implementation details. + +Here we show a minimal example of how you can leverage Strawberry's generic Types +to build the types required to comply with the relay spec. + +```python +import base64 +from typing import List, Generic, TypeVar, Optional + +import strawberry +from strawberry.arguments import UNSET + + +GenericType = TypeVar("GenericType") + + +@strawberry.type +class Connection(Generic[GenericType]): + """Represents a paginated relationship between two entities + + This pattern is used when the relationship itself has attributes. + In a Facebook-based domain example, a friendship between two people + would be a connection that might have a `friendshipStartTime` + """ + page_info: "PageInfo" + edges: list["Edge[GenericType]"] + + +@strawberry.type +class PageInfo: + """Pagination context to navigate objects with cursor-based pagination + + Instead of classic offset pagination via `page` and `limit` parameters, + here we have a cursor of the last object and we fetch items starting from that one + + Read more at: + - https://graphql.org/learn/pagination/#pagination-and-edges + - https://relay.dev/graphql/connections.htm + """ + has_next_page: bool + has_previous_page: bool + start_cursor: Optional[str] + end_cursor: Optional[str] + + +@strawberry.type +class Edge(Generic[GenericType]): + """An edge may contain additional information of the relationship. This is the trivial case""" + node: GenericType + cursor: str + + + +@strawberry.type +class Book: + title: str + author: str + + @classmethod + def from_db_model(cls, instance): + """Adapt this method with logic to map your orm instance to a strawberry decorated class""" + return cls(title=instance.title, author=instance.title) + + + +def build_book_cursor(book: Book): + """Adapt this method to build an *opaque* ID from an instance""" + bookid = f"{id(book)}".encode("utf-8") + return base64.b64encode(bookid).decode() + + +Cursor = str + + +def get_books(first: int = 10, after: Optional[Cursor] = UNSET) -> Connection[Book]: + """ + A non-trivial implementation should efficiently fetch only + the necessary books after the offset. + For simplicity, here we build the list and then slice it accordingly + """ + after = after if after is not UNSET else None + + # Fetch the requested books plus one, just to calculate `has_next_page` + books = [ + Book( + title=f"Title {x}", + author=f"Author {x}", + ) + for x in range(20) + ][after:first+1] + + edges = [ + Edge(node=Book.from_db_model(book), cursor=build_book_cursor(book)) + for book in books + ] + + return Connection( + page_info=PageInfo( + has_previous_page=False, + has_next_page=len(books) > first, + start_cursor=edges[0].cursor if edges else None, + end_cursor=edges[-2].cursor if len(edges) > 1 else None, + ), + edges=edges[:-1] # exclude last one as it was fetched to know if there is a next page + ) + + +@strawberry.type +class Query: + books: List[Book] = strawberry.field(resolver=get_books) + +schema = strawberry.Schema(query=Query) +``` + +Name your file `pagination.py` and run `strawberry server pagination` + +When you visit the GraphQL URL from your terminal output, you should see something like this: +A view of the GraphiQL interface with an example pagination query diff --git a/docs/guides/pagination/connections.md b/docs/guides/pagination/connections.md deleted file mode 100644 index e6e53a0ad6..0000000000 --- a/docs/guides/pagination/connections.md +++ /dev/null @@ -1,632 +0,0 @@ ---- -title: Pagination - Implementing the Relay Connection Specification ---- - -# Implementing the Relay Connection Specification - -We naively implemented cursor based pagination in the [previous tutorial](./cursor-based.md). To ensure a consistent implementation -of this pattern, the Relay project has a formal [specification](https://relay.dev/graphql/connections.htm) you can follow for building -GraphQL APIs which use a cursor based connection pattern. - -By the end of this tutorial, we should be able to return a connection of users when requested. - -```graphql+response -query getUsers { - getUsers(first: 2) { - users { - edges { - node { - id - name - occupation - age - } - } - cursor - } - pageInfo { - endCursor - hasNextPage - } - } -} ---- -{ - "data": { - "getUsers": { - "users": { - "edges": [ - { - "node": { - "id": 1, - "name": "Norman Osborn", - "occupation": "Founder, Oscorp Industries", - "age": 42 - }, - "cursor": "dXNlcjox" - }, - { - "node": { - "id": 2, - "name": "Peter Parker", - "occupation": "Freelance Photographer, The Daily Bugle", - "age": 20 - }, - "cursor": "dXNlcjoy" - } - ] - }, - "pageInfo": { - "endCursor": "dXNlcjoz", - "hasNextPage": true - } - } - } -} -``` - -## Connections - -A Connection represents a paginated relationship between two entities. This pattern is used when the relationship -itself has attributes. For example, we might have a connection of users to represent a paginated list of users. - -Let us define a Connection type which takes in a Generic ObjectType. - -```py -# example.py - -from typing import Generic, TypeVar - -import strawberry - - -GenericType = TypeVar("GenericType") - - -@strawberry.type -class Connection(Generic[GenericType]): - page_info: "PageInfo" = strawberry.field( - description="Information to aid in pagination." - ) - - edges: list["Edge[GenericType]"] = strawberry.field( - description="A list of edges in this connection." - ) - -``` - -Connections must have atleast two fields: `edges` and `page_info`. - -The `page_info` field contains metadata about the connection. -Following the Relay specification, we can define a `PageInfo` type like this: - -```py line=22-38 -# example.py - -from typing import Generic, TypeVar - -import strawberry - - -GenericType = TypeVar("GenericType") - - -@strawberry.type -class Connection(Generic[GenericType]): - page_info: "PageInfo" = strawberry.field( - description="Information to aid in pagination." - ) - - edges: list["Edge[GenericType]"] = strawberry.field( - description="A list of edges in this connection." - ) - - -@strawberry.type -class PageInfo: - has_next_page: bool = strawberry.field( - description="When paginating forwards, are there more items?" - ) - - has_previous_page: bool = strawberry.field( - description="When paginating backwards, are there more items?" - ) - - start_cursor: Optional[str] = strawberry.field( - description="When paginating backwards, the cursor to continue." - ) - - end_cursor: Optional[str] = strawberry.field( - description="When paginating forwards, the cursor to continue." - ) - -``` - -You can read more about the `PageInfo` type at: - -- https://graphql.org/learn/pagination/#pagination-and-edges -- https://relay.dev/graphql/connections.htm - -The `edges` field must return a list type that wraps an edge type. - -Following the Relay specification, let us define an Edge that takes -in a generic ObjectType. - -```py line=41-49 -# example.py - -from typing import Generic, TypeVar - -import strawberry - - -GenericType = TypeVar("GenericType") - - -@strawberry.type -class Connection(Generic[GenericType]): - page_info: "PageInfo" = strawberry.field( - description="Information to aid in pagination." - ) - - edges: list["Edge[GenericType]"] = strawberry.field( - description="A list of edges in this connection." - ) - - -@strawberry.type -class PageInfo: - has_next_page: bool = strawberry.field( - description="When paginating forwards, are there more items?" - ) - - has_previous_page: bool = strawberry.field( - description="When paginating backwards, are there more items?" - ) - - start_cursor: Optional[str] = strawberry.field( - description="When paginating backwards, the cursor to continue." - ) - - end_cursor: Optional[str] = strawberry.field( - description="When paginating forwards, the cursor to continue." - ) - - -@strawberry.type -class Edge(Generic[GenericType]): - node: GenericType = strawberry.field( - description="The item at the end of the edge." - ) - - cursor: str = strawberry.field( - description="A cursor for use in pagination." - ) - -``` - -EdgeTypes must have atleast two fields - `cursor` and `node`. -Each edge has it's own cursor and item (represented by the `node` field). - -Now that we have the types needed to implement pagination using Relay Connections, let -us use them to paginate a list of users. For simplicity's sake, let our dataset be a list of dictionaries. - -```py line=7-32 -# example.py - -from typing import Generic, TypeVar - -import strawberry - -user_data = [ - { - "id": 1, - "name": "Norman Osborn", - "occupation": "Founder, Oscorp Industries", - "age": 42 - }, - { - "id": 2, - "name": "Peter Parker", - "occupation": "Freelance Photographer, The Daily Bugle", - "age": 20 - }, - { - "id": 3, - "name": "Harold Osborn", - "occupation": "President, Oscorp Industries", - "age": 19 - }, - { - "id": 4, - "name": "Eddie Brock", - "occupation": "Journalist, The Eddie Brock Report", - "age": 20 - } -] - - -GenericType = TypeVar("GenericType") - - -@strawberry.type -class Connection(Generic[GenericType]): - page_info: "PageInfo" = strawberry.field( - description="Information to aid in pagination." - ) - - edges: list["Edge[GenericType]"] = strawberry.field( - description="A list of edges in this connection." - ) - - -@strawberry.type -class PageInfo: - has_next_page: bool = strawberry.field( - description="When paginating forwards, are there more items?" - ) - - has_previous_page: bool = strawberry.field( - description="When paginating backwards, are there more items?" - ) - - start_cursor: Optional[str] = strawberry.field( - description="When paginating backwards, the cursor to continue." - ) - - end_cursor: Optional[str] = strawberry.field( - description="When paginating forwards, the cursor to continue." - ) - - -@strawberry.type -class Edge(Generic[GenericType]): - node: GenericType = strawberry.field( - description="The item at the end of the edge." - ) - - cursor: str = strawberry.field( - description="A cursor for use in pagination." - ) - -``` - -Now is a good time to think of what we could use as a cursor for our dataset. Our cursor -needs to be an opaque value, which doesn't usually change over time. It makes sense to use -base64 encoded IDs of users as our cursor, as they fit both criteria. - - - -While working with Connections, it is a convention to base64-encode cursors. It provides a unified interface to the -end user. API clients need not bother about the type of data to paginate, and can pass unique IDs during pagination. -It also makes the cursors opaque. - - - -Let us define a couple of helper functions to encode and decode cursors as follows: - -```py line=3,35-43 -# example.py - -from base64 import b64encode, b64decode -from typing import Generic, TypeVar - -import strawberry - -user_data = [ - { - "id": 1, - "name": "Norman Osborn", - "occupation": "Founder, Oscorp Industries", - "age": 42 - }, - { - "id": 2, - "name": "Peter Parker", - "occupation": "Freelance Photographer, The Daily Bugle", - "age": 20 - }, - { - "id": 3, - "name": "Harold Osborn", - "occupation": "President, Oscorp Industries", - "age": 19 - }, - { - "id": 4, - "name": "Eddie Brock", - "occupation": "Journalist, The Eddie Brock Report", - "age": 20 - } -] - -def encode_user_cursor(id: int) -> str: - """ - Encodes the given user ID into a cursor. - - :param id: The user ID to encode. - - :return: The encoded cursor. - """ - return b64encode(f"user:{id}".encode("ascii")).decode("ascii") - - -def decode_user_cursor(cursor: str) -> int: - """ - Decodes the user ID from the given cursor. - - :param cursor: The cursor to decode. - - :return: The decoded user ID. - """ - cursor_data = b64decode(cursor.encode("ascii")).decode("ascii") - return int(cursor_data.split(":")[1]) - - -GenericType = TypeVar("GenericType") - - -@strawberry.type -class Connection(Generic[GenericType]): - page_info: "PageInfo" = strawberry.field( - description="Information to aid in pagination." - ) - - edges: list["Edge[GenericType]"] = strawberry.field( - description="A list of edges in this connection." - ) - - -@strawberry.type -class PageInfo: - has_next_page: bool = strawberry.field( - description="When paginating forwards, are there more items?" - ) - - has_previous_page: bool = strawberry.field( - description="When paginating backwards, are there more items?" - ) - - start_cursor: Optional[str] = strawberry.field( - description="When paginating backwards, the cursor to continue." - ) - - end_cursor: Optional[str] = strawberry.field( - description="When paginating forwards, the cursor to continue." - ) - - -@strawberry.type -class Edge(Generic[GenericType]): - node: GenericType = strawberry.field( - description="The item at the end of the edge." - ) - - cursor: str = strawberry.field( - description="A cursor for use in pagination." - ) - -``` - -Let us define a `get_users` field which returns a connection of users, as well as an `UserType`. -Let us also plug our query into a schema. - -```python line=104-174 -# example.py - -from base64 import b64encode, b64decode -from typing import List, Optional, Generic, TypeVar - -import strawberry - -user_data = [ - { - "id": 1, - "name": "Norman Osborn", - "occupation": "Founder, Oscorp Industries", - "age": 42 - }, - { - "id": 2, - "name": "Peter Parker", - "occupation": "Freelance Photographer, The Daily Bugle", - "age": 20 - }, - { - "id": 3, - "name": "Harold Osborn", - "occupation": "President, Oscorp Industries", - "age": 19 - }, - { - "id": 4, - "name": "Eddie Brock", - "occupation": "Journalist, The Eddie Brock Report", - "age": 20 - } -] - -def encode_user_cursor(id: int) -> str: - """ - Encodes the given user ID into a cursor. - - :param id: The user ID to encode. - - :return: The encoded cursor. - """ - return b64encode(f"user:{id}".encode("ascii")).decode("ascii") - - -def decode_user_cursor(cursor: str) -> int: - """ - Decodes the user ID from the given cursor. - - :param cursor: The cursor to decode. - - :return: The decoded user ID. - """ - cursor_data = b64decode(cursor.encode("ascii")).decode("ascii") - return int(cursor_data.split(":")[1]) - - -GenericType = TypeVar("GenericType") - - -@strawberry.type -class Connection(Generic[GenericType]): - page_info: "PageInfo" = strawberry.field( - description="Information to aid in pagination." - ) - - edges: list["Edge[GenericType]"] = strawberry.field( - description="A list of edges in this connection." - ) - - -@strawberry.type -class PageInfo: - has_next_page: bool = strawberry.field( - description="When paginating forwards, are there more items?" - ) - - has_previous_page: bool = strawberry.field( - description="When paginating backwards, are there more items?" - ) - - start_cursor: Optional[str] = strawberry.field( - description="When paginating backwards, the cursor to continue." - ) - - end_cursor: Optional[str] = strawberry.field( - description="When paginating forwards, the cursor to continue." - ) - - -@strawberry.type -class Edge(Generic[GenericType]): - node: GenericType = strawberry.field( - description="The item at the end of the edge." - ) - - cursor: str = strawberry.field( - description="A cursor for use in pagination." - ) - -@strawberry.type -class User: - name: str = strawberry.field( - description="The name of the user." - ) - - occupation: str = strawberry.field( - description="The occupation of the user." - ) - - age: int = strawberry.field( - description="The age of the user." - ) - - -@strawberry.type -class Query: - @strawberry.field(description="Get a list of users.") - def get_users(self, first: int = 2, after: Optional[str] = None) -> Connection[User]: - if after is not None: - # decode the user ID from the given cursor. - user_id = decode_user_cursor(cursor=after) - else: - # no cursor was given (this happens usually when the - # client sends a query for the first time). - user_id = 0 - - # filter the user data, going through the next set of results. - filtered_data = map(lambda user: user.id > user_id, user_data) - - # slice the relevant user data (Here, we also slice an - # additional user instance, to prepare the next cursor). - sliced_users = filtered_data[after:first+1] - - if len(sliced_users) > first: - # calculate the client's next cursor. - last_user = sliced_users.pop(-1) - next_cursor = encode_user_cursor(id=last_user.id) - has_next_page = True - else: - # We have reached the last page, and - # don't have the next cursor. - next_cursor = None - has_next_page = False - - # We know that we have items in the - # previous page window if the initial user ID - # was not the first one. - has_previous_page = user_id > 0 - - # build user edges. - edges = [ - Edge( - node=cast(UserType, user), - cursor=encode_user_cursor(id=user.id), - ) - for user in sliced_users - ] - - if edges: - # we have atleast one edge. Get the cursor - # of the first edge we have. - start_cursor = edges[0].cursor - else: - # We have no edges to work with. - start_cursor = None - - if len(edges) > 1: - # We have atleast 2 edges. Get the cursor - # of the last edge we have. - end_cursor = edges[-1].cursor - else: - # We don't have enough edges to work with. - end_cursor = None - - return Connection( - edges=edges, - page_info=PageInfo( - has_next_page=has_next_page, - has_previous_page=has_previous_page, - start_cursor=start_cursor, - end_cursor=end_cursor, - ) - ) - -schema = strawberry.Schema(query=Query) - -``` - -you can start the debug server with the following command: - -``` -strawberry server example:schema -``` - -Here's an example query to try out: - -```graphql -query getUsers { - getUsers(first: 2) { - users { - edges { - node { - id - name - occupation - age - } - } - cursor - } - pageInfo { - endCursor - hasNextPage - } - } -} -``` diff --git a/docs/guides/pagination/cursor-based.md b/docs/guides/pagination/cursor-based.md deleted file mode 100644 index 8bc20b3bd6..0000000000 --- a/docs/guides/pagination/cursor-based.md +++ /dev/null @@ -1,470 +0,0 @@ ---- -title: Pagination - Cursor based ---- - -# Implementing Cursor Pagination - -Make sure to check our introduction to pagination [here](./overview.md)! - -Let us implement cursor based pagination in GraphQL. By the end of this tutorial, we -should be able to return a paginated list of users when requested. - -```graphql+response -query getUsers { - getUsers(limit: 2) { - users { - id - name - occupation - age - } - pageMeta { - nextCursor - } - } -} ---- -{ - "data": { - "getUsers": { - "users": [ - { - "id": 1, - "name": "Norman Osborn", - "occupation": "Founder, Oscorp Industries", - "age": 42 - }, - { - "id": 2, - "name": "Peter Parker", - "occupation": "Freelance Photographer, The Daily Bugle", - "age": 20 - } - ], - "pageMeta": { - "nextCursor": "dXNlcjoz", - } - } - } -} -``` - -The server needs to return a `cursor` along with the sliced user data, so that our client -can know what to query for next. The client could also provide a `limit` value, to specify -how much users it wants at a time. - -Let us model our schema like this: - -```py -# example.py - -from typing import List, Optional - -import strawberry - - -@strawberry.type -class User: - name: str = strawberry.field( - description="The name of the user." - ) - - occupation: str = strawberry.field( - description="The occupation of the user." - ) - - age: int = strawberry.field( - description="The age of the user." - ) - - -@strawberry.type -class PageMeta: - next_cursor: Optional[str] = strawberry.field( - description="The next cursor to continue with." - ) - - -@strawberry.type -class UserResponse: - users: List[User] = strawberry.field( - description="The list of users." - ) - - page_meta: PageMeta = strawberry.field( - description="Metadata to aid in pagination." - ) - - -@strawberry.type -class Query: - @strawberry.field(description="Get a list of users.") - def get_users(self) -> UserResponse: - ... - -schema = strawberry.Schema(query=Query) - -``` - -For simplicity's sake, our dataset is going to be an in-memory list. - -```py line=7-32 -# example.py - -from typing import List, Optional - -import strawberry - -user_data = [ - { - "id": 1, - "name": "Norman Osborn", - "occupation": "Founder, Oscorp Industries", - "age": 42 - }, - { - "id": 2, - "name": "Peter Parker", - "occupation": "Freelance Photographer, The Daily Bugle", - "age": 20 - }, - { - "id": 3, - "name": "Harold Osborn", - "occupation": "President, Oscorp Industries", - "age": 19 - }, - { - "id": 4, - "name": "Eddie Brock", - "occupation": "Journalist, The Eddie Brock Report", - "age": 20 - } -] - - -@strawberry.type -class User: - name: str = strawberry.field( - description="The name of the user." - ) - - occupation: str = strawberry.field( - description="The occupation of the user." - ) - - age: int = strawberry.field( - description="The age of the user." - ) - - -@strawberry.type -class PageMeta: - next_cursor: Optional[str] = strawberry.field( - description="The next cursor to continue with." - ) - - -@strawberry.type -class UserResponse: - users: List[User] = strawberry.field( - description="The list of users." - ) - - page_meta: PageMeta = strawberry.field( - description="Metadata to aid in pagination." - ) - - -@strawberry.type -class Query: - @strawberry.field(description="Get a list of users.") - def get_users(self) -> UserResponse: - ... - -schema = strawberry.Schema(query=Query) - -``` - -Now is a good time to think of what we could use as a cursor for our dataset. Our cursor needs to be an opaque value, -which doesn't usually change over time. It makes sense to use base64 encoded IDs of users as our cursor, as they fit both criteria. - - - -It is good practice to base64-encode cursors, to provide a unified interface to the end user. API clients need not -bother about the type of data to paginate, and can pass unique IDs during pagination. It also makes the cursor opaque. - - - -Let us define a couple of helper functions to encode and decode cursors as follows: - -```py line=3,35-43 -# example.py - -from base64 import b64encode, b64decode -from typing import List, Optional - -import strawberry - -user_data = [ - { - "id": 1, - "name": "Norman Osborn", - "occupation": "Founder, Oscorp Industries", - "age": 42 - }, - { - "id": 2, - "name": "Peter Parker", - "occupation": "Freelance Photographer, The Daily Bugle", - "age": 20 - }, - { - "id": 3, - "name": "Harold Osborn", - "occupation": "President, Oscorp Industries", - "age": 19 - }, - { - "id": 4, - "name": "Eddie Brock", - "occupation": "Journalist, The Eddie Brock Report", - "age": 20 - } -] - -def encode_user_cursor(id: int) -> str: - """ - Encodes the given user ID into a cursor. - - :param id: The user ID to encode. - - :return: The encoded cursor. - """ - return b64encode(f"user:{id}".encode("ascii")).decode("ascii") - - -def decode_user_cursor(cursor: str) -> int: - """ - Decodes the user ID from the given cursor. - - :param cursor: The cursor to decode. - - :return: The decoded user ID. - """ - cursor_data = b64decode(cursor.encode("ascii")).decode("ascii") - return int(cursor_data.split(":")[1]) - - -@strawberry.type -class User: - name: str = strawberry.field( - description="The name of the user." - ) - - occupation: str = strawberry.field( - description="The occupation of the user." - ) - - age: int = strawberry.field( - description="The age of the user." - ) - - -@strawberry.type -class PageMeta: - next_cursor: Optional[str] = strawberry.field( - description="The next cursor to continue with." - ) - - -@strawberry.type -class UserResponse: - users: List[User] = strawberry.field( - description="The list of users." - ) - - page_meta: PageMeta = strawberry.field( - description="Metadata to aid in pagination." - ) - - -@strawberry.type -class Query: - @strawberry.field(description="Get a list of users.") - def get_users(self) -> UserResponse: - ... - -schema = strawberry.Schema(query=Query) - -``` - -We're going to use the dataset we defined in our `get_users` field resolver. -Our field is going to accept two arguments, `limit` and `cursor`, to control pagination. -Let us implement the pagination logic as follows. - -Now, let us implement the pagination logic. - -```py line=79-115 -# example.py - -from base64 import b64encode, b64decode -from typing import List, Optional, cast - -import strawberry - -user_data = [ - { - "id": 1, - "name": "Norman Osborn", - "occupation": "Founder, Oscorp Industries", - "age": 42 - }, - { - "id": 2, - "name": "Peter Parker", - "occupation": "Freelance Photographer, The Daily Bugle", - "age": 20 - }, - { - "id": 3, - "name": "Harold Osborn", - "occupation": "President, Oscorp Industries", - "age": 19 - }, - { - "id": 4, - "name": "Eddie Brock", - "occupation": "Journalist, The Eddie Brock Report", - "age": 20 - } -] - -def encode_user_cursor(id: int) -> str: - """ - Encodes the given user ID into a cursor. - - :param id: The user ID to encode. - - :return: The encoded cursor. - """ - return b64encode(f"user:{id}".encode("ascii")).decode("ascii") - - -def decode_user_cursor(cursor: str) -> int: - """ - Decodes the user ID from the given cursor. - - :param cursor: The cursor to decode. - - :return: The decoded user ID. - """ - cursor_data = b64decode(cursor.encode("ascii")).decode("ascii") - return int(cursor_data.split(":")[1]) - - -@strawberry.type -class User: - name: str = strawberry.field( - description="The name of the user." - ) - - occupation: str = strawberry.field( - description="The occupation of the user." - ) - - age: int = strawberry.field( - description="The age of the user." - ) - - -@strawberry.type -class PageMeta: - next_cursor: Optional[str] = strawberry.field( - description="The next cursor to continue with." - ) - - -@strawberry.type -class UserResponse: - users: List[User] = strawberry.field( - description="The list of users." - ) - - page_meta: PageMeta = strawberry.field( - description="Metadata to aid in pagination." - ) - - -@strawberry.type -class Query: - @strawberry.field(description="Get a list of users.") - def get_users(self, limit: int, cursor: Optional[str] = None) -> UserResponse: - if cursor is not None: - # decode the user ID from the given cursor. - user_id = decode_user_cursor(cursor=cursor) - else: - # no cursor was given (this happens usually when the - # client sends a query for the first time). - user_id = 0 - - # filter the user data, going through the next set of results. - filtered_data = map(lambda user: user.id > user_id, user_data) - - # slice the relevant user data (Here, we also slice an - # additional user instance, to prepare the next cursor). - sliced_users = filtered_data[:limit+1] - - if len(sliced_users) > limit: - # calculate the client's next cursor. - last_user = sliced_users.pop(-1) - next_cursor = encode_user_cursor(id=last_user.id) - else: - # We have reached the last page, and - # don't have the next cursor. - next_cursor = None - - # type cast the sliced data. - sliced_users = cast(List[UserType], sliced_users) - - return UserResponse( - users=sliced_users, - page_meta=PageMeta( - next_cursor=next_cursor - ) - ) - -schema = strawberry.Schema(query=Query) - -``` - - - -Did you notice that cursor argument we defined is optional? That's because the client doesn't know -the cursor initially, when it makes the first request. - - - -Now, let us start a debug server with our schema! - -```text -strawberry server example:schema -``` - -We should be able to query for users on the GraphiQL explorer. Here's a sample query for you! - -```graphql -query getUsers { - getUsers(limit: 2) { - users { - id - name - occupation - age - } - pageMeta { - nextCursor - } - } -} -``` diff --git a/docs/guides/pagination/offset-based.md b/docs/guides/pagination/offset-based.md deleted file mode 100644 index 0df488559e..0000000000 --- a/docs/guides/pagination/offset-based.md +++ /dev/null @@ -1,313 +0,0 @@ ---- -title: Pagination - Offset based ---- - -# Implementing Offset-Based Pagination - -Make sure to check our introduction to pagination [here](./overview.md)! - -Let us implement offset-based pagination in GraphQL. By the end of this tutorial, we -should be able to return a sorted, filtered, and paginated list of users. - -Let us model the `User` type, which represents one user, with a name, occupation, and age. - -```py line=1-24 -# example.py -from typing import List, TypeVar, Dict, Any -import strawberry - - -@strawberry.type -class User: - name: str = strawberry.field( - description="The name of the user." - ) - occupation: str = strawberry.field( - description="The occupation of the user." - ) - age: int = strawberry.field( - description="The age of the user." - ) - - @staticmethod - def from_row(row: Dict[str, Any]): - return User( - name=row['name'], - occupation=row['occupation'], - age=row['age'] - ) -``` - -Let us now model the `PaginationWindow`, which represents one "slice" of sorted, filtered, and paginated items. - -```py line=27-38 -GenericType = TypeVar("GenericType") - - -@strawberry.type -class PaginationWindow(List[GenericType]): - items: List[GenericType] = strawberry.field( - description="The list of items in this pagination window." - ) - - total_items_count: int = strawberry.field( - description="Total number of items in the filtered dataset." - ) -``` - -Note that `PaginationWindow` is generic - it can represent a slice of users, or a slice of any other type of -items that we might want to paginate. - -`PaginationWindow` also contains `total_items_count`, which specifies how many items there are in total in the filtered -dataset, so that the client knows what the highest offset value can be. - -Let's define the query: - -```py line=41-70 -@strawberry.type -class Query: - @strawberry.field(description="Get a list of users.") - def users(self, - order_by: str, - limit: int, - offset: int = 0, - name: str | None = None, - occupation: str| None = None - ) -> PaginationWindow[User]: - - filters = {} - - if name: - filters['name'] = name - - if occupation: - filters['occupation'] = occupation - - return get_pagination_window( - dataset=user_data, - ItemType=User, - order_by=order_by, - limit=limit, - offset=offset, - filters=filters - ) - -schema = strawberry.Schema(query=Query) -``` - -Now we'll define a mock dataset and implement the `get_pagination_window` function, which is used by the `users` query. - -For the sake of simplicity, our dataset will be an in-memory list containing four users: - -```py line=72-97 -user_data = [ - { - "id": 1, - "name": "Norman Osborn", - "occupation": "Founder, Oscorp Industries", - "age": 42 - }, - { - "id": 2, - "name": "Peter Parker", - "occupation": "Freelance Photographer, The Daily Bugle", - "age": 20 - }, - { - "id": 3, - "name": "Harold Osborn", - "occupation": "President, Oscorp Industries", - "age": 19 - }, - { - "id": 4, - "name": "Eddie Brock", - "occupation": "Journalist, The Eddie Brock Report", - "age": 20 - } -] -``` - -Here's the implementation of the `get_pagination_window` function. Note that it is generic and should work for all item types, -not only for the `User` type. - -```py line=100-146 -def get_pagination_window( - dataset: List[GenericType], - ItemType: type, - order_by: str, - limit: int, - offset: int = 0, - filters: dict[str, str] = {}) -> PaginationWindow: - """ - Get one pagination window on the given dataset for the given limit - and offset, ordered by the given attribute and filtered using the - given filters - """ - - if limit <= 0 or limit > 100: - raise Exception(f'limit ({limit}) must be between 0-100') - - if filters: - dataset = list(filter(lambda x: matches(x, filters), dataset)) - - dataset.sort(key=lambda x: x[order_by]) - - if offset != 0 and not 0 <= offset < len(dataset): - raise Exception(f'offset ({offset}) is out of range ' - f'(0-{len(dataset) - 1})') - - total_items_count = len(dataset) - - items = dataset[offset:offset + limit] - - items = [ItemType.from_row(x) for x in items] - - return PaginationWindow( - items=items, - total_items_count=total_items_count - ) - - -def matches(item, filters): - """ - Test whether the item matches the given filters. - This demo only supports filtering by string fields. - """ - - for attr_name, val in filters.items(): - if val not in item[attr_name]: - return False - return True -``` - -The above code first filters the dataset according to the given filters, then sorts the dataset according to the -given `order_by` field. - -It then calculates `total_items_count` (this must be done after filtering), and then slices the relevant items -according to `offset` and `limit`. - -Finally, it converts the items to the given strawberry type, and returns a `PaginationWindow` containing these items, -as well as the `total_items_count`. - -In a real project, you would probably replace this with code that fetches from a database using `offset` and `limit`. - - - -If you're using Strawberry with the Django web framework, you might want to make use of the -Django pagination API. You can check it out [here](https://docs.djangoproject.com/en/4.0/topics/pagination/). - - - -## Running the Query - -Now, let us start the server and see offset-based pagination in action! - -``` -strawberry server example:schema -``` - -You will get the following message: - -``` -Running strawberry on http://0.0.0.0:8000/graphql ๐Ÿ“ -``` - -Go to [http://0.0.0.0:8000/graphql](http://0.0.0.0:8000/graphql) to -open **GraphiQL**, and run the following query to get first two users, -ordered by name: - -```graphql -{ - users(orderBy: "name", offset: 0, limit: 2) { - items { - name - age - occupation - } - totalItemsCount - } -} -``` - -The result should look like this: - -```graphql -{ - "data": { - "users": { - "items": [ - { - "name": "Eddie Brock", - "age": 20, - "occupation": "Journalist, The Eddie Brock Report" - }, - { - "name": "Harold Osborn", - "age": 19, - "occupation": "President, Oscorp Industries" - } - ], - "totalItemsCount": 4 - } - } -} -``` - -The result contains: - -- `items` - A list of the users in this pagination window -- `totalItemsCount` - The total number of items in the filtered dataset. In this case, since no filter was given - in the request, `totalItemsCount` is 4, which is equal to the total number of users in the in-memory dataset. - -Get the next page of users by running the same query, after incrementing -`offset` by `limit`. - -Repeat until `offset` reaches `totalItemsCount`. - -## Running a Filtered Query - -Let's run the query again, but this time we'll filter out some users based on their occupation. - -```graphql -{ - users(orderBy: "name", offset: 0, limit: 2, occupation: "ie") { - items { - name - age - occupation - } - totalItemsCount - } -} -``` - -By supplying `occupation: "ie"` in the query, we are requesting only users whose occupation -contains the substring "ie". - -This is the result: - -``` -{ - "data": { - "users": { - "items": [ - { - "name": "Eddie Brock", - "age": 20, - "occupation": "Journalist, The Eddie Brock Report" - }, - { - "name": "Harold Osborn", - "age": 19, - "occupation": "President, Oscorp Industries" - } - ], - "totalItemsCount": 3 - } - } -} -``` - -Note that `totalItemsCount` is now 3 and not 4, because only 3 users in total -match the filter. diff --git a/docs/guides/pagination/overview.md b/docs/guides/pagination/overview.md deleted file mode 100644 index 0d29c67986..0000000000 --- a/docs/guides/pagination/overview.md +++ /dev/null @@ -1,176 +0,0 @@ ---- -title: Pagination - Overview ---- - -# Pagination - -Whenever we deal with lists in GraphQL, we usually need to limit the number of items returned. Surely, we don't want to send massive lists of -items that take a considerable toll on the server! The goal of this guide is to help you get going fast with pagination! - -## Pagination at a Glance - -Let us take a look at some of the common ways pagination can be implemented today! - -### Offset-Based Pagination - -This type of pagination is widely used, and it is similar to the syntax we use when looking up database records. - -Here, the client specifies: - -- `limit`: The number of items to be obtained at a time, and -- `offset`: The number of items to be skipped from the beginning. - -Implementing offset-based pagination with an SQL database is straightforward. -We use the `limit` and `offset` values given to query for the items. - -Offset-based pagination also provides us the ability to skip ahead to any offset, -without first needing to get all the items before it. - -Let us understand offset-based pagination better, with an example. Let us assume that we want to request a list of users, two at a time, from a server. -We start by sending a request to the server, with the desired `limit` and `offset` values. - -```json -{ - "limit": 2, - "offset": 0 -} -``` - - - -We are not sending GraphQL requests here, don't worry about the request format for now! We are looking into -pagination conceptually. We'll implement pagination in GraphQL later! - - - -The response from the server would be: - -```json -{ - "users": [ - { - "id": 1, - "name": "Norman Osborn", - "occupation": "Founder, Oscorp Industries", - "age": 42 - }, - { - "id": 2, - "name": "Peter Parker", - "occupation": "Freelance Photographer, The Daily Bugle", - "age": 20 - } - ] -} -``` - -To get the next two users, we can send another request, incrementing `offset` by the value of `limit`. - -```json -{ - "limit": 2, - "offset": 2 -} -``` - -We can repeat this process, incrementing `offset` by the value of `limit`, until we get an empty result. - -#### Pagination Metadata - -In the example above, the result contained no metadata, only the items at the requested offset and limit. - -It may be useful to add metadata to the result. For example, the metadata may specify how many items there -are in total, so that the client knows what the greatest offset value can be. - -```json -{ - "users": [ - ... - ] - "metadata": { - "count": 25 - } -} -``` - -#### Using page_number Instead of offset - -Instead of using `limit` and `offset` as the pagination parameters, it may be more useful to use `page_number` -and `page_size`. - -In such a case, the metadata in the result can be `pages_count`. The client starts the pagination at `page_number` 1, -incrementing by 1 each time to get the next page, and ending when `page_size` is reached. - -This approach may be more in line with what a typical client actually needs when paginating. - -#### Limitations of Offset-Based Pagination - -Offset-based pagination has a few limitations: - -- It is not suitable for large datasets, because we need to access offset + limit number of items from the dataset, before discarding the offset - and only returning the requested items. -- It doesn't work well in environments where records are frequently added or removed, because in such cases, the page window becomes - inconsistent and unreliable. This may result in duplicate items or skipped items across pages. - -However, it provides a quick way to get started, and works well with small-medium datasets. When your dataset scales, you will -need a reliable and consistent way to handle pagination. - -### Cursor based pagination - -Cursor based pagination, also known as keyset pagination, works by returning a pointer to a specific item in the dataset. On subsequent requests, -the server returns results after the given pointer. This method addresses the drawbacks of using offset pagination, but does so by making certain trade offs: - -- The cursor must be based on a unique, sequential identifier in the given source. -- There is no concept of the total number of pages or results in the dataset. -- The client canโ€™t jump to a specific page. - -Let us understand cursor based pagination better, with the example given below. We want to request a list of users, 2 at a time, from -the server. We don't know the cursor initially, so we will assign it a null value. - -```json -{ - "limit": 2, - "cursor": null -} -``` - -The response from the server would be: - -```json -{ - "users": [ - { - "id": 3, - "name": "Harold Osborn", - "occupation": "President, Oscorp Industries", - "age": 19 - }, - { - "id": 4, - "name": "Eddie Brock", - "occupation": "Journalist, The Eddie Brock Report", - "age": 20 - } - ], - "next_cursor": "3" -} -``` - -The next cursor returned by the server can be used to get the next set of users from the server. - -```json -{ - "limit": 2, - "cursor": "3" -} -``` - -This is an example of forward pagination, but pagination can be done backwards too! - -## Implementing pagination in GraphQL - -Let us look at how we can implement pagination in GraphQL. - -- [Implementing Offset Pagination](./offset-based.md) -- [Implementing Cursor Pagination](./cursor-based.md) -- [Implementing the Relay Connection Specification](./connections.md) diff --git a/docs/guides/permissions.md b/docs/guides/permissions.md index 6933ffb482..7a3400ad66 100644 --- a/docs/guides/permissions.md +++ b/docs/guides/permissions.md @@ -15,7 +15,6 @@ import strawberry from strawberry.permission import BasePermission from strawberry.types import Info - class IsAuthenticated(BasePermission): message = "User is not authenticated" @@ -23,7 +22,6 @@ class IsAuthenticated(BasePermission): def has_permission(self, source: typing.Any, info: Info, **kwargs) -> bool: return False - @strawberry.type class Query: user: str = strawberry.field(permission_classes=[IsAuthenticated]) @@ -75,7 +73,6 @@ from starlette.websockets import WebSocket from strawberry.permission import BasePermission from strawberry.types import Info - class IsAuthenticated(BasePermission): message = "User is not authenticated" diff --git a/docs/guides/server.md b/docs/guides/server.md index faeb82aa57..fff0be4759 100644 --- a/docs/guides/server.md +++ b/docs/guides/server.md @@ -10,9 +10,7 @@ framework like Flask or Django. Strawberryโ€™s built in server helps with this use case. It allows to quickly have a development server by running the following command: -```bash -strawberry server package.module:schema -``` + strawberry server package.module:schema where `schema` is the name of a Strawberry schema symbol and `package.module` is the qualified name of the module containing the symbol. The symbol name defaults @@ -24,18 +22,6 @@ this url [http://localhost:8000/graphql](http://localhost:8000/graphql). ## Automatic reloading Strawberry's built in server automatically reloads when changes to the module -containing the `schema` are detected. This way you can spend more time -prototyping your API rather than restarting development servers. - -## Disabling operation logging - -By default Strawberry's built in server logs all operations that are executed. -This can be useful for debugging but can also be annoying if you are -prototyping. - -To disable operation logging you can use the `--log-operations` configuration -flag: - -```bash -strawberry server package.module:schema --log-operations False -``` +containing the `schema` are detected. +This way you can spend more time prototyping your API rather than restarting +development servers. diff --git a/docs/guides/tools.md b/docs/guides/tools.md index 599d55b8ed..7ac31a6db6 100644 --- a/docs/guides/tools.md +++ b/docs/guides/tools.md @@ -76,3 +76,60 @@ type ComboQuery { performA: String! } ``` + +--- + +### `QueryDepthLimiter` + +Extension to add a query depth limter validation rule that limits the complexity of queries by +their depth to protect against malicious queries. + +```python +class QueryDepthLimiter( + max_depth: int, + ignore: Optional[List[Union[str, re.Pattern, Callable[[str], bool]]]] = None, + callback: Optional[Callable[Dict[str, int]]] = None +): + ... +``` + +| Parameter name | Type | Default | Description | +| -------------- | --------------------------------------------------------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------- | +| max_depth | `int` | N/A | The maximum allowed depth for any operation in a GraphQL document | +| ignore | `Optional[List[Union[str, re.Pattern, Callable[[str], bool]]]]` | `None` | Stops recursive depth checking based on a field name. Either a string or regexp to match the name, or a function that reaturns a boolean. | +| callback | `Optional[Callable[[Dict[str, int]], None]]` | `None` | Called each time validation runs. Receives an Object which is a map of the depths for each operation | + +Example: + +```python +import strawberry +from strawberry.extensions import QueryDepthLimiter + +# assuming you already have a Query type +schema = strawberry.Schema( + Query, + extensions=[ + # Add the depth limiter extension + QueryDepthLimiter(max_depth=3), + ] +) + +result = schema.execute_sync( + """ + query MyQuery { + user { + pets { + owner { + pets { + name + } + } + } + } + } + """ + ) +) +assert len(result.errors) == 1 +assert result.errors[0].message == "'MyQuery' exceeds maximum operation depth of 3" +``` diff --git a/docs/images/pagination-graphiql-screenshot.png b/docs/images/pagination-graphiql-screenshot.png new file mode 100644 index 0000000000..53ec3b1ee0 Binary files /dev/null and b/docs/images/pagination-graphiql-screenshot.png differ diff --git a/docs/index.md b/docs/index.md index 11e652e2f0..53bddbf4c9 100644 --- a/docs/index.md +++ b/docs/index.md @@ -6,7 +6,7 @@ title: Getting started with Strawberry This tutorial will help you: -- Obtain a basic understanding of GraphQL principles +- Obtain a basic understanding of GraphQL principle - Define a GraphQL schema using Strawberry - Run the Strawberry server that lets you execute queries against your schema @@ -21,23 +21,17 @@ Strawberry is built on top of Pythonโ€™s Letโ€™s create a new folder: -```bash -mkdir strawberry-demo -cd strawberry-demo -``` + mkdir strawberry-demo + cd strawberry-demo After that we need a new virtualenv: -```bash -python -m venv virtualenv -``` + python -m venv virtualenv Activate the virtualenv and then install strawberry plus the debug server. -```bash -source virtualenv/bin/activate -pip install 'strawberry-graphql[debug-server]' -``` + source virtualenv/bin/activate + pip install 'strawberry-graphql[debug-server]' ## Step 2: Define the schema @@ -52,13 +46,11 @@ contents: import typing import strawberry - @strawberry.type class Book: title: str author: str - @strawberry.type class Query: books: typing.List[Book] @@ -79,8 +71,8 @@ Letโ€™s create a function that returns some books. def get_books(): return [ Book( - title="The Great Gatsby", - author="F. Scott Fitzgerald", + title='The Great Gatsby', + author='F. Scott Fitzgerald', ), ] ``` @@ -125,15 +117,11 @@ schema = strawberry.Schema(query=Query) Then run the following command -```bash -strawberry server schema -``` + strawberry server schema This will start a debug server, you should see the following output: -```bash -Running strawberry on http://0.0.0.0:8000/graphql ๐Ÿ“ -``` + Running strawberry on http://0.0.0.0:8000/graphql ๐Ÿ“ ## Step 6: execute your first query diff --git a/docs/integrations/aiohttp.md b/docs/integrations/aiohttp.md index 8c81172654..e9fd804823 100644 --- a/docs/integrations/aiohttp.md +++ b/docs/integrations/aiohttp.md @@ -4,8 +4,8 @@ title: AIOHTTP # AIOHTTP -Strawberry comes with a basic AIOHTTP integration. It provides a view that you -can use to serve your GraphQL schema: +Strawberry comes with a basic AIOHTTP integration. It provides a view that you can +use to serve your GraphQL schema: ```python import strawberry @@ -15,9 +15,7 @@ from strawberry.aiohttp.views import GraphQLView @strawberry.type class Query: - @strawberry.field - def hello(self, name: str = "World") -> str: - return f"Hello, {name}!" + pass schema = strawberry.Schema(query=Query) @@ -32,26 +30,21 @@ app.router.add_route("*", "/graphql", GraphQLView(schema=schema)) The `GraphQLView` accepts two options at the moment: - `schema`: mandatory, the schema created by `strawberry.Schema`. -- `graphiql`: optional, defaults to `True`, whether to enable the GraphiQL - interface. -- `allow_queries_via_get`: optional, defaults to `True`, whether to enable - queries via `GET` requests +- `graphiql`: optional, defaults to `True`, whether to enable the GraphiQL interface. ## Extending the view -The base `GraphQLView` class can be extended by overriding the following -methods: +The base `GraphQLView` class can be extended by overriding the following methods: - `async get_context(self, request: aiohttp.web.Request, response: aiohttp.web.StreamResponse) -> object` - `async get_root_value(self, request: aiohttp.web.Request) -> object` - `async process_result(self, request: aiohttp.web.Request, result: ExecutionResult) -> GraphQLHTTPResponse` -- `def encode_json(self, data: GraphQLHTTPResponse) -> str` ## get_context -By overriding `GraphQLView.get_context` you can provide a custom context object -for your resolvers. You can return anything here; by default GraphQLView returns -a dictionary with the request. +By overriding `GraphQLView.get_context` you can provide a custom context object for +your resolvers. You can return anything here; by default GraphQLView returns a +dictionary with the request. ```python import strawberry @@ -75,14 +68,12 @@ class Query: Here we are returning a custom context dictionary that contains only one item called `"example"`. -Then we can use the context in a resolver. In this case the resolver will return -`1`. +Then we can use the context in a resolver. In this case the resolver will return `1`. ## get_root_value -By overriding `GraphQLView.get_root_value` you can provide a custom root value -for your schema. This is probably not used a lot but it might be useful in -certain situations. +By overriding `GraphQLView.get_root_value` you can provide a custom root value for your +schema. This is probably not used a lot but it might be useful in certain situations. Here's an example: @@ -102,17 +93,17 @@ class Query: name: str ``` -Here we configure a Query where requesting the `name` field will return -`"Patrick"` through the custom root value. +Here we configure a Query where requesting the `name` field will return `"Patrick"` +through the custom root value. ## process_result -By overriding `GraphQLView.process_result` you can customize and/or process -results before they are sent to a client. This can be useful for logging errors, -or even hiding them (for example to hide internal exceptions). +By overriding `GraphQLView.process_result` you can customize and/or process results +before they are sent to a client. This can be useful for logging errors, or even hiding +them (for example to hide internal exceptions). -It needs to return an object of `GraphQLHTTPResponse` and accepts the request -and execution result. +It needs to return an object of `GraphQLHTTPResponse` and accepts the request and +execution result. ```python from aiohttp import web @@ -120,7 +111,7 @@ from strawberry.aiohttp.views import GraphQLView from strawberry.http import GraphQLHTTPResponse from strawberry.types import ExecutionResult -from graphql.error.graphql_error import format_error as format_graphql_error +from graphql.error import format_error as format_graphql_error class MyGraphQLView(GraphQLView): @@ -137,14 +128,3 @@ class MyGraphQLView(GraphQLView): In this case we are doing the default processing of the result, but it can be tweaked based on your needs. - -## encode_json - -`encode_json` allows to customize the encoding of the JSON response. By default -we use `json.dumps` but you can override this method to use a different encoder. - -```python -class MyGraphQLView(GraphQLView): - def encode_json(self, data: GraphQLHTTPResponse) -> str: - return json.dumps(data, indent=2) -``` diff --git a/docs/integrations/asgi.md b/docs/integrations/asgi.md index ae7b52472f..77fc1e3f75 100644 --- a/docs/integrations/asgi.md +++ b/docs/integrations/asgi.md @@ -5,8 +5,8 @@ title: ASGI # ASGI Strawberry comes with a basic ASGI integration. It provides an app that you can -use to serve your GraphQL schema. Before using Strawberry's ASGI support make -sure you install all the required dependencies by running: +use to serve your GraphQL schema. Before using Strawberry's ASGI support make sure +you install all the required dependencies by running: ``` pip install 'strawberry-graphql[asgi]' @@ -23,19 +23,16 @@ from api.schema import schema app = GraphQL(schema) ``` -Every ASGI server will accept this `app` instance to start the server. For -example if you're using [uvicorn](https://pypi.org/project/uvicorn/) you run the -app with `uvicorn server:app` +Every ASGI server will accept this `app` instance to start the server. +For example if you're using [uvicorn](https://pypi.org/project/uvicorn/) you run the app with `uvicorn server:app` ## Options The `GraphQL` app accepts two options at the moment: -- `schema`: mandatory, the schema created by `strawberry.Schema`. -- `graphiql`: optional, defaults to `True`, whether to enable the GraphiQL +- schema: mandatory, the schema created by `strawberry.Schema`. +- graphiql: optional, defaults to `True`, whether to enable the GraphiQL interface. -- `allow_queries_via_get`: optional, defaults to `True`, whether to enable - queries via `GET` requests ## Extending the view @@ -44,7 +41,6 @@ We allow to extend the base `GraphQL` app, by overriding the following methods: - `async get_context(self, request: Union[Request, WebSocket], response: Optional[Response] = None) -> Any` - `async get_root_value(self, request: Request) -> Any` - `async process_result(self, request: Request, result: ExecutionResult) -> GraphQLHTTPResponse` -- `def encode_json(self, response_data: GraphQLHTTPResponse) -> str` ## get_context @@ -54,9 +50,7 @@ the request and the response. ```python class MyGraphQL(GraphQL): - async def get_context( - self, request: Union[Request, WebSocket], response: Optional[Response] = None - ) -> Any: + async def get_context(self, request: Union[Request, WebSocket], response: Optional[Response] = None) -> Any: return {"example": 1} @@ -75,12 +69,10 @@ case. ### Setting response headers -It is possible to use `get_context` to set response headers. A common use case -might be cookie-based user authentication, where your login mutation resolver -needs to set a cookie on the response. +It is possible to use `get_context` to set response headers. A common use case might be cookie-based user authentication, +where your login mutation resolver needs to set a cookie on the response. -This is possible by updating the response object contained inside the context of -the `Info` object. +This is possible by updating the response object contained inside the context of the `Info` object. ```python @strawberry.type @@ -94,17 +86,14 @@ class Mutation: ### Setting background tasks -Similarly, [background tasks](https://www.starlette.io/background/) can be set -on the response via the context: +Similarly, [background tasks](https://www.starlette.io/background/) can be set on the response via the context: ```python from starlette.background import BackgroundTask - async def notify_new_flavour(name: str): ... - @strawberry.type class Mutation: @strawberry.mutation @@ -146,8 +135,7 @@ and the execution results. from strawberry.http import GraphQLHTTPResponse from strawberry.types import ExecutionResult -from graphql.error.graphql_error import format_error as format_graphql_error - +from graphql.error import format_error as format_graphql_error class MyGraphQL(GraphQL): async def process_result( @@ -163,14 +151,3 @@ class MyGraphQL(GraphQL): In this case we are doing the default processing of the result, but it can be tweaked based on your needs. - -## encode_json - -`encode_json` allows to customize the encoding of the JSON response. By default -we use `json.dumps` but you can override this method to use a different encoder. - -```python -class MyGraphQLView(GraphQL): - def encode_json(self, data: GraphQLHTTPResponse) -> str: - return json.dumps(data, indent=2) -``` diff --git a/docs/integrations/chalice.md b/docs/integrations/chalice.md index cabeeb3ad8..46eb311aa6 100644 --- a/docs/integrations/chalice.md +++ b/docs/integrations/chalice.md @@ -41,7 +41,7 @@ class Mutation: schema = strawberry.Schema(query=Query, mutation=Mutation) -view = GraphQLView(schema=schema, graphiql=True) +view = GraphQLView(schema=schema, render_graphiql=True) @app.route("/graphql", methods=["GET", "POST"], content_types=["application/json"]) @@ -49,6 +49,7 @@ def handle_graphql() -> Response: request: Request = app.current_request result = view.execute_request(request) return result + ``` And then run `chalice local` to start the localhost @@ -65,99 +66,3 @@ The `GraphQLView` accepts two options at the moment: - `schema`: mandatory, the schema created by `strawberry.Schema`. - `graphiql`: optional, defaults to `True`, whether to enable the GraphiQL interface. - -## Extending the view - -We allow to extend the base `GraphQLView`, by overriding the following methods: - -- `get_context(self, request: Request, response: TemporalResponse) -> Any` -- `get_root_value(self, request: Request) -> Any` -- `process_result(self, request: Request, result: ExecutionResult) -> GraphQLHTTPResponse` -- `encode_json(self, response_data: GraphQLHTTPResponse) -> str` - -## get_context - -`get_context` allows to provide a custom context object that can be used in your -resolver. You can return anything here, by default we return a dictionary with -the request. By default; the `Response` object from `flask` is injected via the -parameters. - -```python -class MyGraphQLView(GraphQLView): - def get_context(self, response: Response) -> Any: - return {"example": 1} - - -@strawberry.type -class Query: - @strawberry.field - def example(self, info: Info) -> str: - return str(info.context["example"]) -``` - -Here we are returning a custom context dictionary that contains only one item -called "example". - -Then we use the context in a resolver, the resolver will return "1" in this -case. - -## get_root_value - -`get_root_value` allows to provide a custom root value for your schema, this is -probably not used a lot but it might be useful in certain situations. - -Here's an example: - -```python -class MyGraphQLView(GraphQLView): - def get_root_value(self) -> Any: - return Query(name="Patrick") - - -@strawberry.type -class Query: - name: str -``` - -Here we are returning a Query where the name is "Patrick", so we when requesting -the field name we'll return "Patrick" in this case. - -## process_result - -`process_result` allows to customize and/or process results before they are sent -to the clients. This can be useful logging errors or hiding them (for example to -hide internal exceptions). - -It needs to return an object of `GraphQLHTTPResponse` and accepts the execution -result. - -```python -from strawberry.http import GraphQLHTTPResponse -from strawberry.types import ExecutionResult - -from graphql.error.graphql_error import format_error as format_graphql_error - - -class MyGraphQLView(GraphQLView): - def process_result(self, result: ExecutionResult) -> GraphQLHTTPResponse: - data: GraphQLHTTPResponse = {"data": result.data} - - if result.errors: - data["errors"] = [format_graphql_error(err) for err in result.errors] - - return data -``` - -In this case we are doing the default processing of the result, but it can be -tweaked based on your needs. - -## encode_json - -`encode_json` allows to customize the encoding of the JSON response. By default -we use `json.dumps` but you can override this method to use a different encoder. - -```python -class MyGraphQLView(GraphQLView): - def encode_json(self, data: GraphQLHTTPResponse) -> str: - return json.dumps(data, indent=2) -``` diff --git a/docs/integrations/channels.md b/docs/integrations/channels.md deleted file mode 100644 index 5071ca1eab..0000000000 --- a/docs/integrations/channels.md +++ /dev/null @@ -1,523 +0,0 @@ ---- -title: Channels ---- - -# Channels - -Strawberry provides support for [Channels](https://channels.readthedocs.io/) -with -[Consumers](https://channels.readthedocs.io/en/stable/topics/consumers.html) to -provide GraphQL support over WebSockets and HTTP. - -## Introduction - -While Channels does require Django to be installed as a dependency, you can -actually run this integration without using Django's request handler. However, -the most common use case will be to run a normal Django project with GraphQL -subscriptions support, typically taking advantage of the Channel Layers -functionality which is exposed through the Strawberry integration. - ---- - -## Getting Started - -### Pre-requisites - -Make sure you have read the following Channels documentation: - -- [Introduction](https://channels.readthedocs.io/en/stable/introduction.html#) -- [Tutorial](https://channels.readthedocs.io/en/stable/tutorial/index.html) -- [Consumers](https://channels.readthedocs.io/en/stable/topics/consumers.html) -- And our [Subscriptions](../general/subscriptions) documentation. - -If you have read the Channels documentation, You should know by now that: - -1. ASGI application is a callable that can handle multiple send / receive - operations without the need of a new application instance. -2. Channels is all about making ASGI applications instances (whether in another - processes or in another machine) talk to each other seamlessly. -3. A `scope` is a single connection represented by a dict, whether it would be a - websocket or an HTTP request or another protocol. -4. A `Consumer` is an ASGI application abstraction that helps to handle a single - scope. - -### Installation - -Before using Strawberry's Channels support, make sure you install all the -required dependencies by running: - -```shell -pip install 'strawberry-graphql[channels]' -``` - ---- - -## Tutorial - -_The following example will pick up where the Channels tutorials left off._ - -By the end of This tutorial, You will have a graphql chat subscription that will -be able to talk with the channels chat consumer from the tutorial. - -### Types setup - -First, let's create some Strawberry-types for the chat. - -```python -# mysite/gqlchat/subscription.py - - -@strawberry.input -class ChatRoom: - room_name: str - - -@strawberry.type -class ChatRoomMessage: - room_name: str - current_user: str - message: str -``` - -### Channel Layers - -The Context for Channels integration includes the consumer, which has an -instance of the channel layer and the consumer's channel name. This tutorial is -an example of how this can be used in the schema to provide subscriptions to -events generated by background tasks or the web server. Even if these are -executed in other threads, processes, or even other servers, if you are using a -Layer backend like Redis or RabbitMQ you should receive the events. - -To set this up, you'll need to make sure Channel Layers is configured as per the -[documentation](https://channels.readthedocs.io/en/stable/topics/channel_layers.html). - -Then you'll want to add a subscription that accesses the channel layer and joins -one or more broadcast groups. - -Since listening for events and passing them along to the client is a common use -case, the base consumer provides a high level API for that using a generator -pattern, as we will see below. - -### The chat subscription - -Now we will create the chat [subscription](../general/subscriptions.md). - -```python -# mysite/gqlchat/subscription.py - - -@strawberry.type -class Subscription: - @strawberry.subscription - async def join_chat_rooms( - self, - info: Info, - rooms: List[ChatRoom], - user: str, - ) -> AsyncGenerator[ChatRoomMessage, None]: - """Join and subscribe to message sent to the given rooms.""" - ws = info.context.ws - channel_layer = ws.channel_layer - - room_ids = [f"chat_{room.room_name}" for room in rooms] - - for room in room_ids: - # Join room group - await channel_layer.group_add(room, ws.channel_name) - - for room in room_ids: - await channel_layer.group_send( - room, - { - "type": "chat.message", - "room_id": room, - "message": f"process: {os.getpid()} thread: {threading.current_thread().name}" - f" -> Hello my name is {user}!", - }, - ) - - async for message in ws.channel_listen("chat.message", groups=room_ids): - yield ChatRoomMessage( - room_name=message["room_id"], - message=message["message"], - current_user=user, - ) -``` - -Explanation: `Info.context.ws` or `Info.context.request` is a pointer to the -[`ChannelsConsumer`](#channelsconsumer) instance. Here we have first sent a -message to all the channel_layer groups (specified in the subscription argument -`rooms`) that we have joined the chat. - - - -We do not need to call `await channel_layer.group_add(room, ws.channel_name)` If -we don't want to send an initial message while instantiating the subscription. -It is handled by `ws.channel_listen`. - - - -### Chat message mutation - -If you noticed, the subscription client can't send a message willingly. You will -have to create a mutation for sending messages via the `channel_layer` - -```python -# mysite/gqlchat/subscription.py - - -class Mutation: - @strawberry.mutation - async def send_chat_message( - self, - info: Info, - room: ChatRoom, - message: str, - ) -> None: - ws = info.context.ws - channel_layer = ws.channel_layer - - await channel_layer.group_send( - f"chat_{room.room_name}", - { - "type": "chat.message", - "room_id": room.room_name, - "message": message, - }, - ) -``` - -### Creating the consumers - -All we did so far is useless without creating an asgi consumer for our schema. -The easiest way to do that is to use the -[`GraphQLProtocolTypeRouter`](#graphqlprotocoltyperouter) which will wrap your -Django application, and route **HTTP and websockets** for `"/graphql"` to -Strawberry, while sending all other requests to Django. You'll need to modify -the `myproject.asgi.py` file from the Channels instructions to look something -like this: - -```python -import os - -from django.core.asgi import get_asgi_application -from strawberry.channels import GraphQLProtocolTypeRouter - -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mysite.settings") -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 mysite.graphql import schema - - -application = GraphQLProtocolTypeRouter( - schema, - django_application=django_asgi_app, -) -``` - -This approach is not very flexible, taking away some useful capabilities of -Channels. For more complex deployments, i.e you want to integrate several ASGI -applications on different URLs and protocols, like what is described in the -[Channels documentation](https://channels.readthedocs.io/en/stable/topics/protocols.html). -You will probably craft your own -[`ProtocolTypeRouter`](https://channels.readthedocs.io/en/stable/topics/routing.html#protocoltyperouter). - -An example of this (continuing from channels tutorial) would be: - -```python -import os -from channels.auth import AuthMiddlewareStack -from channels.routing import ProtocolTypeRouter, URLRouter -from django.core.asgi import get_asgi_application -from django.urls import re_path -from strawberry.channels import GraphQLHTTPConsumer, GraphQLWSConsumer - -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "berry.settings") -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 chat import routing -from mysite.graphql import schema - -websocket_urlpatterns = routing.websocket_urlpatterns + [ - re_path(r"graphql", GraphQLWSConsumer.as_asgi(schema=schema)), -] - - -gql_http_consumer = AuthMiddlewareStack(GraphQLHTTPConsumer.as_asgi(schema=schema)) -gql_ws_consumer = GraphQLWSConsumer.as_asgi(schema=schema) -application = ProtocolTypeRouter( - { - "http": URLRouter( - [ - re_path("^graphql", gql_http_consumer), - re_path( - "^", django_asgi_app - ), # This might be another endpoint in your app - ] - ), - "websocket": AuthMiddlewareStack(URLRouter(websocket_urlpatterns)), - } -) -``` - -This example demonstrates some ways that Channels can be set up to handle -routing. A very common scenario will be that you want user and session -information inside the GraphQL context, which the AuthMiddlewareStack wrapper -above will provide. It might be apparent by now, there's no reason at all why -you couldn't run a Channels server without any Django ASGI application at all. -However, take care to ensure you run `django.setup()` instead of -`get_asgi_application()`, if you need any Django ORM or other Django features in -Strawberry. - -### Running our example - -First run your asgi application _(The ProtocolTypeRouter)_ using your asgi -server. If you are coming from the channels tutorial, there is no difference. -Then open three different tabs on your browser and go to the following URLs: - -1. `localhost:8000/graphql` -2. `localhost:8000/graphql` -3. `localhost:8000/chat` - -If you want, you can run 3 different instances of your application with -different ports it should work the same! - -On tab #1 start the subscription: - -```graphql -subscription SubscribeToChatRooms { - joinChatRooms( - rooms: [{ roomName: "room1" }, { roomName: "room2" }] - user: "foo" - ) { - roomName - message - currentUser - } -} -``` - -On tab #2 we will run `sendChatMessage` mutation: - -```graphql -mutation echo { - sendChatMessage(message: "hello room 1", room: { roomName: "room1" }) -} -``` - -On tab #3 we will join the room you subscribed to ("room1") and start chatting. -Before we do that there is a slight change we need to make in the `ChatConsumer` -you created with channels in order to make it compatible with our -`ChatRoomMessage` type. - -```python -# Send message to room group -await self.channel_layer.group_send( - self.room_group_name, - { - "type": "chat.message", - "room_id": self.room_group_name, # <<< here is the change - "message": f"process is {os.getpid()}, Thread is {threading.current_thread().name}" - f" -> {message}", - }, -) -``` - -Look here for some more complete examples: - -1. The - [Strawberry Examples repo](https://github.com/strawberry-graphql/examples) - contains a basic example app demonstrating subscriptions with Channels. - ---- - -## Testing - -To test our chat app we can use the Channels -[`ApplicationCommunicator`](https://channels.readthedocs.io/en/stable/topics/testing.html#applicationcommunicator). -Here is an example based on the tutorial above: _Make sure you have pytest-async -installed_ - -```python -from channels.testing import WebsocketCommunicator -import pytest -from strawberry.channels import GraphQLWSConsumer -from strawberry.subscriptions import GRAPHQL_WS_PROTOCOL -from strawberry.subscriptions.protocols.graphql_ws import ( - GQL_CONNECTION_ACK, - GQL_CONNECTION_INIT, - GQL_DATA, - GQL_START, -) - -from mysite.graphql import schema - - -class DebuggableGraphQLWSConsumer(GraphQLWSConsumer): - async def get_context(self, *args, **kwargs) -> object: - context = await super().get_context(*args, **kwargs) - context.tasks = self._handler.tasks - context.connectionInitTimeoutTask = None - return context - - -@pytest.fixture -async def ws(): - client = WebsocketCommunicator( - DebuggableGraphQLWSConsumer.as_asgi( - schema=schema, subscription_protocols=(GRAPHQL_WS_PROTOCOL,) - ), - "", - subprotocols=[ - GRAPHQL_WS_PROTOCOL, - ], - ) - res = await client.connect() - assert res == (True, GRAPHQL_WS_PROTOCOL) - - yield client - - await client.disconnect() - - -chat_subscription_query = """ - subscription fooChat { - joinChatRooms( - rooms: [{ roomName: "room1" }, { roomName: "room2" }] - user: "foo"){ - roomName - message - currentUser - } - } -""" - - -@pytest.mark.asyncio -async def test_joinChatRooms_sends_welcome_message(ws): - await ws.send_json_to({"type": GQL_CONNECTION_INIT}) - await ws.send_json_to( - { - "type": GQL_START, - "id": "demo_consumer", - "payload": {"query": f"{chat_subscription_query}"}, - } - ) - response = await ws.receive_json_from() - assert response["type"] == GQL_CONNECTION_ACK - - response = await ws.receive_json_from() - assert response["type"] == GQL_DATA - assert response["id"] == "demo_consumer" - data = response["payload"]["data"]["joinChatRooms"] - assert data["currentUser"] == "foo" - assert "room1" in data["roomName"] - assert "hello" in data["message"] -``` - -In order to test a real server connection we can use python -[gql client](https://github.com/graphql-python/gql) and channels -[`ChannelsLiveServerTestCase`](https://channels.readthedocs.io/en/latest/topics/testing.html#channelsliveservertestcase). - - - -This example is based on the extended `ChannelsLiveServerTestCase` class from -channels tutorial part 4. **You cannot run this test with a pytest session.** - - - -Add this test in your `ChannelsLiveServerTestCase` extended class: - -```python -from gql import Client, gql -from gql.transport.websockets import WebsocketsTransport - - -def test_send_message_via_channels_chat_joinChatRooms_recieves(self): - transport = WebsocketsTransport(url=self.live_server_ws_url + "/graphql") - - client = Client( - transport=transport, - fetch_schema_from_transport=False, - ) - - query = gql(chat_subscription_query) - for index, result in enumerate(client.subscribe(query)): - if index == 0 or 1: - print(result) - # because we subscribed to 2 rooms we received two welcome messages. - elif index == 2: - print(result) - assert "hello from web browser" in result["joinChatRooms"]["message"] - break - - try: - self._enter_chat_room("room1") - self._post_message("hello from web browser") - finally: - self._close_all_new_windows() -``` - ---- - -## API - -### GraphQLProtocolTypeRouter - -A helper for creating a common strawberry-django -[`ProtocolTypeRouter`](https://channels.readthedocs.io/en/stable/topics/routing.html#protocoltyperouter) -Implementation. - -Example usage: - -``` -from strawberry.channels import GraphQLProtocolTypeRouter -from django.core.asgi import get_asgi_application - -django_asgi = get_asgi_application() - -from myapi import schema - -application = GraphQLProtocolTypeRouter( - 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. - -### ChannelsConsumer - -Strawberries extended -[`AsyncConsumer`](https://channels.readthedocs.io/en/stable/topics/consumers.html#consumers). - -#### \*\*Every graphql session will have an instance of this class inside - -`info.ws` which is actually the `info.context.request`.\*\* - -#### properties - -- `ws.headers: dict` returns a map of the headers from `scope['headers']`. - -```python -async def channel_listen( - self, - type: str, - *, - timeout: float | None = None, - groups: Sequence[str] | None = None -): # AsyncGenerator - ... -``` - -- `type` - The type of the message to wait for, equivalent to `scope['type']` -- `timeout` - An optional timeout to wait for each subsequent message. -- `groups` - list of groups to yield messages from threw channel layer. diff --git a/docs/integrations/django.md b/docs/integrations/django.md index 9bc250498f..fe8af7487d 100644 --- a/docs/integrations/django.md +++ b/docs/integrations/django.md @@ -4,8 +4,7 @@ title: Django # Django -Strawberry comes with a basic -[Django integration](https://github.com/strawberry-graphql/strawberry-graphql-django). +Strawberry comes with a basic [Django integration](https://github.com/strawberry-graphql/strawberry-graphql-django). It provides a view that you can use to serve your GraphQL schema: ```python @@ -25,35 +24,25 @@ project, this is needed to provide the template for the GraphiQL interface. ## Options -The `GraphQLView` accepts the following arguments: +The `GraphQLView` accepts five options at the moment: - `schema`: mandatory, the schema created by `strawberry.Schema`. - `graphiql`: optional, defaults to `True`, whether to enable the GraphiQL interface. -- `allow_queries_via_get`: optional, defaults to `True`, whether to enable - queries via `GET` requests -- `subscriptions_enabled`: optional boolean paramenter enabling subscriptions in - the GraphiQL interface, defaults to `False`. - -## Deprecated options - -The following options are deprecated and will be removed in a future release: - +- `subscriptions_enabled`: optional boolean paramenter enabling subscriptions + in the GraphiQL interface, defaults to `False`. - `json_encoder`: optional JSON encoder, defaults to `DjangoJSONEncoder`, will be used to serialize the data. -- `json_dumps_params`: optional dictionary of keyword arguments to pass to the - `json.dumps` call used to generate the response. To get the most compact JSON - representation, you should specify `{"separators": (",", ":")}`, defaults to - `None`. - -You can extend the view and override `encode_json` to customize the JSON -encoding process. +- `json_dumps_params`: optional dictionary of keyword arguments to pass to + the `json.dumps` call used to generate the response. To get the most compact + JSON representation, you should specify `{"separators": (",", ":")}`, + defaults to `None`. ## Extending the view We allow to extend the base `GraphQLView`, by overriding the following methods: -- `get_context(self, request: HttpRequest, response: HttpResponse) -> Any` +- `get_context(self, request: HttpRequest) -> Any` - `get_root_value(self, request: HttpRequest) -> Any` - `process_result(self, request: HttpRequest, result: ExecutionResult) -> GraphQLHTTPResponse` @@ -126,8 +115,7 @@ and the execution results. from strawberry.http import GraphQLHTTPResponse from strawberry.types import ExecutionResult -from graphql.error.graphql_error import format_error as format_graphql_error - +from graphql.error import format_error as format_graphql_error class MyGraphQLView(GraphQLView): def process_result( @@ -165,15 +153,11 @@ project, this is needed to provide the template for the GraphiQL interface. ## Options -The `AsyncGraphQLView` accepts the following arguments: +The `AsyncGraphQLView` accepts two options at the moment: -- `schema`: mandatory, the schema created by `strawberry.Schema`. -- `graphiql`: optional, defaults to `True`, whether to enable the GraphiQL +- schema: mandatory, the schema created by `strawberry.Schema`. +- graphiql: optional, defaults to `True`, whether to enable the GraphiQL interface. -- `allow_queries_via_get`: optional, defaults to `True`, whether to enable - queries via `GET` requests -- `subscriptions_enabled`: optional boolean paramenter enabling subscriptions in - the GraphiQL interface, defaults to `False`. ## Extending the view @@ -183,7 +167,6 @@ methods: - `async get_context(self, request: HttpRequest) -> Any` - `async get_root_value(self, request: HttpRequest) -> Any` - `async process_result(self, request: HttpRequest, result: ExecutionResult) -> GraphQLHTTPResponse` -- `def encode_json(self, data: GraphQLHTTPResponse) -> str` ## get_context @@ -244,8 +227,7 @@ and the execution results. from strawberry.http import GraphQLHTTPResponse from strawberry.types import ExecutionResult -from graphql.error.graphql_error import format_error as format_graphql_error - +from graphql.error import format_error as format_graphql_error class MyGraphQLView(AsyncGraphQLView): async def process_result( @@ -261,21 +243,3 @@ class MyGraphQLView(AsyncGraphQLView): In this case we are doing the default processing of the result, but it can be tweaked based on your needs. - -## encode_json - -`encode_json` allows to customize the encoding of the JSON response. By default -we use `json.dumps` but you can override this method to use a different encoder. - -```python -class MyGraphQLView(AsyncGraphQLView): - def encode_json(self, data: GraphQLHTTPResponse) -> str: - return json.dumps(data, indent=2) -``` - -## Subscriptions - -Subscriptions run over websockets and thus depend on -[channels](https://channels.readthedocs.io/). Take a look at our -[channels integraton](/docs/integrations/channels.md) page for more information -regarding it. diff --git a/docs/integrations/fastapi.md b/docs/integrations/fastapi.md index 4a6af63cc9..da5e51c184 100644 --- a/docs/integrations/fastapi.md +++ b/docs/integrations/fastapi.md @@ -24,14 +24,12 @@ import strawberry from fastapi import FastAPI from strawberry.fastapi import GraphQLRouter - @strawberry.type class Query: @strawberry.field def hello(self) -> str: return "Hello World" - schema = strawberry.Schema(Query) graphql_app = GraphQLRouter(schema) @@ -44,20 +42,18 @@ app.include_router(graphql_app, prefix="/graphql") The `GraphQLRouter` accepts the following options: -- `schema`: mandatory, the schema created by `strawberry.Schema`. -- `graphiql`: optional, defaults to `True`, whether to enable the GraphiQL +- schema: mandatory, the schema created by `strawberry.Schema`. +- graphiql: optional, defaults to `True`, whether to enable the GraphiQL interface. -- `allow_queries_via_get`: optional, defaults to `True`, whether to enable - queries via `GET` requests -- `context_getter`: optional FastAPI dependency for providing custom context +- context_getter: optional FastAPI dependency for providing custom context value. -- `root_value_getter`: optional FastAPI dependency for providing custom root +- root_value_getter: optional FastAPI dependency for providing custom root value. ## context_getter -The `context_getter` option allows you to provide a custom context object that -can be used in your resolver. `context_getter` is a +The `context_getter` option allows you to provide a custom context object that can be +used in your resolver. `context_getter` is a [FastAPI dependency](https://fastapi.tiangolo.com/tutorial/dependencies/) and can inject other dependencies if you so wish. @@ -66,13 +62,13 @@ There are two options at your disposal here: 1. Define your custom context as a dictionary, 2. Define your custom context as a class. -If no context is supplied, then the default context returned is a dictionary -containing the request, the response, and any background tasks. +If no context is supplied, then the default context returned is a dictionary containing +the request, the response, and any background tasks. However, you can define a class-based custom context inline with [FastAPI practice](https://fastapi.tiangolo.com/tutorial/dependencies/classes-as-dependencies/). -If you choose to do this, you must ensure that your custom context class -inherits from `BaseContext` or an `InvalidCustomContext` exception is raised. +If you choose to do this, you must ensure that your custom context class inherits from +`BaseContext` or an `InvalidCustomContext` exception is raised. For dictionary-based custom contexts, an example might look like the following. @@ -102,12 +98,11 @@ class Query: def example(self, info: Info) -> str: return f"Hello {info.context['custom_value']}" - schema = strawberry.Schema(Query) graphql_app = GraphQLRouter( - schema, - context_getter=get_context, + schema, + context_getter=get_context, ) app = FastAPI() @@ -119,8 +114,8 @@ called "custom*value", which is injected from `custom_context_dependency`. This value exists alongside `request`, `response`, and `background_tasks` in the `info.context` \_dictionary* and so it requires `['request']` indexing. -Then we use the context in a resolver. The resolver will return "Hello John" in -this case. +Then we use the context in a resolver. +The resolver will return "Hello John" in this case. For class-based custom contexts, an example might look like the following. @@ -154,26 +149,25 @@ class Query: def example(self, info: Info) -> str: return f"Hello {info.context.name}, {info.context.greeting}" - schema = strawberry.Schema(Query) graphql_app = GraphQLRouter( - schema, - context_getter=get_context, + schema, + context_getter=get_context, ) app = FastAPI() app.include_router(graphql_app, prefix="/graphql") ``` -In this case, we are returning a custom context class that inherits from -BaseContext with fields `name` and `greeting`, which is also injected by -`custom_context_dependency`. These custom values exist alongside `request`, -`response`, and `background_tasks` in the `info.context` _class_ and so it -requires `.request` indexing. +In this case, we are returning a custom context class that inherits +from BaseContext with fields `name` and `greeting`, which is also +injected by `custom_context_dependency`. These custom values exist +alongside `request`, `response`, and `background_tasks` in the +`info.context` _class_ and so it requires `.request` indexing. -Then we use the context in a resolver. The resolver will return โ€œHello John, you -rock!โ€ in this case. +Then we use the context in a resolver. +The resolver will return โ€œHello John, you rock!โ€ in this case. ### Setting background tasks @@ -188,7 +182,6 @@ from fastapi import FastAPI, BackgroundTasks from strawberry.types import Info from strawberry.fastapi import GraphQLRouter - async def notify_new_flavour(name: str): print(name) @@ -216,13 +209,12 @@ app = FastAPI() app.include_router(graphql_app, prefix="/graphql") ``` -If using a custom context class, then background tasks should be stored within -the class object as `.background_tasks`. +If using a custom context class, then background tasks should be stored within the class object as `.background_tasks`. ## root_value_getter -The `root_value_getter` option allows you to provide a custom root value for -your schema. This is most likely a rare usecase but might be useful in certain +The `root_value_getter` option allows you to provide a custom root value for your +schema. This is most likely a rare usecase but might be useful in certain situations. Here's an example: @@ -252,6 +244,7 @@ graphql_app = GraphQLRouter( app = FastAPI() app.include_router(graphql_app, prefix="/graphql") + ``` Here we are returning a Query where the name is "Patrick", so when we request @@ -259,12 +252,12 @@ the field name we'll return "Patrick". ## process_result -The `process_result` option allows you to customize and/or process results -before they are sent to the clients. This can be useful for logging errors or -hiding them (for example to hide internal exceptions). +The `process_result` option allows you to customize and/or process results before they are sent +to the clients. This can be useful for logging errors or hiding them (for example to +hide internal exceptions). -It needs to return a `GraphQLHTTPResponse` object and accepts the request and -execution results. +It needs to return a `GraphQLHTTPResponse` object and accepts the request +and execution results. ```python from fastapi import Request @@ -272,11 +265,11 @@ from strawberry.fastapi import GraphQLRouter from strawberry.http import GraphQLHTTPResponse from strawberry.types import ExecutionResult -from graphql.error.graphql_error import format_error as format_graphql_error - +from graphql.error import format_error as format_graphql_error class MyGraphQLRouter(GraphQLRouter): - async def process_result( + + async def process_result( self, request: Request, result: ExecutionResult ) -> GraphQLHTTPResponse: data: GraphQLHTTPResponse = {"data": result.data} @@ -289,14 +282,3 @@ class MyGraphQLRouter(GraphQLRouter): In this case we are doing the default processing of the result, but it can be tweaked based on your needs. - -## encode_json - -`encode_json` allows to customize the encoding of the JSON response. By default -we use `json.dumps` but you can override this method to use a different encoder. - -```python -class MyGraphQLRouter(GraphQLRouter): - def encode_json(self, data: GraphQLHTTPResponse) -> str: - return json.dumps(data, indent=2) -``` diff --git a/docs/integrations/flask.md b/docs/integrations/flask.md index dcb796ec2f..4b0bf531da 100644 --- a/docs/integrations/flask.md +++ b/docs/integrations/flask.md @@ -8,7 +8,6 @@ Strawberry comes with a basic Flask integration. It provides a view that you can use to serve your GraphQL schema: ```python -from flask import Flask from strawberry.flask.views import GraphQLView from api.schema import schema @@ -19,56 +18,33 @@ app.add_url_rule( "/graphql", view_func=GraphQLView.as_view("graphql_view", schema=schema), ) - -if __name__ == "__main__": - app.run() -``` - -If you'd prefer to use an asynchronous view you can instead use the following -import which has the same interface as `GraphQLView`. This is helpful if using a -dataloader. - -```python -from strawberry.flask.views import AsyncGraphQLView ``` ## Options The `GraphQLView` accepts two options at the moment: -- `schema`: mandatory, the schema created by `strawberry.Schema`. -- `graphiql:` optional, defaults to `True`, whether to enable the GraphiQL +- schema: mandatory, the schema created by `strawberry.Schema`. +- graphiql: optional, defaults to `True`, whether to enable the GraphiQL interface. -- `allow_queries_via_get`: optional, defaults to `True`, whether to enable - queries via `GET` requests ## Extending the view We allow to extend the base `GraphQLView`, by overriding the following methods: -- `get_context(self, response: Response) -> Any` +- `get_context(self) -> Any` - `get_root_value(self) -> Any` - `process_result(self, result: ExecutionResult) -> GraphQLHTTPResponse` -- `encode_json(self, response_data: GraphQLHTTPResponse) -> str` - - - -Note that the `AsyncGraphQLView` can also be extended by overriding the same -methods above, but `get_context`, `get_root_value` and `process_result` are -async functions. - - ## get_context `get_context` allows to provide a custom context object that can be used in your resolver. You can return anything here, by default we return a dictionary with -the request. By default; the `Response` object from `flask` is injected via the -parameters. +the request. ```python class MyGraphQLView(GraphQLView): - def get_context(self, response: Response) -> Any: + def get_context(self) -> Any: return {"example": 1} @@ -112,18 +88,18 @@ the field name we'll return "Patrick" in this case. to the clients. This can be useful logging errors or hiding them (for example to hide internal exceptions). -It needs to return an object of `GraphQLHTTPResponse` and accepts the execution -result. +It needs to return an object of `GraphQLHTTPResponse` and accepts the execution result. ```python from strawberry.http import GraphQLHTTPResponse from strawberry.types import ExecutionResult -from graphql.error.graphql_error import format_error as format_graphql_error - +from graphql.error import format_error as format_graphql_error class MyGraphQLView(GraphQLView): - def process_result(self, result: ExecutionResult) -> GraphQLHTTPResponse: + def process_result( + self, result: ExecutionResult + ) -> GraphQLHTTPResponse: data: GraphQLHTTPResponse = {"data": result.data} if result.errors: @@ -134,14 +110,3 @@ class MyGraphQLView(GraphQLView): In this case we are doing the default processing of the result, but it can be tweaked based on your needs. - -## encode_json - -`encode_json` allows to customize the encoding of the JSON response. By default -we use `json.dumps` but you can override this method to use a different encoder. - -```python -class MyGraphQLView(GraphQLView): - def encode_json(self, data: GraphQLHTTPResponse) -> str: - return json.dumps(data, indent=2) -``` diff --git a/docs/integrations/pydantic.md b/docs/integrations/pydantic.md index e711b05fb2..41d75943e7 100644 --- a/docs/integrations/pydantic.md +++ b/docs/integrations/pydantic.md @@ -33,7 +33,6 @@ import strawberry from .models import User - @strawberry.experimental.pydantic.type(model=User) class UserType: id: strawberry.auto @@ -57,7 +56,6 @@ import strawberry from .models import User - @strawberry.experimental.pydantic.type(model=User, all_fields=True) class UserType: pass @@ -73,7 +71,6 @@ import strawberry from .models import User - @strawberry.experimental.pydantic.input(model=User) class UserInput: id: strawberry.auto @@ -91,33 +88,27 @@ import strawberry from pydantic import BaseModel from typing import List - # pydantic types class User(BaseModel): id: int name: str - class NormalUser(User): friends: List[int] = [] - class AdminUser(User): role: int - # strawberry types @strawberry.experimental.pydantic.interface(model=User) class UserType: id: strawberry.auto name: strawberry.auto - @strawberry.experimental.pydantic.type(model=NormalUser) class NormalUserType(UserType): # note the base class friends: strawberry.auto - @strawberry.experimental.pydantic.type(model=AdminUser) class AdminUserType(UserType): role: strawberry.auto @@ -211,8 +202,7 @@ class UserType: id: strawberry.auto name: strawberry.auto - -instance = User(id="123", name="Jake") +instance = User(id='123', name='Jake') data = UserType.from_pydantic(instance) ``` @@ -238,10 +228,9 @@ class UserType: name: strawberry.auto age: int +instance = User(id='123', name='Jake') -instance = User(id="123", name="Jake") - -data = UserType.from_pydantic(instance, extra={"age": 10}) +data = UserType.from_pydantic(instance, extra={'age': 10}) ``` The data dictionary structure follows the structure of your data -- if you have @@ -270,52 +259,12 @@ class UserInput: id: strawberry.auto name: strawberry.auto - -input_data = UserInput(id="abc", name="Jake") +input_data = UserInput(id='abc', name='Jake') # this will run pydantic's validation instance = input_data.to_pydantic() ``` -### Constrained types - -Strawberry supports [pydantic constrained types](https://pydantic-docs.helpmanual.io/usage/types/#constrained-types). -Note that constraint is not enforced in the graphql type. Thus, we recommend always working on the pydantic -type such that the validation is enforced. - -```python+schema -from pydantic import BaseModel, conlist -import strawberry - -class Example(BaseModel): - friends: conlist(str, min_items=1) - -@strawberry.experimental.pydantic.input(model=Example, all_fields=True) -class ExampleGQL: - ... - -@strawberry.type -class Query: - @strawberry.field() - def test(self, example: ExampleGQL) -> None: - # friends may be an empty list here - print(example.friends) - # calling to_pydantic() runs the validation and raises - # an error if friends is empty - print(example.to_pydantic().friends) - -schema = strawberry.Schema(query=Query) - ---- -input ExampleGQL { - friends: [String!]! -} - -type Query { - test(example: ExampleGQL!): Void -} -``` - ### Classes with `__get_validators__` Pydantic BaseModels may define a custom type with [`__get_validators__`](https://pydantic-docs.helpmanual.io/usage/types/#classes-with-__get_validators__) @@ -409,7 +358,7 @@ class UserType: class Query: @strawberry.field def test() -> UserType: - return UserType.from_pydantic(User(id=123, hash=b"abcd")) + return UserType.from_pydantic(User(id=123, hash=b'abcd')) schema = strawberry.Schema(query=Query) @@ -442,7 +391,6 @@ class ContentType(enum.Enum): NAME = "name" DESCRIPTION = "description" - class User(BaseModel): id: str content: Dict[ContentType, str] @@ -473,12 +421,11 @@ class UserType: content[enum_member.value] = data.pop(key) return User(content=content, **data) - user = User(id="abc", content={ContentType.NAME: "Bob"}) print(UserType.from_pydantic(user)) # UserType(id='abc', content_name='Bob', content_description=None) -user_type = UserType(id="abc", content_name="Bob", content_description=None) +user_type = UserType(id='abc', content_name='Bob', content_description=None) print(user_type.to_pydantic()) # id='abc' content={: 'Bob'} ``` diff --git a/docs/integrations/sanic.md b/docs/integrations/sanic.md index 9a79279ed7..5a88219e99 100644 --- a/docs/integrations/sanic.md +++ b/docs/integrations/sanic.md @@ -4,8 +4,8 @@ title: Sanic # Sanic -Strawberry comes with a basic [Sanic](https://github.com/sanic-org/sanic) -integration. It provides a view that you can use to serve your GraphQL schema: +Strawberry comes with a basic [Sanic](https://github.com/sanic-org/sanic) integration. It provides a view that you can +use to serve your GraphQL schema: ```python from strawberry.sanic.views import GraphQLView @@ -27,14 +27,10 @@ The `GraphQLView` accepts two options at the moment: - `schema`: mandatory, the schema created by `strawberry.Schema`. - `graphiql`: optional, defaults to `True`, whether to enable the GraphiQL interface. -- `allow_queries_via_get`: optional, defaults to `True`, whether to enable - queries via `GET` requests -- `def encode_json(self, data: GraphQLHTTPResponse) -> str` ## Extending the view -The base `GraphQLView` class can be extended by overriding the following -methods: +The base `GraphQLView` class can be extended by overriding the following methods: - `async get_context(self) -> Any` - `get_root_value(self) -> Any` @@ -42,9 +38,9 @@ methods: ## get_context -By overriding `GraphQLView.get_context` you can provide a custom context object -for your resolvers. You can return anything here; by default GraphQLView returns -a dictionary with the request. +By overriding `GraphQLView.get_context` you can provide a custom context object for +your resolvers. You can return anything here; by default GraphQLView returns a +dictionary with the request. ```python class MyGraphQLView(GraphQLView): @@ -62,14 +58,12 @@ class Query: Here we are returning a custom context dictionary that contains only one item called `"example"`. -Then we can use the context in a resolver. In this case the resolver will return -`1`. +Then we can use the context in a resolver. In this case the resolver will return `1`. ## get_root_value -By overriding `GraphQLView.get_root_value` you can provide a custom root value -for your schema. This is probably not used a lot but it might be useful in -certain situations. +By overriding `GraphQLView.get_root_value` you can provide a custom root value for your +schema. This is probably not used a lot but it might be useful in certain situations. Here's an example: @@ -84,46 +78,34 @@ class Query: name: str ``` -Here we configure a Query where requesting the `name` field will return -`"Patrick"` through the custom root value. +Here we configure a Query where requesting the `name` field will return `"Patrick"` +through the custom root value. ## process_result -By overriding `GraphQLView.process_result` you can customize and/or process -results before they are sent to a client. This can be useful for logging errors, -or even hiding them (for example to hide internal exceptions). +By overriding `GraphQLView.process_result` you can customize and/or process results +before they are sent to a client. This can be useful for logging errors, or even hiding +them (for example to hide internal exceptions). -It needs to return an object of `GraphQLHTTPResponse` and accepts the execution -result. +It needs to return an object of `GraphQLHTTPResponse` and accepts the execution result. ```python -from strawberry.sanic.views import GraphQLView -from strawberry.http import GraphQLHTTPResponse, process_result +from strawberry.http import GraphQLHTTPResponse from strawberry.types import ExecutionResult -from sanic.request import Request -from graphql.error.graphql_error import format_error as format_graphql_error +from graphql.error import format_error as format_graphql_error class MyGraphQLView(GraphQLView): - async def process_result( - self, request: Request, result: ExecutionResult + def process_result( + self, result: ExecutionResult ) -> GraphQLHTTPResponse: + data: GraphQLHTTPResponse = {"data": result.data} + if result.errors: - result.errors = [format_graphql_error(err) for err in result.errors] + data["errors"] = [format_graphql_error(err) for err in result.errors] - return process_result(data) + return data ``` In this case we are doing the default processing of the result, but it can be tweaked based on your needs. - -## encode_json - -`encode_json` allows to customize the encoding of the JSON response. By default -we use `json.dumps` but you can override this method to use a different encoder. - -```python -class MyGraphQLView(GraphQLView): - def encode_json(self, data: GraphQLHTTPResponse) -> str: - return json.dumps(data, indent=2) -``` diff --git a/docs/operations/testing.md b/docs/operations/testing.md index 0e53ff7f5c..8619b2a566 100644 --- a/docs/operations/testing.md +++ b/docs/operations/testing.md @@ -106,7 +106,6 @@ import asyncio import pytest import strawberry - @strawberry.type class Subscription: @strawberry.subscription @@ -115,17 +114,14 @@ class Subscription: yield i await asyncio.sleep(0.5) - @strawberry.type class Query: @strawberry.field def hello() -> str: return "world" - schema = strawberry.Schema(query=Query, subscription=Subscription) - @pytest.mark.asyncio async def test_subscription(): query = """ diff --git a/docs/operations/tracing.md b/docs/operations/tracing.md index ac65592e61..cee8e25493 100644 --- a/docs/operations/tracing.md +++ b/docs/operations/tracing.md @@ -24,29 +24,9 @@ from strawberry.extensions.tracing import ApolloTracingExtensionSync schema = strawberry.Schema(query=Query, extensions=[ApolloTracingExtensionSync]) ``` -## Datadog - -In addition to Apollo Tracing we also support tracing with -[Datadog](https://www.datadoghq.com/). using the DatadogTracingExtension. - -```python -from strawberry.extensions.tracing import DatadogTracingExtension - -schema = strawberry.Schema(query=Query, extensions=[DatadogTracingExtension]) -``` - -Note that if you're not running under ASGI you'd need to use the sync version of -DatadogTracingExtension: - -```python -from strawberry.extensions.tracing import DatadogTracingExtensionSync - -schema = strawberry.Schema(query=Query, extensions=[DatadogTracingExtensionSync]) -``` - ## Open Telemetry -In addition to Datadog and Apollo Tracing we also support +In addition to Apollo Tracing we also support [opentelemetry](https://opentelemetry.io/), using the OpenTelemetryExtension. You also need to install the extras for opentelemetry by doing: @@ -70,22 +50,16 @@ from strawberry.extensions.tracing import OpenTelemetryExtensionSync schema = strawberry.Schema(query=Query, extensions=[OpenTelemetryExtensionSync]) ``` -Example Elasticsearch, Kibana, APM, Collector docker-compose to track django and -strawberry tracing metrics +Example Elasticsearch, Kibana, APM, Collector docker-compose to track django and strawberry tracing metrics This will spin up: - an elastic search instance to keep your data - kibana to visualize data - the elastic APM Server for processing incoming traces -- a - [collector binding](https://github.com/open-telemetry/opentelemetry-python/tree/main/exporter/opentelemetry-exporter-otlp) - to transform the opentelemetry data (more exactly the Opentelementry Line - Protocol OTLP) to something AMP can read - ([our APM agent](https://github.com/open-telemetry/opentelemetry-collector)) +- a [collector binding](https://github.com/open-telemetry/opentelemetry-python/tree/main/exporter/opentelemetry-exporter-otlp) to transform the opentelemetry data (more exactly the Opentelementry Line Protocol OTLP) to something AMP can read ([our APM agent](https://github.com/open-telemetry/opentelemetry-collector)) -For more details see the elasticsearch -[docs](https://www.elastic.co/guide/en/apm/get-started/current/open-telemetry-elastic.html) +For more details see the elasticsearch [docs](https://www.elastic.co/guide/en/apm/get-started/current/open-telemetry-elastic.html) ```yaml version: "3" @@ -110,8 +84,7 @@ services: healthcheck: interval: 10s retries: 12 - test: curl -s http://localhost:9200/_cluster/health | grep -vq - '"status":"red"' + test: curl -s http://localhost:9200/_cluster/health | grep -vq '"status":"red"' kibana: image: docker.elastic.co/kibana/kibana:7.16.2 @@ -164,9 +137,7 @@ services: healthcheck: interval: 10s retries: 12 - test: - curl --write-out 'HTTP %{http_code}' --fail --silent --output /dev/null - http://localhost:8200/ + test: curl --write-out 'HTTP %{http_code}' --fail --silent --output /dev/null http://localhost:8200/ otel-collector: image: otel/opentelemetry-collector:0.41.0 @@ -255,7 +226,6 @@ trace.get_tracer_provider().add_span_processor(span_processor) ... - def main(): DjangoInstrumentor().instrument() ... diff --git a/docs/types/enums.md b/docs/types/enums.md index 63957d7335..1219394f54 100644 --- a/docs/types/enums.md +++ b/docs/types/enums.md @@ -4,7 +4,7 @@ title: Enums # Enums -Enums are a special kind of type that is restricted to a particular set of values. +Enums are a special kind of type that is restrict to a particular set of values. For example, we have a few options of ice cream available, and we want to allow user to choose only from those options. @@ -17,9 +17,7 @@ First, create a new class for the new type, which extends class Enum: ```python from enum import Enum - class IceCreamFlavour(Enum): - ... ``` Then, list options as variables in that class: @@ -89,7 +87,6 @@ class Cone: flavour: IceCreamFlavour num_scoops: int - @strawberry.type class Query: @strawberry.field @@ -129,18 +126,6 @@ type.
-You can also deprecate enum value. To do so you need more verbose syntax using -`strawberry.enum_value` and `deprecation_reason`. You can mix and match string -and verbose syntax. - -```python -@strawberry.enum -class IceCreamFlavour(Enum): - VANILLA = strawberry.enum_value("vanilla") - STRAWBERRY = strawberry.enum_value("strawberry", deprecation_reason="We ran out") - CHOCOLATE = "chocolate" -``` - str: return "I'm a resolver" - # Throws 'Field "c" on type "Query" cannot define a default_factory and a resolver.' + ``` ### FieldWithResolverAndDefaultValueError -This exception is raised when `strawberry.field` is used with both `resolver` -and `default` arguments. +This exception is raised when in `strawberry.field` is used with both `resolver` and +`default` arguments. ```python def test_resolver() -> str: return "I'm a resolver" - @strawberry.type class Query: c: str = strawberry.field(default="Example C", resolver=test_resolver) - # Throws 'Field "c" on type "Query" cannot define a default value and a resolver.' ``` +### InvalidFieldArgument + +This exception is raised when a `Union` or an `Interface` is used as an argument type. + +```python +@strawberry.type +class Noun: + text: str + +@strawberry.type +class Verb: + text: str + +Word = strawberry.union("Word", types=(Noun, Verb)) + +@strawberry.field +def add_word(word: Word) -> bool: + return True + +# Throws 'Argument "word" on field "add_word" cannot be of type "Union"' +``` + +### InvalidUnionType + +This exception is raised when an invalid type is used with `Union`. + +```python +Result = strawberry.union("Result", (int, )) + +# Throws 'Type `int` cannot be used in a GraphQL Union' +``` + +### MissingArgumentsAnnotationsError + +The `MissingArgumentsAnnotationsError` exception is raised when a resolver's arguments +are missing type annotations. + +```python +@strawberry.field +def hello(self, foo) -> str: + return "I'm a resolver" + +# Throws 'Missing annotation for argument "foo" in field "hello", did you forget to add it?' +``` + +### MissingFieldAnnotationError + +The `MissingFieldAnnotationError` exception is raised when a `strawberry.field` is not +type-annotated but also has no resolver to determine its type. + +```python +@strawberry.type +class Query: # noqa: F841 + foo = strawberry.field() + +# Throws 'Unable to determine the type of field "foo". Either annotate it directly, or provide a typed resolver using @strawberry.field.' +``` + +### MissingReturnAnnotationError + +The `MissingReturnAnnotationError` exception is raised when a resolver is missing the +type annotation for the return type. + +```python +@strawberry.type +class Query: + @strawberry.field + def goodbye(self): + return "I'm a resolver" + +# Throws 'Return annotation missing for field "goodbye", did you forget to add it?' +``` + ### MissingTypesForGenericError This exception is raised when a `Generic` type is added to the Strawberry Schema without @@ -68,10 +139,108 @@ def name( ) -> str: return "Name" - # Throws 'Annotation for argument `argument` on field `name` cannot have multiple `strawberry.argument`s' ``` +### ObjectIsNotAClassError + +This exception is raised when `strawberry.type`, `strawberry.input` or +`strawberry.interface` are used with an object that is not class. + +```python +@strawberry.type +def not_a_class(): + pass + +# Throws 'strawberry.type can only be used with class types. Provided object is not a type.' +``` + +### ObjectIsNotAnEnumError + +This exception is raised when `strawberry.enum` is used with an object that is not an +Enum. + +```python +@strawberry.enum +class NormalClass: + hello = "world" + +# Throws 'strawberry.exceptions.NotAnEnum: strawberry.enum can only be used with subclasses of Enum' +``` + +### PrivateStrawberryFieldError + +This exception is raised when a `strawberry.field` is type annotated with +`strawberry.Private` + +```python +@strawberry.type +class Query: + name: str + age: strawberry.Private[int] = strawberry.field(description="๐Ÿคซ") + + +# Throws 'Field age on type Query cannot be both private and a strawberry.field' +``` + +### ScalarAlreadyRegisteredError + +This exception is raised when two scalars are used with the same name or the same type. +Note that also `graphql` library will throw a `TypeError` exception with the same +message. + +```python +MyCustomScalar = strawberry.scalar( + str, + name="MyCustomScalar", +) + +MyCustomScalar2 = strawberry.scalar( + int, + name="MyCustomScalar", +) + +@strawberry.type +class Query: + scalar_1: MyCustomScalar + scalar_2: MyCustomScalar2 + +# Throws 'Scalar `MyCustomScalar` has already been registered' +# The traceback will look like: +.../venv/lib/python3.9/site-packages/graphql/type/definition.py:767: in fields + fields = resolve_thunk(self._fields) +.../venv/lib/python3.9/site-packages/graphql/type/definition.py:296: in resolve_thunk + return thunk() if callable(thunk) else thunk +.../venv/lib/python3.9/site-packages/strawberry/schema/schema_converter.py:294: in get_graphql_fields + graphql_fields[field_name] = self.from_field(field) +.../venv/lib/python3.9/site-packages/strawberry/schema/schema_converter.py:140: in from_field + field_type = self.from_non_optional(field.type) +.../venv/lib/python3.9/site-packages/strawberry/schema/schema_converter.py:276: in from_non_optional + of_type = self.from_type(type_) +.../venv/lib/python3.9/site-packages/strawberry/schema/schema_converter.py:456: in from_type + return self.from_scalar(type_) +.../venv/lib/python3.9/site-packages/strawberry/schema/schema_converter.py:429: in from_scalar + raise ScalarAlreadyRegisteredError(scalar_definition.name) +E strawberry.exceptions.ScalarAlreadyRegisteredError: Scalar `MyCustomScalar` has already been registered + +During handling of the above exception, another exception occurred: +test_schema.py:4: in + from schema import schema +schema.py:79: in + schema = strawberry.Schema( +.../venv/lib/python3.9/site-packages/strawberry/schema/schema.py:84: in __init__ + self._schema = GraphQLSchema( +.../venv/lib/python3.9/site-packages/graphql/type/schema.py:208: in __init__ + collect_referenced_types(query) +.../venv/lib/python3.9/site-packages/graphql/type/schema.py:422: in collect_referenced_types + for field in named_type.fields.values(): +....9envet__ + val = self.func(instance) +.../venv/lib/python3.9/site-packages/graphql/type/definition.py:769: in fields + raise TypeError(f"{self.name} fields cannot be resolved. {error}") +E TypeError: Query fields cannot be resolved. Scalar `MyCustomScalar` has already been registered +``` + ### UnsupportedTypeError This exception is thrown when the type-annotation used is not supported by @@ -82,22 +251,21 @@ only class Model(pydantic.BaseModel): field: pydantic.Json - @strawberry.experimental.pydantic.type(Model, fields=["field"]) class Type: pass + ``` ### WrongNumberOfResultsReturned -This exception is raised when the DataLoader returns a different number of -results than requested. +This exception is raised when the DataLoader returns a different number of results than +requested. ```python async def idx(keys): return [1, 2] - loader = DataLoader(load_fn=idx) await loader.load(1) @@ -107,8 +275,7 @@ await loader.load(1) ## Runtime exceptions -Some errors are also thrown when trying to exectuing queries (mutations or -subscriptions). +Some errors are also thrown when trying to exectuing queries (mutations or subscriptions). ### MissingQueryError @@ -122,32 +289,28 @@ client.post("/graphql", data={}) ## UnallowedReturnTypeForUnion -This error is raised when the return type of a `Union` is not in the list of -Union types. +This error is raised when the return type of a `Union` is not in the list of Union +types. ```python @strawberry.type class Outside: c: int - @strawberry.type class A: a: int - @strawberry.type class B: b: int - @strawberry.type class Mutation: @strawberry.mutation def hello(self) -> Union[A, B]: return Outside(c=5) - query = """ mutation { hello { @@ -181,24 +344,21 @@ result = schema.execute_sync(query) ## WrongReturnTypeForUnion -This exception is thrown when the Union type cannot be resolved because it's not -a `strawberry.field`. +This exception is thrown when the Union type cannot be resolved because it's not a +`strawberry.field`. ```python @strawberry.type class A: a: int - @strawberry.type class B: b: int - @strawberry.type class Query: - ab: Union[A, B] = "ciao" # missing `strawberry.field` ! - + ab: Union[A, B] = "ciao" // missing `strawberry.field` ! query = """{ ab { diff --git a/docs/types/generics.md b/docs/types/generics.md index dcbd849ec8..9d83e59f55 100644 --- a/docs/types/generics.md +++ b/docs/types/generics.md @@ -4,174 +4,4 @@ title: Generics # Generics -Strawberry supports using Python's `Generic` typing to dynamically create -reusable types. - -Strawberry will automatically generate the correct GraphQL schema from the -combination of the generic type and the type arguments. Generics are supported -in Object types, Input types, and Arguments to queries, mutations, and scalars. - -Let's take a look at an example: - -# Object Types - -```python -from typing import Generic, List, TypeVar - -import strawberry - -T = TypeVar("T") - - -@strawberry.type -class Page(Generic[T]): - number: int - items: List[T] -``` - -This example defines a generic type `Page` that can be used to represent a page -of any type. For example, we can create a page of `User` objects: - -```python+schema -import strawberry - -@strawberry.type -class User: - name: str - -@strawberry.type -class Query: - users: Page[User] ---- -type Query { - users: UserPage! -} - -type User { - name: String! -} - -type UserPage { - number: Int! - items: [User!]! -} -``` - -# Input and Argument Types - -Arguments to queries and mutations can also be made generic by creating Generic -Input types. Here we'll define an input type that can serve as a collection of -anything, then create a specialization by using as a filled-in argument on a -mutation. - -```python+schema -import strawberry -from typing import Generic, List, Optional, TypeVar - -T = TypeVar("T") - -@strawberry.input -class CollectionInput(Generic[T]): - values: List[T] - -@strawberry.input -class PostInput: - name: str - -@strawberry.type -class Post: - id: int - name: str - -@strawberry.type -class Mutation: - @strawberry.mutation - def add_posts(self, posts: CollectionInput[PostInput]) -> bool: - return True - -@strawberry.type -class Query: - most_recent_post: Optional[Post] = None - -schema = strawberry.Schema(query=Query, mutation=Mutation) ---- -input PostInputCollectionInput { - values: [PostInput!]! -} - -input PostInput { - name: String! -} - -type Post { - id: Int! - name: String! -} - -type Query { - mostRecentPost: Post -} - -type Mutation { - addPosts(posts: PostInputCollectionInput!): Boolean! -} -``` - -> **Note**: Pay attention to the fact that both `CollectionInput` and -> `PostInput` are Input types. Providing `posts: CollectionInput[Post]` to -> `add_posts` (i.e. using the non-input `Post` type) would have resulted in an -> error: -> -> ``` -> PostCollectionInput fields cannot be resolved. Input field type must be a -> GraphQL input type -> ``` - -# Multiple Specializations - -Using multiple specializations of a Generic type will work as expected. Here we -define a `Point2D` type and then specialize it for both `int`s and `float`s. - -```python+schema -from typing import Generic, TypeVar - -import strawberry - -T = TypeVar('T') - -@strawberry.input -class Point2D(Generic[T]): - x: T - y: T - -@strawberry.type -class Mutation: - @strawberry.mutation - def store_line_float(self, a: Point2D[float], b: Point2D[float]) -> bool: - return True - - @strawberry.mutation - def store_line_int(self, a: Point2D[int], b: Point2D[int]) -> bool: - return True ---- -type Mutation { - storeLineFloat(a: FloatPoint2D!, b: FloatPoint2D!): Boolean! - storeLineInt(a: IntPoint2D!, b: IntPoint2D!): Boolean! -} - -input FloatPoint2D { - x: Float! - y: Float! -} - -input IntPoint2D { - x: Int! - y: Int! -} -``` - -# Variadic Generics - -Variadic Generics, introduced in [PEP-646][pep-646], are currently unsupported. - -[pep-646]: https://peps.python.org/pep-0646/ +Documentation coming soon diff --git a/docs/types/input-types.md b/docs/types/input-types.md index 930238a087..fb6c63b21c 100644 --- a/docs/types/input-types.md +++ b/docs/types/input-types.md @@ -4,23 +4,15 @@ title: Input types # Input types -In addition to [object types](./object-types) GraphQL also supports input types. -While being similar to object types, they are better suited for input data as -they limit the kind of types you can use for fields. +In addition to [object types](./object-types) GraphQL also supports input types. While being similar to object types, they are better suited for input data as they limit the kind of types you can use for fields. -This is how the -[GraphQL spec defines the difference between object types and input types](https://spec.graphql.org/June2018/#sec-Input-Objects): +This is how the [GraphQL spec defines the difference between object types and input types](https://spec.graphql.org/June2018/#sec-Input-Objects): -> The GraphQL Object type (ObjectTypeDefinition)... is inappropriate for reโ€use -> (as input), because Object types can contain fields that define arguments or -> contain references to interfaces and unions, neither of which is appropriate -> for use as an input argument. For this reason, input objects have a separate -> type in the system. +> The GraphQL Object type (ObjectTypeDefinition)... is inappropriate for reโ€use (as input), because Object types can contain fields that define arguments or contain references to interfaces and unions, neither of which is appropriate for use as an input argument. For this reason, input objects have a separate type in the system. ## Defining input types -In Strawberry, you can define input types by using the `@strawberry.input` -decorator, like this: +In Strawberry, you can define input types by using the `@strawberry.input` decorator, like this: ```python+schema import strawberry @@ -43,7 +35,6 @@ Then you can use input types as argument for your fields or mutations: ```python import strawberry - @strawberry.type class Mutation: @strawberry.mutation @@ -51,54 +42,11 @@ class Mutation: return True ``` -If you want to include optional arguments, you need to provide them with a -default. For example if we want to expand on the above example to allow optional -labeling of our point we could do: - -```python+schema -import strawberry -from typing import Optional - -@strawberry.input -class Point2D: - x: float - y: float - label: Optional[str] = None ---- -type Point2D { - x: Float! - y: Float! - label: String = null -} -``` - -Alternatively you can also use `strawberry.UNSET` instead of the `None` default -value, which will make the field optional in the schema: - -```python+schema -import strawberry -from typing import Optional - -@strawberry.input -class Point2D: - x: float - y: float - label: Optional[str] = strawberry.UNSET ---- -type Point2D { - x: Float! - y: Float! - label: String -} -``` - ## API `@strawberry.input(name: str = None, description: str = None)` Creates an input type from a class definition. -- `name`: if set this will be the GraphQL name, otherwise the GraphQL will be - generated by camel-casing the name of the class. -- `description`: this is the GraphQL description that will be returned when - introspecting the schema or when navigating the schema using GraphiQL. +- `name`: if set this will be the GraphQL name, otherwise the GraphQL will be generated by camel-casing the name of the class. +- `description`: this is the GraphQL description that will be returned when introspecting the schema or when navigating the schema using GraphiQL. diff --git a/docs/types/interfaces.md b/docs/types/interfaces.md index 07c2e9532d..82a21704ad 100644 --- a/docs/types/interfaces.md +++ b/docs/types/interfaces.md @@ -103,24 +103,20 @@ from the interface: ```python import strawberry - @strawberry.type class Individual(Customer): # additional fields - ... - @strawberry.type class Company(Customer): # additional fields - ... ``` -If you add an object type which implements an interface, but that object type -doesnโ€™t appear in your schema as a field return type or a union member, then you -will need to add that object to the Schema definition directly. +If you add an object type which implements an interface, but that object +type doesnโ€™t appear in your schema as a field return type or a union member, +then you will need to add that object to the Schema definition directly. ```python schema = strawberry.Schema(query=Query, types=[Individual, Company]) @@ -171,7 +167,6 @@ Interfaces can provide field implementations as well. For example: ```python import strawberry - @strawberry.interface class Customer: @strawberry.field @@ -186,7 +181,6 @@ field: ```python import strawberry - @strawberry.type class Company(Customer): @strawberry.field @@ -204,7 +198,6 @@ always return an instance of an object type from your resolver: ```python import strawberry - @strawberry.type class Query: @strawberry.field diff --git a/docs/types/lazy.md b/docs/types/lazy.md deleted file mode 100644 index 6f1a5b571a..0000000000 --- a/docs/types/lazy.md +++ /dev/null @@ -1,75 +0,0 @@ ---- -title: Lazy Types ---- - -# Lazy Types - -Strawberry supports lazy types, which are useful when you have circular -dependencies between types. - -For example, let's say we have a `User` type that has a list of `Post` types, -and each `Post` type has a `User` field. In this case, we can't define the -`User` type before the `Post` type, and vice versa. - -To solve this, we can use lazy types: - -```python -# posts.py -from typing import TYPE_CHECKING, Annotated - -import strawberry - -if TYPE_CHECKING: - from .users import User - - -@strawberry.type -class Post: - title: str - author: Annotated["User", strawberry.lazy(".users")] -``` - -```python -# users.py -from typing import TYPE_CHECKING, Annotated, List - -import strawberry - -if TYPE_CHECKING: - from .posts import Post - - -@strawberry.type -class User: - name: str - posts: List[Annotated["Post", strawberry.lazy(".posts")]] -``` - -`strawberry.lazy` in combination with `Annotated` allows us to define the path -of the module of the type we want to use, this allows us to leverage Python's -type hints, while preventing circular imports and preserving type safety by -using `TYPE_CHECKING` to tell type checkers where to look for the type. - - - -`Annotated` is only available in Python 3.9+, if you are using an older version -of Python you can use `typing_extensions.Annotated` instead. - -```python -# users.py -from typing import TYPE_CHECKING, List -from typing_extensions import Annotated - -import strawberry - -if TYPE_CHECKING: - from .posts import Post - - -@strawberry.type -class User: - name: str - posts: List[Annotated["Post", strawberry.lazy(".posts")]] -``` - - diff --git a/docs/types/private.md b/docs/types/private.md deleted file mode 100644 index 5d6ea806de..0000000000 --- a/docs/types/private.md +++ /dev/null @@ -1,83 +0,0 @@ ---- -title: Private Fields ---- - -# Private Fields - -Private (external) fields can provide local context for later resolution. -These fields will act as plain fields so will not be exposed in the GraphQL -API. - -Some uses include: - -- Context that relies upon field inputs. -- Avoiding fully materializing an object hierarchy (lazy resolution) - -# Defining a private field - -Specifying a field with `strawberry.Private[...]` will desigate it as -internal and not for GraphQL. - -# Example - -Consider the following type, which can accept any Python object and handle -converting it to string, representation, or templated output: - -``` - -@strawberry.type -class Stringable: - value: strawberry.Private[object] - - @strawberry.field - def string(self) -> str: - return str(self.value) - - @strawberry.field - def repr(self) -> str: - return repr(self.value) - - @strawberry.field - def format(self, template: str) -> str: - return template.format(my=self.value) - -``` - -The `Private[...]` type lets Strawberry know that this field is not -a GraphQL field. "value" is a regular field on the class, but it is not -exposed on the GraphQL API. - -``` - -@strawberry.type -class Query: - @strawberry.field - def now(self) -> Stringable: - return Stringable(value=datetime.datetime.now()) - -``` - -Queries can then select the fields and formats desired, but formatting only -happens as requested: - -```graphql+json -{ - now { - format(template: "{my.year}") - string - repr - } -} - ---- - -{ - "data": { - "now": { - "format": "2022", - "string": "2022-09-03 17:03:04.923068", - "repr": "datetime.datetime(2022, 9, 3, 17, 3, 4, 923068)" - } - } -} -``` diff --git a/docs/types/resolvers.md b/docs/types/resolvers.md index 4ea04a2b40..c4ebbd4952 100644 --- a/docs/types/resolvers.md +++ b/docs/types/resolvers.md @@ -41,7 +41,6 @@ resolvers; the first is to pass a function to the field definition, like this: def get_last_user() -> User: return User(name="Marco") - @strawberry.type class Query: last_user: User = strawberry.field(resolver=get_last_user) @@ -72,6 +71,7 @@ The other way to define a resolver is to use `strawberry.field` as a decorator, like here: ```python + @strawberry.type class Query: @strawberry.field @@ -84,11 +84,12 @@ very small resolvers. -The _self_ argument is a bit special here, when executing a GraphQL query, in -case of resolvers defined with a decorator, the _self_ argument corresponds to -the _root_ value that field. In this example the _root_ value is the value -`Query` type, which is usually `None`. You can change the _root_ value when -calling the `execute` method on a `Schema`. More on _root_ values below. +The _self_ argument is a bit special here, when executing a GraphQL +query, in case of resolvers defined with a decorator, the _self_ argument +corresponds to the _root_ value that field. In this example the _root_ value +is the value `Query` type, which is usually `None`. You can change the _root_ +value when calling the `execute` method on a `Schema`. More on _root_ values +below. @@ -123,56 +124,6 @@ type Query { } ``` -### Optional arguments - -Optional or nullable arguments can be expressed using `Optional`. If you need to -differentiate between `null` (maps to `None` in Python) and no arguments being -passed, you can use `UNSET`: - -```python+schema -from typing import Optional -import strawberry - -@strawberry.type -class Query: - @strawberry.field - def hello(self, name: Optional[str] = None) -> str: - if name is None: - return "Hello world!" - return f"Hello {name}!" - - @strawberry.field - def greet(self, name: Optional[str] = strawberry.UNSET) -> str: - if name is strawberry.UNSET: - return "Name was not set!" - if name is None: - return "Name was null!" - return f"Hello {name}!" ---- -type Query { - hello(name: String = null): String! - greet(name: String): String! -} -``` - -Like this you will get the following responses: - -```graphql+response -{ - unset: greet - null: greet(name: null) - name: greet(name: "Dominique") -} ---- -{ - "data": { - "unset": "Name was not set!", - "null": "Name was null!", - "name": "Hello Dominique!" - } -} -``` - ## Accessing field's parent's data It is quite common to want to be able to access the data from the field's parent @@ -210,11 +161,9 @@ the value of the parent: ```python import strawberry - def full_name(root: User) -> str: return f"{root.first_name} {root.last_name}" - @strawberry.type class User: first_name: str @@ -225,19 +174,15 @@ class User: ## Accessing execution information Sometimes it is useful to access the information for the current execution -context. Strawberry allows to declare a parameter of type `Info` that will be -automatically passed to the resolver. This parameter containes the information -for the current execution context. +context. To do so you can provide the `info` parameter to resolvers, like this: ```python import strawberry from strawberry.types import Info - def full_name(root: User, info: Info) -> str: return f"{root.first_name} {root.last_name} {info.field_name}" - @strawberry.type class User: first_name: str @@ -245,13 +190,6 @@ class User: full_name: str = strawberry.field(resolver=full_name) ``` - - -You don't have to call this parameter `info`, its name can be anything. -Strawberry uses the type to pass the correct value to the resolver. - - - ### API Info objects contain information for the current execution context: diff --git a/docs/types/scalars.md b/docs/types/scalars.md index 535c8bbda7..b2d9c4cbf6 100644 --- a/docs/types/scalars.md +++ b/docs/types/scalars.md @@ -78,7 +78,6 @@ Scalar types can also be used as inputs: import datetime import strawberry - @strawberry.type class Query: @strawberry.field @@ -106,34 +105,22 @@ import strawberry Base64 = strawberry.scalar( NewType("Base64", bytes), serialize=lambda v: base64.b64encode(v).decode("utf-8"), - parse_value=lambda v: base64.b64decode(v).encode("utf-8"), + parse_value=lambda v: base64.b64decode(v.encode("utf-8")), ) - @strawberry.type class Query: @strawberry.field def base64(self) -> Base64: return Base64(b"hi") - schema = strawberry.Schema(Query) result = schema.execute_sync("{ base64 }") -assert results.data == {"base64": "aGk="} +assert results.data == {"base64": "aGk="} ``` - - -The `Base16`, `Base32` and `Base64` scalar types are available in `strawberry.scalars` - -```python -from strawberry.scalars import Base16, Base32, Base64 -``` - - - ## Example JSONScalar ```python @@ -142,12 +129,13 @@ from typing import Any, NewType import strawberry -JSON = strawberry.scalar( - NewType("JSON", object), - description="The `JSON` scalar type represents JSON values as specified by ECMA-404", +JSONScalar = strawberry.scalar( + NewType("JSONScalar", Any), serialize=lambda v: v, - parse_value=lambda v: v, + parse_value=lambda v: json.loads(v), + description="The GenericScalar scalar type represents a generic GraphQL scalar value that could be: List or Object." ) + ``` Usage: @@ -156,8 +144,9 @@ Usage: @strawberry.type class Query: @strawberry.field - def data(self, info) -> JSON: + def data(self, info) -> JSONScalar: return {"hello": {"a": 1}, "someNumbers": [1, 2, 3]} + ``` ```graphql+response @@ -175,16 +164,6 @@ query ExampleDataQuery { } ``` - - -The `JSON` scalar type is available in `strawberry.scalars` - -```python -from strawberry.scalars import JSON -``` - - - ## Overriding built in scalars To override the behaviour of the built in scalars you can pass a map of @@ -204,19 +183,17 @@ EpochDateTime = strawberry.scalar( parse_value=lambda value: datetime.fromtimestamp(int(value), timezone.utc), ) - @strawberry.type class Query: @strawberry.field def current_time(self) -> datetime: return datetime.now() - schema = strawberry.Schema( - Query, - scalar_overrides={ - datetime: EpochDateTime, - }, + Query, + scalar_overrides={ + datetime: EpochDateTime, + } ) result = schema.execute_sync("{ currentTime }") assert result.data == {"currentTime": 1628683200} diff --git a/docs/types/schema-configurations.md b/docs/types/schema-configurations.md index 2e8832bda8..45cf9e1a6b 100644 --- a/docs/types/schema-configurations.md +++ b/docs/types/schema-configurations.md @@ -21,7 +21,9 @@ class Query: example_field: str -schema = strawberry.Schema(query=Query, config=StrawberryConfig(auto_camel_case=False)) +schema = strawberry.Schema( + query=Query, config=StrawberryConfig(auto_camel_case=False) +) ``` In this case we are disabling the auto camel casing feature, so your output schema diff --git a/docs/types/schema-directives.md b/docs/types/schema-directives.md index d8ec5c971f..3d9f8671a9 100644 --- a/docs/types/schema-directives.md +++ b/docs/types/schema-directives.md @@ -24,7 +24,6 @@ Here's how we can use it in our schema: import strawberry from strawberry.schema_directive import Location - @strawberry.schema_directive(locations=[Location.OBJECT]) class Keys: fields: str @@ -32,8 +31,7 @@ class Keys: from .directives import Keys - -@strawberry.type(directives=[Keys(fields="id")]) +@strawberry.type(directives=Keys(fields="id")) class User: id: strawberry.ID name: str @@ -48,16 +46,6 @@ type User @keys(fields: "id") { } ``` -## Overriding field names - -You can use `strawberry.directive_field` to override the name of a field: - -```python -@strawberry.schema_directive(locations=[Location.OBJECT]) -class Keys: - fields: str = strawberry.directive_field(name="as") -``` - ## Locations Schema directives can be applied to many different parts of a schema. Here's the diff --git a/docs/types/schema.md b/docs/types/schema.md index a9e6f23de7..bbbb99230f 100644 --- a/docs/types/schema.md +++ b/docs/types/schema.md @@ -16,22 +16,19 @@ This is an example of a schema defined using Strawberry: ```python import strawberry - @strawberry.type class Query: @strawberry.field def hello(self) -> str: return "Hello World" - schema = strawberry.Schema(Query) ``` ## API reference ```python -class Schema(Query, mutation=None, subscription=None, **kwargs): - ... +class Schema(Query, mutation=None, subscription=None, **kwargs) ``` @@ -56,13 +53,13 @@ The root subscription type. Usually called `Subscription`. #### `config: Optional[StrawberryConfig] = None` -Pass a `StrawberryConfig` object to configure how the schema is generated. -[Read more](/docs/types/schema-configurations). +Pass a `StrawberryConfig` object to configure how the schema is generated. [Read +more](/docs/types/schema-configurations). #### `types: List[Type] = []` -List of extra types to register with the Schema that are not directly linked to -from the root Query. +List of extra types to register with the Schema that are not directly linked +to from the root Query.
Defining extra `types` when using Interfaces @@ -71,35 +68,28 @@ from the root Query. from datetime import date import strawberry - @strawberry.interface class Customer: name: str - @strawberry.type class Individual(Customer): date_of_birth: date - @strawberry.type class Company(Customer): founded: date - @strawberry.type class Query: @strawberry.field - def get_customer( - self, id: strawberry.ID - ): # -> Customer note we're returning the interface here + def get_customer(self, id: strawberry.ID) -> Customer # note we're returning the interface here if id == "mark": return Individual(name="Mark", date_of_birth=date(1984, 5, 14)) if id == "facebook": return Company(name="Facebook", founded=date(2004, 2, 1)) - schema = strawberry.Schema(Query, types=[Individual, Company]) ``` @@ -111,8 +101,7 @@ List of [extensions](/docs/extensions) to add to your Schema. #### `scalar_overrides: Optional[Dict[object, ScalarWrapper]] = None` -Override the implementation of the built in scalars. -[More information](/docs/types/scalars#overriding-built-in-scalars). +Override the implementation of the built in scalars. [More information](/docs/types/scalars#overriding-built-in-scalars). --- @@ -123,8 +112,7 @@ Override the implementation of the built in scalars. Executes a GraphQL operation against a schema (async) ```python -async def execute(query, variable_values, context_value, root_value, operation_name): - ... +async def execute(query, variable_values, context_value, root_value, operation_name) ``` #### `query: str` @@ -145,17 +133,14 @@ The value for the root value that will passed to root resolvers. #### `operation_name: Optional[str] = None` -The name of the operation you want to execute, useful when sending a document -with multiple operations. If no `operation_name` is specified the first -operation in the document will be executed. +The name of the operation you want to execute, useful when sending a document with multiple operations. If no `operation_name` is specified the first operation in the document will be executed. ### `.execute_sync()` Executes a GraphQL operation against a schema ```python -def execute_sync(query, variable_values, context_value, root_value, operation_name): - ... +def execute_sync(query, variable_values, context_value, root_value, operation_name)` ``` #### `query: str` @@ -176,62 +161,30 @@ The value for the root value that will passed to root resolvers. #### `operation_name: Optional[str] = None` -The name of the operation you want to execute, useful when sending a document -with multiple operations. If no `operation_name` is specified the first -operation in the document will be executed. +The name of the operation you want to execute, useful when sending a document with multiple operations. If no `operation_name` is specified the first operation in the document will be executed. --- ## Handling execution errors -By default Strawberry will log any errors encountered during a query execution -to a `strawberry.execution` logger. This behaviour can be changed by overriding -the `process_errors` function on the `strawberry.Schema` class. +By default Strawberry will log any errors encountered during a query execution to a `strawberry.execution` logger. This behaviour can be changed by overriding the `process_errors` function on the `strawberry.Schema` class. The default functionality looks like this: ```python -# strawberry/schema/base.py +# strawberry/schema/schema.py from strawberry.types import ExecutionContext logger = logging.getLogger("strawberry.execution") - -class BaseSchema: +class Schema: ... - def process_errors( - self, - errors: List[GraphQLError], - execution_context: Optional[ExecutionContext] = None, - ) -> None: - StrawberryLogger.error(error, execution_context) -``` - -```python -# strawberry/utils/logging.py -from strawberry.types import ExecutionContext - - -class StrawberryLogger: - logger: Final[logging.Logger] = logging.getLogger("strawberry.execution") - - @classmethod - def error( - cls, - error: GraphQLError, - execution_context: Optional[ExecutionContext] = None, - # https://www.python.org/dev/peps/pep-0484/#arbitrary-argument-lists-and-default-argument-values - **logger_kwargs: Any, - ) -> None: - # "stack_info" is a boolean; check for None explicitly - if logger_kwargs.get("stack_info") is None: - logger_kwargs["stack_info"] = True - - # stacklevel was added in version 3.8 - # https://docs.python.org/3/library/logging.html#logging.Logger.debug - if sys.version_info >= (3, 8): - logger_kwargs["stacklevel"] = 3 - - cls.logger.error(error, exc_info=error.original_error, **logger_kwargs) + def process_errors(self, errors: List[GraphQLError], execution_context: ExecutionContext) -> None: + for error in errors: + # A GraphQLError wraps the underlying error so we have to access it + # through the `original_error` property + # https://graphql-core-3.readthedocs.io/en/latest/modules/error.html#graphql.error.GraphQLError + actual_error = error.original_error or error + logger.error(actual_error, exc_info=actual_error) ``` diff --git a/docs/types/union.md b/docs/types/union.md index f8ac44f192..900becff28 100644 --- a/docs/types/union.md +++ b/docs/types/union.md @@ -45,7 +45,7 @@ fields depending on which kind of object that member is. We can do that by using In Strawberry there are two ways to define a union: -You can use the `Union` type from the `typing` module which will +You can use the use the `Union` type from the `typing` module which will autogenerate the type name from the names of the union members: ```python+schema @@ -100,7 +100,7 @@ class Query: union MediaItem = Audio | Video | Image type Query { - latest_media: MediaItem! + latest_media: AudioVideoImage! } type Audio { @@ -127,7 +127,6 @@ to always return an instance of an object type from your resolver: from typing import Union import strawberry - @strawberry.type class Query: @strawberry.field diff --git a/federation-compatibility/Dockerfile b/federation-compatibility/Dockerfile deleted file mode 100644 index 2c22b87f4e..0000000000 --- a/federation-compatibility/Dockerfile +++ /dev/null @@ -1,18 +0,0 @@ -FROM python:3.10-slim -WORKDIR /web - -RUN apt update && apt install -y gcc python3-dev -RUN pip install poetry - -COPY strawberry ./strawberry -COPY pyproject.toml ./ -COPY poetry.lock ./ -COPY README.md ./ - -RUN poetry install - -COPY federation-compatibility/schema.py ./ - -EXPOSE 4001 - -CMD poetry run strawberry server -p 4001 -h 0.0.0.0 schema:schema diff --git a/federation-compatibility/docker-compose.yml b/federation-compatibility/docker-compose.yml deleted file mode 100644 index 9c820eadd1..0000000000 --- a/federation-compatibility/docker-compose.yml +++ /dev/null @@ -1,7 +0,0 @@ -services: - products: - build: - context: . - dockerfile: federation-compatibility/Dockerfile - ports: - - 4001:4001 diff --git a/federation-compatibility/schema.py b/federation-compatibility/schema.py deleted file mode 100644 index a193f28c75..0000000000 --- a/federation-compatibility/schema.py +++ /dev/null @@ -1,287 +0,0 @@ -from typing import List, Optional - -import strawberry - -# ------- data ------- - -dimension = { - "size": "small", - "weight": 1, - "unit": "kg", -} - -user = { - "email": "support@apollographql.com", - "name": "Jane Smith", - "total_products_created": 1337, - "years_of_employment": 10, -} - - -deprecated_product = { - "sku": "apollo-federation-v1", - "package": "@apollo/federation-v1", - "reason": "Migrate to Federation V2", - "created_by": user["email"], -} - -products_research = [ - { - "study": { - "case_number": "1234", - "description": "Federation Study", - }, - "outcome": None, - }, - { - "study": { - "case_number": "1235", - "description": "Studio Study", - }, - "outcome": None, - }, -] - - -products = [ - { - "id": "apollo-federation", - "sku": "federation", - "package": "@apollo/federation", - "variation": {"id": "OSS"}, - "dimensions": dimension, - "research": [products_research[0]], - "created_by": user["email"], - "notes": None, - }, - { - "id": "apollo-studio", - "sku": "studio", - "package": "", - "variation": {"id": "platform"}, - "dimensions": dimension, - "research": [products_research[1]], - "created_by": user["email"], - "notes": None, - }, -] - - -# ------- resolvers ------- - - -def get_product_by_id(id: strawberry.ID) -> Optional["Product"]: - data = next((product for product in products if product["id"] == id), None) - - if not data: - return None - - return Product.from_data(data) - - -def get_product_by_sku_and_package(sku: str, package: str) -> Optional["Product"]: - data = next( - ( - product - for product in products - if product["sku"] == sku and product["package"] == package - ), - None, - ) - - return Product.from_data(data) if data else None - - -def get_product_by_sku_and_variation(sku: str, variation: dict) -> Optional["Product"]: - data = next( - ( - product - for product in products - if product["sku"] == sku and product["variation"]["id"] == variation["id"] - ), - None, - ) - - return Product.from_data(data) if data else None - - -# ------- types ------- - - -@strawberry.federation.type(extend=True, keys=["email"]) -class User: - email: strawberry.ID = strawberry.federation.field(external=True) - name: Optional[str] = strawberry.federation.field(override="users") - total_products_created: Optional[int] = strawberry.federation.field(external=True) - years_of_employment: int = strawberry.federation.field(external=True) - - # TODO: the camel casing will be fixed in a future release of Strawberry - @strawberry.federation.field(requires=["totalProductsCreated", "yearsOfEmployment"]) - def average_products_created_per_year(self) -> Optional[int]: - if self.total_products_created is not None: - return round(self.total_products_created / self.years_of_employment) - - return None - - @classmethod - def resolve_reference(cls, **data) -> Optional["User"]: - if email := data.get("email"): - years_of_employment = data.get("yearsOfEmployment") - - return User( - email=email, - name="Jane Smith", - total_products_created=1337, - years_of_employment=years_of_employment, - ) - - return None - - -@strawberry.federation.type(shareable=True) -class ProductDimension: - size: Optional[str] - weight: Optional[float] - unit: Optional[str] = strawberry.federation.field(inaccessible=True) - - -@strawberry.type -class ProductVariation: - id: strawberry.ID - - -@strawberry.type -class CaseStudy: - case_number: strawberry.ID - description: Optional[str] - - -@strawberry.federation.type(keys=["study { caseNumber }"]) -class ProductResearch: - study: CaseStudy - outcome: Optional[str] - - @classmethod - def from_data(cls, data: dict) -> "ProductResearch": - return ProductResearch( - study=CaseStudy( - case_number=data["study"]["case_number"], - description=data["study"]["description"], - ), - outcome=data["outcome"], - ) - - @classmethod - def resolve_reference(cls, **data) -> Optional["ProductResearch"]: - study = data.get("study") - - if not study: - return None - - case_number = study["caseNumber"] - - research = next( - ( - product_research - for product_research in products_research - if product_research["study"]["case_number"] == case_number - ), - None, - ) - - return ProductResearch.from_data(research) if research else None - - -@strawberry.federation.type(keys=["sku package"]) -class DeprecatedProduct: - sku: str - package: str - reason: Optional[str] - created_by: Optional[User] - - @classmethod - def resolve_reference(cls, **data) -> Optional["DeprecatedProduct"]: - if deprecated_product["sku"] == data.get("sku") and deprecated_product[ - "package" - ] == data.get("package"): - return DeprecatedProduct( - sku=deprecated_product["sku"], - package=deprecated_product["package"], - reason=deprecated_product["reason"], - created_by=User.resolve_reference( - email=deprecated_product["created_by"] - ), - ) - - return None - - -@strawberry.federation.type(keys=["id", "sku package", "sku variation { id }"]) -class Product: - id: strawberry.ID - sku: Optional[str] - package: Optional[str] - variation_id: strawberry.Private[str] - - @strawberry.field - def variation(self) -> Optional[ProductVariation]: - return ( - ProductVariation(strawberry.ID(self.variation_id)) - if self.variation_id - else None - ) - - @strawberry.field - def dimensions(self) -> Optional[ProductDimension]: - return ProductDimension(**dimension) - - @strawberry.federation.field(provides=["totalProductsCreated"]) - def created_by(self) -> Optional[User]: - return User(**user) - - notes: Optional[str] = strawberry.federation.field(tags=["internal"]) - research: List[ProductResearch] - - @classmethod - def from_data(cls, data: dict): - research = [ - ProductResearch.from_data(research) for research in data.get("research", []) - ] - - return cls( - id=data["id"], - sku=data["sku"], - package=data["package"], - variation_id=data["variation"], - notes="hello", - research=research, - ) - - @classmethod - def resolve_reference(cls, **data) -> Optional["Product"]: - if "id" in data: - return get_product_by_id(id=data["id"]) - - if "sku" in data: - if "variation" in data: - return get_product_by_sku_and_variation( - sku=data["sku"], variation=data["variation"] - ) - elif "package" in data: - return get_product_by_sku_and_package( - sku=data["sku"], package=data["package"] - ) - - return None - - -@strawberry.federation.type(extend=True) -class Query: - product: Optional[Product] = strawberry.field(resolver=get_product_by_id) - - @strawberry.field(deprecation_reason="Use product query instead") - def deprecated_product(self, sku: str, package: str) -> Optional[DeprecatedProduct]: - return None - - -schema = strawberry.federation.Schema(query=Query, enable_federation_2=True) diff --git a/mypy.ini b/mypy.ini index 9926a69f47..4ac4e3b02b 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1,48 +1,8 @@ [mypy] -plugins = pydantic.mypy, strawberry.ext.mypy_plugin +plugins = ./strawberry/ext/mypy_plugin.py implicit_reexport = False -warn_unused_configs = True -warn_unused_ignores = True -check_untyped_defs = True -ignore_errors = False -strict_optional = True -show_error_codes = True -warn_redundant_casts = True -# TODO: enable strict at some point -;strict = True - ; Disabled because of this bug: https://github.com/python/mypy/issues/9689 ; disallow_untyped_decorators = True -[mypy-graphql.*] -ignore_errors = True - -[mypy-pydantic.*] -ignore_errors = True - -[mypy-rich.*] -ignore_errors = True - -[mypy-libcst.*] -ignore_errors = True - -[mypy-pygments.*] -ignore_missing_imports = True - -[mypy-email_validator.*] -ignore_missing_imports = True - -[mypy-dotenv.*] -ignore_missing_imports = True - -[mypy-django.apps.*] -ignore_missing_imports = True - -[mypy-django.http.*] -ignore_missing_imports = True - -[mypy-strawberry_django.*] -ignore_missing_imports = True - [mypy-cached_property.*] ignore_missing_imports = True diff --git a/mypy_tests.ini b/mypy_tests.ini new file mode 100644 index 0000000000..45aae61cd2 --- /dev/null +++ b/mypy_tests.ini @@ -0,0 +1,39 @@ +[mypy] +plugins = strawberry.ext.mypy_plugin +check_untyped_defs = True +ignore_errors = False +strict_optional = True +implicit_reexport = False +; Disabled because of this bug: https://github.com/python/mypy/issues/9689 +; disallow_untyped_decorators = True + +[mypy-graphql.*] +ignore_errors = True + +[mypy-pydantic.*] +ignore_errors = True + +[mypy-sqlalchemy.*] +ignore_errors = True +check_untyped_defs = False + +[mypy-sqlmodel.*] +ignore_errors = True + +[mypy-ormar.*] +ignore_errors = True + +[mypy-databases.*] +ignore_errors = True + +[mypy-email_validator.*] +ignore_missing_imports = True + +[mypy-dotenv.*] +ignore_missing_imports = True + +[mypy-cached_property.*] +ignore_missing_imports = True + +[mypy-sentinel.*] +ignore_missing_imports = True diff --git a/poetry.lock b/poetry.lock index 4429940c72..05f3e44aed 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,14 +1,14 @@ [[package]] name = "aiofiles" -version = "22.1.0" +version = "0.8.0" description = "File support for asyncio." category = "main" optional = false -python-versions = ">=3.7,<4.0" +python-versions = ">=3.6,<4.0" [[package]] name = "aiohttp" -version = "3.8.3" +version = "3.8.1" description = "Async http client/server framework (asyncio)" category = "main" optional = false @@ -26,15 +26,15 @@ typing-extensions = {version = ">=3.7.4", markers = "python_version < \"3.8\""} yarl = ">=1.0,<2.0" [package.extras] -speedups = ["Brotli", "aiodns", "cchardet"] +speedups = ["aiodns", "brotli", "cchardet"] [[package]] name = "aiosignal" -version = "1.3.1" +version = "1.2.0" description = "aiosignal: a list of registered asynchronous callbacks" category = "main" optional = false -python-versions = ">=3.7" +python-versions = ">=3.6" [package.dependencies] frozenlist = ">=1.1.0" @@ -49,7 +49,7 @@ python-versions = "*" [[package]] name = "anyio" -version = "3.6.2" +version = "3.5.0" description = "High level compatibility layer for multiple asynchronous event loop implementations" category = "main" optional = false @@ -61,13 +61,13 @@ sniffio = ">=1.1" typing-extensions = {version = "*", markers = "python_version < \"3.8\""} [package.extras] -doc = ["packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] -test = ["contextlib2", "coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "mock (>=4)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (<0.15)", "uvloop (>=0.15)"] -trio = ["trio (>=0.16,<0.22)"] +doc = ["packaging", "sphinx-rtd-theme", "sphinx-autodoc-typehints (>=1.2.0)"] +test = ["coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "pytest (>=6.0)", "pytest-mock (>=3.6.1)", "trustme", "contextlib2", "uvloop (<0.15)", "mock (>=4)", "uvloop (>=0.15)"] +trio = ["trio (>=0.16)"] [[package]] name = "asgiref" -version = "3.6.0" +version = "3.5.0" description = "ASGI specs, helper code, and adapters" category = "main" optional = false @@ -77,7 +77,7 @@ python-versions = ">=3.7" typing-extensions = {version = "*", markers = "python_version < \"3.8\""} [package.extras] -tests = ["mypy (>=0.800)", "pytest", "pytest-asyncio"] +tests = ["pytest", "pytest-asyncio", "mypy (>=0.800)"] [[package]] name = "async-timeout" @@ -98,6 +98,14 @@ category = "main" optional = false python-versions = ">=3.5" +[[package]] +name = "atomicwrites" +version = "1.4.0" +description = "Atomic file writes." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + [[package]] name = "attrs" version = "21.4.0" @@ -107,55 +115,14 @@ optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [package.extras] -dev = ["cloudpickle", "coverage[toml] (>=5.0.2)", "furo", "hypothesis", "mypy", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "six", "sphinx", "sphinx-notfound-page", "zope.interface"] -docs = ["furo", "sphinx", "sphinx-notfound-page", "zope.interface"] -tests = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "six", "zope.interface"] -tests-no-zope = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "six"] - -[[package]] -name = "autobahn" -version = "22.12.1" -description = "WebSocket client & server library, WAMP real-time framework" -category = "main" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -cryptography = ">=3.4.6" -hyperlink = ">=21.0.0" -setuptools = "*" -txaio = ">=21.2.1" - -[package.extras] -all = ["PyGObject (>=3.40.0)", "argon2_cffi (>=20.1.0)", "attrs (>=20.3.0)", "base58 (>=2.1.0)", "cbor2 (>=5.2.0)", "cffi (>=1.14.5)", "click (>=8.1.2)", "ecdsa (>=0.16.1)", "eth-abi (>=2.1.1)", "flatbuffers (>=22.12.6)", "hkdf (>=0.0.3)", "jinja2 (>=2.11.3)", "mnemonic (>=0.19)", "msgpack (>=1.0.2)", "passlib (>=1.7.4)", "py-ecc (>=5.1.0)", "py-eth-sig-utils (>=0.4.0)", "py-multihash (>=2.0.1)", "py-ubjson (>=0.16.1)", "pynacl (>=1.4.0)", "pyopenssl (>=20.0.1)", "python-snappy (>=0.6.0)", "pytrie (>=0.4.0)", "qrcode (>=7.3.1)", "rlp (>=2.0.1)", "service_identity (>=18.1.0)", "spake2 (>=0.8)", "twisted (>=20.3.0)", "ujson (>=4.0.2)", "web3 (>=5.29.0)", "xbr (>=21.2.1)", "yapf (==0.29.0)", "zlmdb (>=21.2.1)", "zope.interface (>=5.2.0)"] -compress = ["python-snappy (>=0.6.0)"] -dev = ["awscli", "backports.tempfile (>=1.0)", "bumpversion (>=0.5.3)", "codecov (>=2.0.15)", "flake8 (<5)", "humanize (>=0.5.1)", "mypy (>=0.610)", "passlib", "pep8-naming (>=0.3.3)", "pip (>=9.0.1)", "pyenchant (>=1.6.6)", "pyflakes (>=1.0.0)", "pyinstaller (>=4.2)", "pylint (>=1.9.2)", "pytest (>=3.4.2)", "pytest-aiohttp", "pytest-asyncio (>=0.14.0)", "pytest-runner (>=2.11.1)", "pyyaml (>=4.2b4)", "qualname", "sphinx (>=1.7.1)", "sphinx-autoapi (>=1.7.0)", "sphinx_rtd_theme (>=0.1.9)", "sphinxcontrib-images (>=0.9.1)", "tox (>=2.9.1)", "tox-gh-actions (>=2.2.0)", "twine (>=3.3.0)", "twisted (>=18.7.0)", "txaio (>=20.4.1)", "watchdog (>=0.8.3)", "wheel (>=0.36.2)", "yapf (==0.29.0)"] -encryption = ["pynacl (>=1.4.0)", "pyopenssl (>=20.0.1)", "pytrie (>=0.4.0)", "qrcode (>=7.3.1)", "service_identity (>=18.1.0)"] -nvx = ["cffi (>=1.14.5)"] -scram = ["argon2_cffi (>=20.1.0)", "cffi (>=1.14.5)", "passlib (>=1.7.4)"] -serialization = ["cbor2 (>=5.2.0)", "flatbuffers (>=22.12.6)", "msgpack (>=1.0.2)", "py-ubjson (>=0.16.1)", "ujson (>=4.0.2)"] -twisted = ["attrs (>=20.3.0)", "twisted (>=20.3.0)", "zope.interface (>=5.2.0)"] -ui = ["PyGObject (>=3.40.0)"] -xbr = ["base58 (>=2.1.0)", "cbor2 (>=5.2.0)", "click (>=8.1.2)", "ecdsa (>=0.16.1)", "eth-abi (>=2.1.1)", "hkdf (>=0.0.3)", "jinja2 (>=2.11.3)", "mnemonic (>=0.19)", "py-ecc (>=5.1.0)", "py-eth-sig-utils (>=0.4.0)", "py-multihash (>=2.0.1)", "rlp (>=2.0.1)", "spake2 (>=0.8)", "twisted (>=20.3.0)", "web3 (>=5.29.0)", "xbr (>=21.2.1)", "yapf (==0.29.0)", "zlmdb (>=21.2.1)"] - -[[package]] -name = "automat" -version = "22.10.0" -description = "Self-service finite-state machines for the programmer on the go." -category = "main" -optional = false -python-versions = "*" - -[package.dependencies] -attrs = ">=19.2.0" -six = "*" - -[package.extras] -visualize = ["Twisted (>=16.1.1)", "graphviz (>0.5.1)"] +dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit", "cloudpickle"] +docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] +tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "cloudpickle"] +tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "cloudpickle"] [[package]] -name = "backports-cached-property" -version = "1.0.2" +name = "backports.cached-property" +version = "1.0.1" description = "cached_property() - computed once per instance, cached as attribute" category = "main" optional = false @@ -163,25 +130,29 @@ python-versions = ">=3.6.0" [[package]] name = "black" -version = "22.12.0" +version = "21.12b0" description = "The uncompromising code formatter." category = "dev" optional = false -python-versions = ">=3.7" +python-versions = ">=3.6.2" [package.dependencies] -click = ">=8.0.0" +click = ">=7.1.2" mypy-extensions = ">=0.4.3" -pathspec = ">=0.9.0" +pathspec = ">=0.9.0,<1" platformdirs = ">=2" -tomli = {version = ">=1.1.0", markers = "python_full_version < \"3.11.0a7\""} +tomli = ">=0.2.6,<2.0.0" typed-ast = {version = ">=1.4.2", markers = "python_version < \"3.8\" and implementation_name == \"cpython\""} -typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""} +typing-extensions = [ + {version = ">=3.10.0.0", markers = "python_version < \"3.10\""}, + {version = "!=3.10.0.1", markers = "python_version >= \"3.10\""}, +] [package.extras] colorama = ["colorama (>=0.4.3)"] d = ["aiohttp (>=3.7.4)"] jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] +python2 = ["typed-ast (>=1.4.3)"] uvloop = ["uvloop (>=0.15.2)"] [[package]] @@ -199,74 +170,39 @@ wcwidth = ">=0.1.4" [[package]] name = "botocore" -version = "1.29.45" +version = "1.24.2" description = "Low-level, data-driven core of boto 3." category = "main" optional = false -python-versions = ">= 3.7" +python-versions = ">= 3.6" [package.dependencies] -jmespath = ">=0.7.1,<2.0.0" +jmespath = ">=0.7.1,<1.0.0" python-dateutil = ">=2.1,<3.0.0" urllib3 = ">=1.25.4,<1.27" [package.extras] -crt = ["awscrt (==0.15.3)"] - -[[package]] -name = "bytecode" -version = "0.13.0" -description = "Python module to generate and modify bytecode" -category = "dev" -optional = false -python-versions = ">=3.6" - -[[package]] -name = "bytecode" -version = "0.14.0" -description = "Python module to generate and modify bytecode" -category = "dev" -optional = false -python-versions = ">=3.8" - -[package.dependencies] -typing-extensions = {version = "*", markers = "python_version < \"3.10\""} - -[[package]] -name = "cattrs" -version = "22.2.0" -description = "Composable complex class support for attrs and dataclasses." -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -attrs = ">=20" -exceptiongroup = {version = "*", markers = "python_version < \"3.11\""} -typing_extensions = {version = "*", markers = "python_version < \"3.8\""} +crt = ["awscrt (==0.12.5)"] [[package]] name = "certifi" -version = "2022.12.7" +version = "2021.10.8" description = "Python package for providing Mozilla's CA Bundle." -category = "main" +category = "dev" optional = false -python-versions = ">=3.6" +python-versions = "*" [[package]] -name = "cffi" -version = "1.15.1" -description = "Foreign Function Interface for Python calling C code." -category = "main" +name = "cfgv" +version = "3.3.1" +description = "Validate configuration and produce human readable error messages." +category = "dev" optional = false -python-versions = "*" - -[package.dependencies] -pycparser = "*" +python-versions = ">=3.6.1" [[package]] name = "chalice" -version = "1.27.3" +version = "1.26.6" description = "Microframework" category = "main" optional = false @@ -277,45 +213,25 @@ attrs = ">=19.3.0,<21.5.0" botocore = ">=1.14.0,<2.0.0" click = ">=7,<9.0" inquirer = ">=2.7.0,<3.0.0" -jmespath = ">=0.9.3,<2.0.0" -pip = ">=9,<22.3" +jmespath = ">=0.9.3,<1.0.0" +mypy-extensions = "0.4.3" pyyaml = ">=5.3.1,<7.0.0" -setuptools = "*" six = ">=1.10.0,<2.0.0" -typing-extensions = ">=4.0.0,<5.0.0" -wheel = "*" [package.extras] cdk = ["aws-cdk.aws-iam (>=1.85.0,<2.0)", "aws-cdk.aws-s3-assets (>=1.85.0,<2.0)", "aws-cdk.cloudformation-include (>=1.85.0,<2.0)", "aws-cdk.core (>=1.85.0,<2.0)"] -cdkv2 = ["aws-cdk-lib (>2.0,<3.0)"] event-file-poller = ["watchdog (==0.9.0)"] -[[package]] -name = "channels" -version = "3.0.5" -description = "Brings async, event-driven capabilities to Django. Django 2.2 and up only." -category = "main" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -asgiref = ">=3.5.0,<4" -daphne = ">=3.0,<4" -Django = ">=2.2" - -[package.extras] -tests = ["async-timeout", "coverage (>=4.5,<5.0)", "pytest", "pytest-asyncio", "pytest-django"] - [[package]] name = "charset-normalizer" -version = "2.1.1" +version = "2.0.12" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." category = "main" optional = false -python-versions = ">=3.6.0" +python-versions = ">=3.5.0" [package.extras] -unicode-backport = ["unicodedata2"] +unicode_backport = ["unicodedata2"] [[package]] name = "chevron" @@ -327,132 +243,51 @@ python-versions = "*" [[package]] name = "click" -version = "8.1.3" +version = "7.1.2" description = "Composable command line interface toolkit" category = "main" optional = false -python-versions = ">=3.7" - -[package.dependencies] -colorama = {version = "*", markers = "platform_system == \"Windows\""} -importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [[package]] name = "colorama" -version = "0.4.6" +version = "0.4.4" description = "Cross-platform colored terminal text." -category = "main" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" - -[[package]] -name = "commonmark" -version = "0.9.1" -description = "Python parser for the CommonMark Markdown spec" -category = "main" -optional = false -python-versions = "*" - -[package.extras] -test = ["flake8 (==3.7.8)", "hypothesis (==3.55.3)"] - -[[package]] -name = "constantly" -version = "15.1.0" -description = "Symbolic constants in Python" -category = "main" +category = "dev" optional = false -python-versions = "*" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [[package]] name = "coverage" -version = "7.0.4" +version = "6.3.1" description = "Code coverage measurement for Python" category = "dev" optional = false python-versions = ">=3.7" [package.dependencies] -tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} +tomli = {version = "*", optional = true, markers = "extra == \"toml\""} [package.extras] toml = ["tomli"] [[package]] -name = "cryptography" -version = "39.0.0" -description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." -category = "main" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -cffi = ">=1.12" - -[package.extras] -docs = ["sphinx (>=1.6.5,!=1.8.0,!=3.1.0,!=3.1.1,!=5.2.0,!=5.2.0.post0)", "sphinx-rtd-theme"] -docstest = ["pyenchant (>=1.6.11)", "sphinxcontrib-spelling (>=4.0.1)", "twine (>=1.12.0)"] -pep8test = ["black", "ruff"] -sdist = ["setuptools-rust (>=0.11.4)"] -ssh = ["bcrypt (>=3.1.5)"] -test = ["hypothesis (>=1.11.4,!=3.79.2)", "iso8601", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-subtests", "pytest-xdist", "pytz"] - -[[package]] -name = "daphne" -version = "3.0.2" -description = "Django ASGI (HTTP/WebSocket) server" +name = "databases" +version = "0.5.4" +description = "Async database support for Python." category = "main" optional = false python-versions = ">=3.6" [package.dependencies] -asgiref = ">=3.2.10,<4" -autobahn = ">=0.18" -twisted = {version = ">=18.7", extras = ["tls"]} - -[package.extras] -tests = ["hypothesis (==4.23)", "pytest (>=3.10,<4.0)", "pytest-asyncio (>=0.8,<1.0)"] - -[[package]] -name = "ddsketch" -version = "2.0.4" -description = "Distributed quantile sketches" -category = "dev" -optional = false -python-versions = ">=2.7" - -[package.dependencies] -protobuf = {version = ">=3.0.0", markers = "python_version >= \"3.7\""} -six = "*" - -[[package]] -name = "ddtrace" -version = "1.7.3" -description = "Datadog APM client library" -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" - -[package.dependencies] -attrs = {version = ">=20", markers = "python_version > \"2.7\""} -bytecode = [ - {version = ">=0.13.0,<0.14.0", markers = "python_version == \"3.7\""}, - {version = "*", markers = "python_version >= \"3.8\""}, -] -cattrs = "*" -ddsketch = ">=2.0.1" -envier = "*" -importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} -jsonschema = "*" -packaging = ">=17.1" -protobuf = {version = ">=3", markers = "python_version >= \"3.7\""} -six = ">=1.12.0" -tenacity = ">=5" -typing-extensions = "*" -xmltodict = ">=0.12" +sqlalchemy = ">=1.4,<1.5" [package.extras] -opentracing = ["opentracing (>=2.0.0)"] +mysql = ["aiomysql"] +mysql_asyncmy = ["asyncmy"] +postgresql = ["asyncpg"] +postgresql_aiopg = ["aiopg"] +sqlite = ["aiosqlite"] [[package]] name = "decorator" @@ -474,11 +309,19 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" wrapt = ">=1.10,<2" [package.extras] -dev = ["PyTest", "PyTest (<5)", "PyTest-Cov", "PyTest-Cov (<2.6)", "bump2version (<1)", "configparser (<5)", "importlib-metadata (<3)", "importlib-resources (<4)", "sphinx (<2)", "sphinxcontrib-websupport (<2)", "tox", "zipp (<2)"] +dev = ["tox", "bump2version (<1)", "sphinx (<2)", "importlib-metadata (<3)", "importlib-resources (<4)", "configparser (<5)", "sphinxcontrib-websupport (<2)", "zipp (<2)", "PyTest (<5)", "PyTest-Cov (<2.6)", "pytest", "pytest-cov"] + +[[package]] +name = "distlib" +version = "0.3.4" +description = "Distribution utilities" +category = "dev" +optional = false +python-versions = "*" [[package]] name = "django" -version = "3.2.16" +version = "3.2.12" description = "A high-level Python Web framework that encourages rapid development and clean, pragmatic design." category = "main" optional = false @@ -495,15 +338,15 @@ bcrypt = ["bcrypt"] [[package]] name = "dnspython" -version = "2.2.1" +version = "2.2.0" description = "DNS toolkit" -category = "main" +category = "dev" optional = false python-versions = ">=3.6,<4.0" [package.extras] -curio = ["curio (>=1.2,<2.0)", "sniffio (>=1.1,<2.0)"] dnssec = ["cryptography (>=2.6,<37.0)"] +curio = ["curio (>=1.2,<2.0)", "sniffio (>=1.1,<2.0)"] doh = ["h2 (>=4.1.0)", "httpx (>=0.21.1)", "requests (>=2.23.0,<3.0.0)", "requests-toolbelt (>=0.9.1,<0.10.0)"] idna = ["idna (>=2.1,<4.0)"] trio = ["trio (>=0.14,<0.20)"] @@ -511,122 +354,158 @@ wmi = ["wmi (>=1.5.1,<2.0.0)"] [[package]] name = "email-validator" -version = "1.3.1" -description = "A robust email address syntax and deliverability validation library." -category = "main" +version = "1.1.3" +description = "A robust email syntax and deliverability validation library for Python 2.x/3.x." +category = "dev" optional = false -python-versions = ">=3.5" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" [package.dependencies] dnspython = ">=1.15.0" idna = ">=2.0.0" [[package]] -name = "envier" -version = "0.4.0" -description = "Python application configuration via the environment" +name = "eradicate" +version = "2.0.0" +description = "Removes commented-out code." category = "dev" optional = false -python-versions = ">=2.7" +python-versions = "*" + +[[package]] +name = "fastapi" +version = "0.70.1" +description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" +category = "main" +optional = false +python-versions = ">=3.6.1" + +[package.dependencies] +pydantic = ">=1.6.2,<1.7 || >1.7,<1.7.1 || >1.7.1,<1.7.2 || >1.7.2,<1.7.3 || >1.7.3,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0" +starlette = "0.16.0" [package.extras] -mypy = ["mypy"] +all = ["requests (>=2.24.0,<3.0.0)", "jinja2 (>=2.11.2,<4.0.0)", "python-multipart (>=0.0.5,<0.0.6)", "itsdangerous (>=1.1.0,<3.0.0)", "pyyaml (>=5.3.1,<6.0.0)", "ujson (>=4.0.1,<5.0.0)", "orjson (>=3.2.1,<4.0.0)", "email_validator (>=1.1.1,<2.0.0)", "uvicorn[standard] (>=0.12.0,<0.16.0)"] +dev = ["python-jose[cryptography] (>=3.3.0,<4.0.0)", "passlib[bcrypt] (>=1.7.2,<2.0.0)", "autoflake (>=1.4.0,<2.0.0)", "flake8 (>=3.8.3,<4.0.0)", "uvicorn[standard] (>=0.12.0,<0.16.0)"] +doc = ["mkdocs (>=1.1.2,<2.0.0)", "mkdocs-material (>=7.1.9,<8.0.0)", "mdx-include (>=1.4.1,<2.0.0)", "mkdocs-markdownextradata-plugin (>=0.1.7,<0.3.0)", "typer-cli (>=0.0.12,<0.0.13)", "pyyaml (>=5.3.1,<6.0.0)"] +test = ["pytest (>=6.2.4,<7.0.0)", "pytest-cov (>=2.12.0,<4.0.0)", "mypy (==0.910)", "flake8 (>=3.8.3,<4.0.0)", "black (==21.9b0)", "isort (>=5.0.6,<6.0.0)", "requests (>=2.24.0,<3.0.0)", "httpx (>=0.14.0,<0.19.0)", "email_validator (>=1.1.1,<2.0.0)", "sqlalchemy (>=1.3.18,<1.5.0)", "peewee (>=3.13.3,<4.0.0)", "databases[sqlite] (>=0.3.2,<0.6.0)", "orjson (>=3.2.1,<4.0.0)", "ujson (>=4.0.1,<5.0.0)", "python-multipart (>=0.0.5,<0.0.6)", "flask (>=1.1.2,<3.0.0)", "anyio[trio] (>=3.2.1,<4.0.0)", "types-ujson (==0.1.1)", "types-orjson (==3.6.0)", "types-dataclasses (==0.1.7)"] [[package]] -name = "exceptiongroup" -version = "1.1.0" -description = "Backport of PEP 654 (exception groups)" +name = "filelock" +version = "3.6.0" +description = "A platform independent file lock." category = "dev" optional = false python-versions = ">=3.7" [package.extras] -test = ["pytest (>=6)"] +docs = ["furo (>=2021.8.17b43)", "sphinx (>=4.1)", "sphinx-autodoc-typehints (>=1.12)"] +testing = ["covdefaults (>=1.2.0)", "coverage (>=4)", "pytest (>=4)", "pytest-cov", "pytest-timeout (>=1.4.2)"] [[package]] -name = "execnet" -version = "1.9.0" -description = "execnet: rapid multi-Python deployment" +name = "flake8" +version = "4.0.1" +description = "the modular source code checker: pep8 pyflakes and co" category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = ">=3.6" -[package.extras] -testing = ["pre-commit"] +[package.dependencies] +importlib-metadata = {version = "<4.3", markers = "python_version < \"3.8\""} +mccabe = ">=0.6.0,<0.7.0" +pycodestyle = ">=2.8.0,<2.9.0" +pyflakes = ">=2.4.0,<2.5.0" [[package]] -name = "faker" -version = "16.6.1" -description = "Faker is a Python package that generates fake data for you." -category = "main" +name = "flake8-black" +version = "0.2.4" +description = "flake8 plugin to call black as a code style validator" +category = "dev" optional = false -python-versions = ">=3.7" +python-versions = "*" [package.dependencies] -python-dateutil = ">=2.4" +black = "*" +flake8 = ">=3.0.0" +toml = "*" [[package]] -name = "fast-query-parsers" -version = "0.3.0" -description = "Ultra-fast query string and url-encoded form-data parsers" -category = "main" +name = "flake8-bugbear" +version = "22.1.11" +description = "A plugin for flake8 finding likely bugs and design problems in your program. Contains warnings that don't belong in pyflakes and pycodestyle." +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +attrs = ">=19.2.0" +flake8 = ">=3.0.0" + +[package.extras] +dev = ["coverage", "hypothesis", "hypothesmith (>=0.2)", "pre-commit"] + +[[package]] +name = "flake8-eradicate" +version = "1.2.0" +description = "Flake8 plugin to find commented out code" +category = "dev" optional = false -python-versions = ">=3.8" +python-versions = ">=3.6,<4.0" [package.dependencies] -maturin = "*" +attrs = "*" +eradicate = ">=2.0,<3.0" +flake8 = ">=3.5,<5" [[package]] -name = "fastapi" -version = "0.89.1" -description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" -category = "main" +name = "flake8-isort" +version = "4.1.1" +description = "flake8 plugin that integrates isort ." +category = "dev" optional = false -python-versions = ">=3.7" +python-versions = "*" [package.dependencies] -pydantic = ">=1.6.2,<1.7 || >1.7,<1.7.1 || >1.7.1,<1.7.2 || >1.7.2,<1.7.3 || >1.7.3,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0" -starlette = "0.22.0" +flake8 = ">=3.2.1,<5" +isort = ">=4.3.5,<6" +testfixtures = ">=6.8.0,<7" [package.extras] -all = ["email-validator (>=1.1.1)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "python-multipart (>=0.0.5)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"] -dev = ["pre-commit (>=2.17.0,<3.0.0)", "ruff (==0.0.138)", "uvicorn[standard] (>=0.12.0,<0.21.0)"] -doc = ["mdx-include (>=1.4.1,<2.0.0)", "mkdocs (>=1.1.2,<2.0.0)", "mkdocs-markdownextradata-plugin (>=0.1.7,<0.3.0)", "mkdocs-material (>=8.1.4,<9.0.0)", "pyyaml (>=5.3.1,<7.0.0)", "typer[all] (>=0.6.1,<0.8.0)"] -test = ["anyio[trio] (>=3.2.1,<4.0.0)", "black (==22.10.0)", "coverage[toml] (>=6.5.0,<8.0)", "databases[sqlite] (>=0.3.2,<0.7.0)", "email-validator (>=1.1.1,<2.0.0)", "flask (>=1.1.2,<3.0.0)", "httpx (>=0.23.0,<0.24.0)", "isort (>=5.0.6,<6.0.0)", "mypy (==0.982)", "orjson (>=3.2.1,<4.0.0)", "passlib[bcrypt] (>=1.7.2,<2.0.0)", "peewee (>=3.13.3,<4.0.0)", "pytest (>=7.1.3,<8.0.0)", "python-jose[cryptography] (>=3.3.0,<4.0.0)", "python-multipart (>=0.0.5,<0.0.6)", "pyyaml (>=5.3.1,<7.0.0)", "ruff (==0.0.138)", "sqlalchemy (>=1.3.18,<1.4.43)", "types-orjson (==3.6.2)", "types-ujson (==5.6.0.0)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0,<6.0.0)"] +test = ["pytest-cov"] [[package]] name = "flask" -version = "2.2.2" +version = "1.1.4" description = "A simple framework for building complex web applications." category = "main" optional = false -python-versions = ">=3.7" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [package.dependencies] -click = ">=8.0" -importlib-metadata = {version = ">=3.6.0", markers = "python_version < \"3.10\""} -itsdangerous = ">=2.0" -Jinja2 = ">=3.0" -Werkzeug = ">=2.2.2" +click = ">=5.1,<8.0" +itsdangerous = ">=0.24,<2.0" +Jinja2 = ">=2.10.1,<3.0" +Werkzeug = ">=0.15,<2.0" [package.extras] -async = ["asgiref (>=3.2)"] +dev = ["pytest", "coverage", "tox", "sphinx", "pallets-sphinx-themes", "sphinxcontrib-log-cabinet", "sphinx-issues"] +docs = ["sphinx", "pallets-sphinx-themes", "sphinxcontrib-log-cabinet", "sphinx-issues"] dotenv = ["python-dotenv"] [[package]] name = "freezegun" -version = "1.2.2" +version = "1.1.0" description = "Let your Python tests travel through time" category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.5" [package.dependencies] python-dateutil = ">=2.7" [[package]] name = "frozenlist" -version = "1.3.3" +version = "1.3.0" description = "A list-like structure which implements collections.abc.MutableSequence" category = "main" optional = false @@ -634,38 +513,32 @@ python-versions = ">=3.7" [[package]] name = "graphql-core" -version = "3.2.3" +version = "3.1.7" description = "GraphQL implementation for Python, a port of GraphQL.js, the JavaScript reference implementation for GraphQL." category = "main" optional = false python-versions = ">=3.6,<4" -[package.dependencies] -typing-extensions = {version = ">=4.2,<5", markers = "python_version < \"3.8\""} - [[package]] name = "h11" -version = "0.14.0" +version = "0.12.0" description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" category = "main" optional = false -python-versions = ">=3.7" - -[package.dependencies] -typing-extensions = {version = "*", markers = "python_version < \"3.8\""} +python-versions = ">=3.6" [[package]] name = "httpcore" -version = "0.16.3" +version = "0.14.7" description = "A minimal low-level HTTP client." -category = "main" +category = "dev" optional = false -python-versions = ">=3.7" +python-versions = ">=3.6" [package.dependencies] -anyio = ">=3.0,<5.0" +anyio = ">=3.0.0,<4.0.0" certifi = "*" -h11 = ">=0.13,<0.15" +h11 = ">=0.11,<0.13" sniffio = ">=1.0.0,<2.0.0" [package.extras] @@ -674,7 +547,7 @@ socks = ["socksio (>=1.0.0,<2.0.0)"] [[package]] name = "httptools" -version = "0.5.0" +version = "0.3.0" description = "A collection of framework independent HTTP protocol utils." category = "main" optional = false @@ -685,38 +558,38 @@ test = ["Cython (>=0.29.24,<0.30.0)"] [[package]] name = "httpx" -version = "0.23.3" +version = "0.21.3" description = "The next generation HTTP client." -category = "main" +category = "dev" optional = false -python-versions = ">=3.7" +python-versions = ">=3.6" [package.dependencies] certifi = "*" -httpcore = ">=0.15.0,<0.17.0" +charset-normalizer = "*" +httpcore = ">=0.14.0,<0.15.0" rfc3986 = {version = ">=1.3,<2", extras = ["idna2008"]} sniffio = "*" [package.extras] -brotli = ["brotli", "brotlicffi"] -cli = ["click (>=8.0.0,<9.0.0)", "pygments (>=2.0.0,<3.0.0)", "rich (>=10,<13)"] +brotli = ["brotlicffi", "brotli"] +cli = ["click (>=8.0.0,<9.0.0)", "rich (>=10.0.0,<11.0.0)", "pygments (>=2.0.0,<3.0.0)"] http2 = ["h2 (>=3,<5)"] -socks = ["socksio (>=1.0.0,<2.0.0)"] [[package]] -name = "hyperlink" -version = "21.0.0" -description = "A featureful, immutable, and correct URL for Python." -category = "main" +name = "identify" +version = "2.4.10" +description = "File identification library for Python" +category = "dev" optional = false -python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +python-versions = ">=3.7" -[package.dependencies] -idna = ">=2.5" +[package.extras] +license = ["ukkonen"] [[package]] name = "idna" -version = "3.4" +version = "3.3" description = "Internationalized Domain Names in Applications (IDNA)" category = "main" optional = false @@ -724,59 +597,31 @@ python-versions = ">=3.5" [[package]] name = "importlib-metadata" -version = "6.0.0" +version = "4.2.0" description = "Read metadata from Python packages" category = "main" optional = false -python-versions = ">=3.7" +python-versions = ">=3.6" [package.dependencies] typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} zipp = ">=0.5" [package.extras] -docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -perf = ["ipython"] -testing = ["flake8 (<5)", "flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)"] - -[[package]] -name = "importlib-resources" -version = "5.10.2" -description = "Read resources from Python packages" -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -zipp = {version = ">=3.1.0", markers = "python_version < \"3.10\""} - -[package.extras] -docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -testing = ["flake8 (<5)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] - -[[package]] -name = "incremental" -version = "22.10.0" -description = "\"A small library that versions your Python projects.\"" -category = "main" -optional = false -python-versions = "*" - -[package.extras] -mypy = ["click (>=6.0)", "mypy (==0.812)", "twisted (>=16.4.0)"] -scripts = ["click (>=6.0)", "twisted (>=16.4.0)"] +docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] +testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"] [[package]] name = "iniconfig" -version = "2.0.0" -description = "brain-dead simple config-ini parsing" +version = "1.1.1" +description = "iniconfig: brain-dead simple config-ini parsing" category = "dev" optional = false -python-versions = ">=3.7" +python-versions = "*" [[package]] name = "inquirer" -version = "2.10.1" +version = "2.9.1" description = "Collection of common interactive command line user interfaces, based on Inquirer.js" category = "main" optional = false @@ -785,33 +630,47 @@ python-versions = ">=3.7" [package.dependencies] blessed = ">=1.19.0" python-editor = ">=1.0.4" -readchar = ">=3.0.6" +readchar = ">=2.0.1" + +[[package]] +name = "isort" +version = "5.10.1" +description = "A Python utility / library to sort Python imports." +category = "dev" +optional = false +python-versions = ">=3.6.1,<4.0" + +[package.extras] +pipfile_deprecated_finder = ["pipreqs", "requirementslib"] +requirements_deprecated_finder = ["pipreqs", "pip-api"] +colors = ["colorama (>=0.4.3,<0.5.0)"] +plugins = ["setuptools"] [[package]] name = "itsdangerous" -version = "2.1.2" -description = "Safely pass data to untrusted environments and back." +version = "1.1.0" +description = "Various helpers to pass data to untrusted environments and back." category = "main" optional = false -python-versions = ">=3.7" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "jinja2" -version = "3.1.2" +version = "2.11.3" description = "A very fast and expressive template engine." category = "main" optional = false -python-versions = ">=3.7" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [package.dependencies] -MarkupSafe = ">=2.0" +MarkupSafe = ">=0.23" [package.extras] -i18n = ["Babel (>=2.7)"] +i18n = ["Babel (>=0.8)"] [[package]] name = "jinxed" -version = "1.2.0" +version = "1.1.0" description = "Jinxed Terminal Library" category = "main" optional = false @@ -822,122 +681,53 @@ ansicon = {version = "*", markers = "platform_system == \"Windows\""} [[package]] name = "jmespath" -version = "1.0.1" +version = "0.10.0" description = "JSON Matching Expressions" category = "main" optional = false -python-versions = ">=3.7" - -[[package]] -name = "jsonschema" -version = "4.17.3" -description = "An implementation of JSON Schema validation for Python" -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -attrs = ">=17.4.0" -importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} -importlib-resources = {version = ">=1.4.0", markers = "python_version < \"3.9\""} -pkgutil-resolve-name = {version = ">=1.3.10", markers = "python_version < \"3.9\""} -pyrsistent = ">=0.14.0,<0.17.0 || >0.17.0,<0.17.1 || >0.17.1,<0.17.2 || >0.17.2" -typing-extensions = {version = "*", markers = "python_version < \"3.8\""} - -[package.extras] -format = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3987", "uri-template", "webcolors (>=1.11)"] -format-nongpl = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3986-validator (>0.1.0)", "uri-template", "webcolors (>=1.11)"] - -[[package]] -name = "libcst" -version = "0.4.9" -description = "A concrete syntax tree with AST-like properties for Python 3.5, 3.6, 3.7, 3.8, 3.9, and 3.10 programs." -category = "main" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -pyyaml = ">=5.2" -typing-extensions = ">=3.7.4.2" -typing-inspect = ">=0.4.0" - -[package.extras] -dev = ["Sphinx (>=5.1.1)", "black (==22.10.0)", "coverage (>=4.5.4)", "fixit (==0.1.1)", "flake8 (>=3.7.8,<5)", "hypothesis (>=4.36.0)", "hypothesmith (>=0.0.4)", "jinja2 (==3.1.2)", "jupyter (>=1.0.0)", "maturin (>=0.8.3,<0.14)", "nbsphinx (>=0.4.2)", "prompt-toolkit (>=2.0.9)", "pyre-check (==0.9.9)", "setuptools-rust (>=1.5.2)", "setuptools-scm (>=6.0.1)", "slotscheck (>=0.7.1)", "sphinx-rtd-theme (>=0.4.3)", "ufmt (==2.0.1)", "usort (==1.0.5)"] - -[[package]] -name = "mako" -version = "1.2.4" -description = "A super-fast templating language that borrows the best ideas from the existing templating languages." -category = "main" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -MarkupSafe = ">=0.9.2" - -[package.extras] -babel = ["Babel"] -lingua = ["lingua"] -testing = ["pytest"] +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" [[package]] name = "markupsafe" -version = "2.1.1" +version = "2.1.0" description = "Safely add untrusted strings to HTML/XML markup." category = "main" optional = false python-versions = ">=3.7" -[[package]] -name = "maturin" -version = "0.14.10" -description = "Build and publish crates with pyo3, rust-cpython and cffi bindings as well as rust binaries as python packages" -category = "main" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} - -[package.extras] -patchelf = ["patchelf"] -zig = ["ziglang (>=0.10.0,<0.11.0)"] - -[[package]] -name = "msgspec" -version = "0.12.0" -description = "A fast and friendly JSON/MessagePack library, with optional schema validation" -category = "main" +[[package]] +name = "mccabe" +version = "0.6.1" +description = "McCabe checker, plugin for flake8" +category = "dev" optional = false -python-versions = ">=3.8" +python-versions = "*" [[package]] name = "multidict" -version = "6.0.4" +version = "5.2.0" description = "multidict implementation" category = "main" optional = false -python-versions = ">=3.7" +python-versions = ">=3.6" [[package]] name = "mypy" -version = "0.991" +version = "0.931" description = "Optional static typing for Python" category = "dev" optional = false -python-versions = ">=3.7" +python-versions = ">=3.6" [package.dependencies] mypy-extensions = ">=0.4.3" -tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +tomli = ">=1.1.0" typed-ast = {version = ">=1.4.0,<2", markers = "python_version < \"3.8\""} typing-extensions = ">=3.10" [package.extras] dmypy = ["psutil (>=4.0)"] -install-types = ["pip"] python2 = ["typed-ast (>=1.4.0,<2)"] -reports = ["lxml"] [[package]] name = "mypy-extensions" @@ -947,86 +737,76 @@ category = "main" optional = false python-versions = "*" +[[package]] +name = "nodeenv" +version = "1.6.0" +description = "Node.js virtual environment builder" +category = "dev" +optional = false +python-versions = "*" + [[package]] name = "opentelemetry-api" -version = "1.15.0" +version = "1.9.1" description = "OpenTelemetry Python API" category = "main" optional = false -python-versions = ">=3.7" +python-versions = ">=3.6" [package.dependencies] -deprecated = ">=1.2.6" -setuptools = ">=16.0" +Deprecated = ">=1.2.6" [[package]] name = "opentelemetry-sdk" -version = "1.15.0" +version = "1.9.1" description = "OpenTelemetry Python SDK" category = "main" optional = false -python-versions = ">=3.7" +python-versions = ">=3.6" [package.dependencies] -opentelemetry-api = "1.15.0" -opentelemetry-semantic-conventions = "0.36b0" -setuptools = ">=16.0" +opentelemetry-api = "1.9.1" +opentelemetry-semantic-conventions = "0.28b1" typing-extensions = ">=3.7.4" [[package]] name = "opentelemetry-semantic-conventions" -version = "0.36b0" +version = "0.28b1" description = "OpenTelemetry Semantic Conventions" category = "main" optional = false -python-versions = ">=3.7" +python-versions = ">=3.6" [[package]] name = "packaging" -version = "23.0" +version = "21.3" description = "Core utilities for Python packages" category = "dev" optional = false -python-versions = ">=3.7" +python-versions = ">=3.6" + +[package.dependencies] +pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" [[package]] name = "pathspec" -version = "0.10.3" +version = "0.9.0" description = "Utility library for gitignore style pattern matching of file paths." category = "dev" optional = false -python-versions = ">=3.7" - -[[package]] -name = "pip" -version = "22.2.2" -description = "The PyPA recommended tool for installing Python packages." -category = "main" -optional = false -python-versions = ">=3.7" - -[[package]] -name = "pkgutil-resolve-name" -version = "1.3.10" -description = "Resolve a name to an object." -category = "dev" -optional = false -python-versions = ">=3.6" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" [[package]] name = "platformdirs" -version = "2.6.2" -description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +version = "2.5.0" +description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." category = "dev" optional = false python-versions = ">=3.7" -[package.dependencies] -typing-extensions = {version = ">=4.4", markers = "python_version < \"3.8\""} - [package.extras] -docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx (>=5.3)", "sphinx-autodoc-typehints (>=1.19.5)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.2.2)", "pytest (>=7.2)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"] +docs = ["Sphinx (>=4)", "furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)"] +test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)"] [[package]] name = "pluggy" @@ -1044,150 +824,117 @@ dev = ["pre-commit", "tox"] testing = ["pytest", "pytest-benchmark"] [[package]] -name = "protobuf" -version = "4.21.12" -description = "" +name = "pre-commit" +version = "2.17.0" +description = "A framework for managing and maintaining multi-language pre-commit hooks." category = "dev" optional = false -python-versions = ">=3.7" +python-versions = ">=3.6.1" + +[package.dependencies] +cfgv = ">=2.0.0" +identify = ">=1.0.0" +importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} +nodeenv = ">=0.11.1" +pyyaml = ">=5.1" +toml = "*" +virtualenv = ">=20.0.8" [[package]] name = "psutil" -version = "5.9.4" +version = "5.9.0" description = "Cross-platform lib for process and system monitoring in Python." category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [package.extras] -test = ["enum34", "ipaddress", "mock", "pywin32", "wmi"] +test = ["ipaddress", "mock", "unittest2", "enum34", "pywin32", "wmi"] [[package]] -name = "py-cpuinfo" -version = "9.0.0" -description = "Get CPU info with pure Python" +name = "py" +version = "1.11.0" +description = "library with cross-python path, ini-parsing, io, code, log facilities" category = "dev" optional = false -python-versions = "*" - -[[package]] -name = "pyasn1" -version = "0.4.8" -description = "ASN.1 types and codecs" -category = "main" -optional = false -python-versions = "*" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [[package]] -name = "pyasn1-modules" -version = "0.2.8" -description = "A collection of ASN.1-based protocols modules." -category = "main" +name = "py-cpuinfo" +version = "8.0.0" +description = "Get CPU info with pure Python 2 & 3" +category = "dev" optional = false python-versions = "*" -[package.dependencies] -pyasn1 = ">=0.4.6,<0.5.0" - [[package]] -name = "pycparser" -version = "2.21" -description = "C parser in Python" -category = "main" +name = "pycodestyle" +version = "2.8.0" +description = "Python style guide checker" +category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [[package]] name = "pydantic" -version = "1.10.4" -description = "Data validation and settings management using python type hints" +version = "1.9.0" +description = "Data validation and settings management using python 3.6 type hinting" category = "main" optional = false -python-versions = ">=3.7" +python-versions = ">=3.6.1" [package.dependencies] -typing-extensions = ">=4.2.0" +typing-extensions = ">=3.7.4.3" [package.extras] dotenv = ["python-dotenv (>=0.10.4)"] email = ["email-validator (>=1.0.3)"] [[package]] -name = "pydantic-factories" -version = "1.17.1" -description = "Mock data generation for pydantic based models and python dataclasses" -category = "main" -optional = false -python-versions = ">=3.8,<4.0" - -[package.dependencies] -faker = "*" -pydantic = ">=1.10.0" -typing-extensions = "*" - -[[package]] -name = "pydantic-openapi-schema" -version = "1.5.1" -description = "OpenAPI Schema using pydantic. Forked for Starlite-API from 'openapi-schema-pydantic'." -category = "main" +name = "pyflakes" +version = "2.4.0" +description = "passive checker of Python programs" +category = "dev" optional = false -python-versions = ">=3.8" - -[package.dependencies] -email-validator = "*" -pydantic = ">=1.10.0" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "pygments" -version = "2.14.0" +version = "2.11.2" description = "Pygments is a syntax highlighting package written in Python." category = "main" optional = false -python-versions = ">=3.6" - -[package.extras] -plugins = ["importlib-metadata"] +python-versions = ">=3.5" [[package]] -name = "pyopenssl" -version = "23.0.0" -description = "Python wrapper module around the OpenSSL library" -category = "main" +name = "pyparsing" +version = "3.0.7" +description = "Python parsing module" +category = "dev" optional = false python-versions = ">=3.6" -[package.dependencies] -cryptography = ">=38.0.0,<40" - [package.extras] -docs = ["sphinx (!=5.2.0,!=5.2.0.post0)", "sphinx-rtd-theme"] -test = ["flaky", "pretend", "pytest (>=3.0.1)"] - -[[package]] -name = "pyrsistent" -version = "0.19.3" -description = "Persistent/Functional/Immutable data structures" -category = "dev" -optional = false -python-versions = ">=3.7" +diagrams = ["jinja2", "railroad-diagrams"] [[package]] name = "pytest" -version = "7.2.1" +version = "7.0.1" description = "pytest: simple powerful testing with Python" category = "dev" optional = false -python-versions = ">=3.7" +python-versions = ">=3.6" [package.dependencies] +atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} attrs = ">=19.2.0" colorama = {version = "*", markers = "sys_platform == \"win32\""} -exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} iniconfig = "*" packaging = "*" pluggy = ">=0.12,<2.0" -tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} +py = ">=1.8.2" +tomli = ">=1.0.0" [package.extras] testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] @@ -1210,7 +957,7 @@ testing = ["coverage (==6.2)", "mypy (==0.931)"] [[package]] name = "pytest-asyncio" -version = "0.20.3" +version = "0.18.1" description = "Pytest support for asyncio" category = "dev" optional = false @@ -1221,16 +968,15 @@ pytest = ">=6.1.0" typing-extensions = {version = ">=3.7.2", markers = "python_version < \"3.8\""} [package.extras] -docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"] -testing = ["coverage (>=6.2)", "flaky (>=3.5.0)", "hypothesis (>=5.7.1)", "mypy (>=0.931)", "pytest-trio (>=0.7.0)"] +testing = ["coverage (==6.2)", "hypothesis (>=5.7.1)", "flaky (>=3.5.0)", "mypy (==0.931)"] [[package]] name = "pytest-benchmark" -version = "4.0.0" +version = "3.4.1" description = "A ``pytest`` fixture for benchmarking code. It will group the tests into rounds that are calibrated to the chosen timer." category = "dev" optional = false -python-versions = ">=3.7" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [package.dependencies] py-cpuinfo = "*" @@ -1243,7 +989,7 @@ histogram = ["pygal", "pygaljs"] [[package]] name = "pytest-cov" -version = "4.0.0" +version = "3.0.0" description = "Pytest plugin for measuring coverage." category = "dev" optional = false @@ -1254,7 +1000,7 @@ coverage = {version = ">=5.2.1", extras = ["toml"]} pytest = ">=4.6" [package.extras] -testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] +testing = ["fields", "hunter", "process-tests", "six", "pytest-xdist", "virtualenv"] [[package]] name = "pytest-django" @@ -1269,7 +1015,7 @@ pytest = ">=5.4.0" [package.extras] docs = ["sphinx", "sphinx-rtd-theme"] -testing = ["Django", "django-configurations (>=2.0)"] +testing = ["django", "django-configurations (>=2.0)"] [[package]] name = "pytest-emoji" @@ -1296,11 +1042,11 @@ pytest = ">=5.2" Werkzeug = ">=0.7" [package.extras] -docs = ["Sphinx", "sphinx-rtd-theme"] +docs = ["sphinx", "sphinx-rtd-theme"] [[package]] name = "pytest-mock" -version = "3.10.0" +version = "3.7.0" description = "Thin-wrapper around the mock package for easier use with pytest" category = "dev" optional = false @@ -1310,52 +1056,35 @@ python-versions = ">=3.7" pytest = ">=5.0" [package.extras] -dev = ["pre-commit", "pytest-asyncio", "tox"] +dev = ["pre-commit", "tox", "pytest-asyncio"] [[package]] name = "pytest-mypy-plugins" -version = "1.10.1" +version = "1.9.3" description = "pytest plugin for writing tests for mypy plugins" category = "dev" optional = false -python-versions = ">=3.7" +python-versions = ">=3.6" [package.dependencies] chevron = "*" decorator = "*" -mypy = ">=0.970" +mypy = ">=0.900" pytest = ">=6.0.0" pyyaml = "*" regex = "*" [[package]] -name = "pytest-snapshot" -version = "0.9.0" -description = "A plugin for snapshot testing with pytest." +name = "pytest-xprocess" +version = "0.18.1" +description = "A pytest plugin for managing processes across test runs." category = "dev" optional = false python-versions = ">=3.5" [package.dependencies] -pytest = ">=3.0.0" - -[[package]] -name = "pytest-xdist" -version = "3.1.0" -description = "pytest xdist plugin for distributed testing, most importantly across multiple CPUs" -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -execnet = ">=1.1" -psutil = {version = ">=3.0", optional = true, markers = "extra == \"psutil\""} -pytest = ">=6.2.0" - -[package.extras] -psutil = ["psutil (>=3.0)"] -setproctitle = ["setproctitle"] -testing = ["filelock"] +psutil = "*" +pytest = ">=2.8" [[package]] name = "python-dateutil" @@ -1389,7 +1118,7 @@ six = ">=1.4.0" [[package]] name = "pytz" -version = "2022.7" +version = "2021.3" description = "World timezone definitions, modern and historical" category = "main" optional = false @@ -1405,46 +1134,43 @@ python-versions = ">=3.6" [[package]] name = "readchar" -version = "4.0.3" -description = "Library to easily read single chars and key strokes" +version = "3.0.5" +description = "Utilities to read single characters and key-strokes" category = "main" optional = false -python-versions = ">=3.6" - -[package.dependencies] -setuptools = ">=41.0" +python-versions = "*" [[package]] name = "regex" -version = "2022.10.31" +version = "2022.1.18" description = "Alternative regular expression module, to replace re." category = "dev" optional = false -python-versions = ">=3.6" +python-versions = "*" [[package]] name = "requests" -version = "2.28.1" +version = "2.27.1" description = "Python HTTP for Humans." category = "dev" optional = false -python-versions = ">=3.7, <4" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" [package.dependencies] certifi = ">=2017.4.17" -charset-normalizer = ">=2,<3" -idna = ">=2.5,<4" +charset-normalizer = {version = ">=2.0.0,<2.1.0", markers = "python_version >= \"3\""} +idna = {version = ">=2.5,<4", markers = "python_version >= \"3\""} urllib3 = ">=1.21.1,<1.27" [package.extras] -socks = ["PySocks (>=1.5.6,!=1.5.7)"] -use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] +socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] +use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"] [[package]] name = "rfc3986" version = "1.5.0" description = "Validating URI References per RFC 3986" -category = "main" +category = "dev" optional = false python-versions = "*" @@ -1454,25 +1180,9 @@ idna = {version = "*", optional = true, markers = "extra == \"idna2008\""} [package.extras] idna2008 = ["idna"] -[[package]] -name = "rich" -version = "12.6.0" -description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" -category = "main" -optional = false -python-versions = ">=3.6.3,<4.0.0" - -[package.dependencies] -commonmark = ">=0.9.0,<0.10.0" -pygments = ">=2.6.0,<3.0.0" -typing-extensions = {version = ">=4.0.0,<5.0", markers = "python_version < \"3.9\""} - -[package.extras] -jupyter = ["ipywidgets (>=7.5.1,<8.0.0)"] - [[package]] name = "sanic" -version = "22.12.0" +version = "21.12.1" description = "A web server and web framework that's written to go fast. Build fast. Run fast." category = "main" optional = false @@ -1481,23 +1191,22 @@ python-versions = ">=3.7" [package.dependencies] aiofiles = ">=0.6.0" httptools = ">=0.0.10" -multidict = ">=5.0,<7.0" -sanic-routing = ">=22.8.0" +multidict = ">=5.0,<6.0" +sanic-routing = ">=0.7,<1.0" ujson = {version = ">=1.35", markers = "sys_platform != \"win32\" and implementation_name == \"cpython\""} -uvloop = {version = ">=0.15.0", markers = "sys_platform != \"win32\" and implementation_name == \"cpython\""} +uvloop = {version = ">=0.5.3", markers = "sys_platform != \"win32\" and implementation_name == \"cpython\""} websockets = ">=10.0" [package.extras] -all = ["bandit", "beautifulsoup4", "black", "chardet (>=3.0.0,<4.0.0)", "coverage", "cryptography", "docutils", "enum-tools[sphinx]", "flake8", "isort (>=5.0.0)", "m2r2", "mistune (<2.0.0)", "mypy (>=0.901,<0.910)", "pygments", "pytest (>=7.1.0,<7.2.0)", "pytest-benchmark", "pytest-sanic", "sanic-testing (>=22.9.0)", "slotscheck (>=0.8.0,<1)", "sphinx (>=2.1.2)", "sphinx-rtd-theme (>=0.4.3)", "towncrier", "tox", "types-ujson", "uvicorn (<0.15.0)"] -dev = ["bandit", "beautifulsoup4", "black", "chardet (>=3.0.0,<4.0.0)", "coverage", "cryptography", "docutils", "flake8", "isort (>=5.0.0)", "mypy (>=0.901,<0.910)", "pygments", "pytest (>=7.1.0,<7.2.0)", "pytest-benchmark", "pytest-sanic", "sanic-testing (>=22.9.0)", "slotscheck (>=0.8.0,<1)", "towncrier", "tox", "types-ujson", "uvicorn (<0.15.0)"] -docs = ["docutils", "enum-tools[sphinx]", "m2r2", "mistune (<2.0.0)", "pygments", "sphinx (>=2.1.2)", "sphinx-rtd-theme (>=0.4.3)"] +all = ["mistune (<2.0.0)", "flake8", "isort (>=5.0.0)", "sanic-testing (>=0.7.0)", "towncrier", "bandit", "m2r2", "mypy (>=0.901,<0.910)", "pytest-cov", "sphinx (>=2.1.2)", "coverage (==5.3)", "pytest-sugar", "cryptography", "tox", "beautifulsoup4", "black", "pytest-benchmark", "docutils", "gunicorn (==20.0.4)", "pytest-sanic", "uvicorn (<0.15.0)", "pygments", "chardet (>=3.0.0,<4.0.0)", "sphinx-rtd-theme (>=0.4.3)", "pytest (==6.2.5)", "types-ujson"] +dev = ["sanic-testing (>=0.7.0)", "pytest (==6.2.5)", "coverage (==5.3)", "gunicorn (==20.0.4)", "pytest-cov", "beautifulsoup4", "pytest-sanic", "pytest-sugar", "pytest-benchmark", "chardet (>=3.0.0,<4.0.0)", "flake8", "black", "isort (>=5.0.0)", "bandit", "mypy (>=0.901,<0.910)", "docutils", "pygments", "uvicorn (<0.15.0)", "cryptography", "tox", "towncrier", "types-ujson"] +docs = ["sphinx (>=2.1.2)", "sphinx-rtd-theme (>=0.4.3)", "docutils", "pygments", "m2r2", "mistune (<2.0.0)"] ext = ["sanic-ext"] -http3 = ["aioquic"] -test = ["bandit", "beautifulsoup4", "black", "chardet (>=3.0.0,<4.0.0)", "coverage", "docutils", "flake8", "isort (>=5.0.0)", "mypy (>=0.901,<0.910)", "pygments", "pytest (>=7.1.0,<7.2.0)", "pytest-benchmark", "pytest-sanic", "sanic-testing (>=22.9.0)", "slotscheck (>=0.8.0,<1)", "types-ujson", "uvicorn (<0.15.0)"] +test = ["sanic-testing (>=0.7.0)", "pytest (==6.2.5)", "coverage (==5.3)", "gunicorn (==20.0.4)", "pytest-cov", "beautifulsoup4", "pytest-sanic", "pytest-sugar", "pytest-benchmark", "chardet (>=3.0.0,<4.0.0)", "flake8", "black", "isort (>=5.0.0)", "bandit", "mypy (>=0.901,<0.910)", "docutils", "pygments", "uvicorn (<0.15.0)", "types-ujson"] [[package]] name = "sanic-routing" -version = "22.8.0" +version = "0.7.2" description = "Core routing component for Sanic" category = "main" optional = false @@ -1505,48 +1214,25 @@ python-versions = "*" [[package]] name = "sanic-testing" -version = "22.12.0" +version = "0.8.2" description = "Core testing clients for Sanic" category = "dev" optional = false python-versions = "*" [package.dependencies] -httpx = ">=0.18,<0.24" - -[[package]] -name = "service-identity" -version = "21.1.0" -description = "Service identity verification for pyOpenSSL & cryptography." -category = "main" -optional = false -python-versions = "*" - -[package.dependencies] -attrs = ">=19.1.0" -cryptography = "*" -pyasn1 = "*" -pyasn1-modules = "*" -six = "*" - -[package.extras] -dev = ["coverage[toml] (>=5.0.2)", "furo", "idna", "pyOpenSSL", "pytest", "sphinx"] -docs = ["furo", "sphinx"] -idna = ["idna"] -tests = ["coverage[toml] (>=5.0.2)", "pytest"] +httpx = ">=0.18,<0.22" [[package]] -name = "setuptools" -version = "65.6.3" -description = "Easily download, build, install, upgrade, and uninstall Python packages" +name = "sentinel" +version = "0.3.0" +description = "Create sentinel objects, akin to None, NotImplemented, Ellipsis" category = "main" optional = false -python-versions = ">=3.7" +python-versions = ">=3.6,<4.0" [package.extras] -docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"] -testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8 (<5)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] -testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] +varname = ["varname (>=0.1)"] [[package]] name = "six" @@ -1558,15 +1244,15 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" [[package]] name = "sniffio" -version = "1.3.0" +version = "1.2.0" description = "Sniff out which async library your code is running under" category = "main" optional = false -python-versions = ">=3.7" +python-versions = ">=3.5" [[package]] name = "sqlparse" -version = "0.4.3" +version = "0.4.2" description = "A non-validating SQL parser." category = "main" optional = false @@ -1574,135 +1260,51 @@ python-versions = ">=3.5" [[package]] name = "starlette" -version = "0.22.0" +version = "0.16.0" description = "The little ASGI library that shines." category = "main" optional = false -python-versions = ">=3.7" +python-versions = ">=3.6" [package.dependencies] -anyio = ">=3.4.0,<5" -typing-extensions = {version = ">=3.10.0", markers = "python_version < \"3.10\""} +anyio = ">=3.0.0,<4" +typing-extensions = {version = "*", markers = "python_version < \"3.8\""} [package.extras] -full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart", "pyyaml"] +full = ["itsdangerous", "jinja2", "python-multipart", "pyyaml", "requests", "graphene"] [[package]] -name = "starlite" -version = "1.50.2" -description = "Performant, light and flexible ASGI API Framework" -category = "main" +name = "testfixtures" +version = "6.18.3" +description = "A collection of helpers and mock objects for unit tests and doc tests." +category = "dev" optional = false -python-versions = ">=3.8,<4.0" - -[package.dependencies] -anyio = ">=3" -fast-query-parsers = "*" -httpx = ">=0.22" -importlib-metadata = {version = "*", markers = "python_version < \"3.10\""} -mako = ">=1.2.4" -msgspec = ">=0.11.0" -multidict = ">=6.0.2" -pydantic = "*" -pydantic-factories = "*" -pydantic-openapi-schema = ">=1.5.0" -pyyaml = "*" -typing-extensions = "*" +python-versions = "*" [package.extras] -brotli = ["brotli"] -cli = ["click", "jsbeautifier", "rich (>=13.0.0)"] -cryptography = ["cryptography"] -full = ["aiomcache", "brotli", "click", "cryptography", "jinja2 (>=3.1.2)", "opentelemetry-instrumentation-asgi", "picologging", "python-jose", "redis[hiredis]", "rich (>=13.0.0)", "structlog"] -jinja = ["jinja2 (>=3.1.2)"] -jwt = ["cryptography", "python-jose"] -memcached = ["aiomcache"] -opentelemetry = ["opentelemetry-instrumentation-asgi"] -picologging = ["picologging"] -redis = ["redis[hiredis]"] -standard = ["click", "jinja2 (>=3.1.2)", "jsbeautifier", "picologging", "rich (>=13.0.0)"] -structlog = ["structlog"] +build = ["setuptools-git", "wheel", "twine"] +docs = ["sphinx", "zope.component", "sybil", "twisted", "mock", "django (<2)", "django"] +test = ["pytest (>=3.6)", "pytest-cov", "pytest-django", "zope.component", "sybil", "twisted", "mock", "django (<2)", "django"] [[package]] -name = "tenacity" -version = "8.1.0" -description = "Retry code until it succeeds" +name = "toml" +version = "0.10.2" +description = "Python Library for Tom's Obvious, Minimal Language" category = "dev" optional = false -python-versions = ">=3.6" - -[package.extras] -doc = ["reno", "sphinx", "tornado (>=4.5)"] +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" [[package]] name = "tomli" -version = "2.0.1" +version = "1.2.3" description = "A lil' TOML parser" -category = "main" -optional = false -python-versions = ">=3.7" - -[[package]] -name = "twisted" -version = "22.4.0" -description = "An asynchronous networking framework written in Python" -category = "main" -optional = false -python-versions = ">=3.6.7" - -[package.dependencies] -attrs = ">=19.2.0" -Automat = ">=0.8.0" -constantly = ">=15.1" -hyperlink = ">=17.1.1" -idna = {version = ">=2.4", optional = true, markers = "extra == \"tls\""} -incremental = ">=21.3.0" -pyopenssl = {version = ">=16.0.0", optional = true, markers = "extra == \"tls\""} -service-identity = {version = ">=18.1.0", optional = true, markers = "extra == \"tls\""} -twisted-iocpsupport = {version = ">=1.0.2,<2", markers = "platform_system == \"Windows\""} -typing-extensions = ">=3.6.5" -"zope.interface" = ">=4.4.2" - -[package.extras] -all-non-platform = ["PyHamcrest (>=1.9.0)", "appdirs (>=1.4.0)", "bcrypt (>=3.0.0)", "contextvars (>=2.4,<3)", "cryptography (>=2.6)", "cython-test-exception-raiser (>=1.0.2,<2)", "h2 (>=3.0,<5.0)", "idna (>=2.4)", "priority (>=1.1.0,<2.0)", "pyasn1", "pyopenssl (>=16.0.0)", "pyserial (>=3.0)", "pywin32 (!=226)", "service-identity (>=18.1.0)"] -conch = ["appdirs (>=1.4.0)", "bcrypt (>=3.0.0)", "cryptography (>=2.6)", "pyasn1"] -conch-nacl = ["PyNaCl", "appdirs (>=1.4.0)", "bcrypt (>=3.0.0)", "cryptography (>=2.6)", "pyasn1"] -contextvars = ["contextvars (>=2.4,<3)"] -dev = ["coverage (>=6b1,<7)", "pydoctor (>=21.9.0,<21.10.0)", "pyflakes (>=2.2,<3.0)", "python-subunit (>=1.4,<2.0)", "readthedocs-sphinx-ext (>=2.1,<3.0)", "sphinx (>=4.1.2,<6)", "sphinx-rtd-theme (>=0.5,<1.0)", "towncrier (>=19.2,<20.0)", "twistedchecker (>=0.7,<1.0)"] -dev-release = ["pydoctor (>=21.9.0,<21.10.0)", "readthedocs-sphinx-ext (>=2.1,<3.0)", "sphinx (>=4.1.2,<6)", "sphinx-rtd-theme (>=0.5,<1.0)", "towncrier (>=19.2,<20.0)"] -http2 = ["h2 (>=3.0,<5.0)", "priority (>=1.1.0,<2.0)"] -macos-platform = ["PyHamcrest (>=1.9.0)", "appdirs (>=1.4.0)", "bcrypt (>=3.0.0)", "contextvars (>=2.4,<3)", "cryptography (>=2.6)", "cython-test-exception-raiser (>=1.0.2,<2)", "h2 (>=3.0,<5.0)", "idna (>=2.4)", "priority (>=1.1.0,<2.0)", "pyasn1", "pyobjc-core", "pyobjc-framework-CFNetwork", "pyobjc-framework-Cocoa", "pyopenssl (>=16.0.0)", "pyserial (>=3.0)", "pywin32 (!=226)", "service-identity (>=18.1.0)"] -mypy = ["PyHamcrest (>=1.9.0)", "PyNaCl", "appdirs (>=1.4.0)", "bcrypt (>=3.0.0)", "contextvars (>=2.4,<3)", "coverage (>=6b1,<7)", "cryptography (>=2.6)", "cython-test-exception-raiser (>=1.0.2,<2)", "h2 (>=3.0,<5.0)", "idna (>=2.4)", "mypy (==0.930)", "mypy-zope (==0.3.4)", "priority (>=1.1.0,<2.0)", "pyasn1", "pydoctor (>=21.9.0,<21.10.0)", "pyflakes (>=2.2,<3.0)", "pyopenssl (>=16.0.0)", "pyserial (>=3.0)", "python-subunit (>=1.4,<2.0)", "pywin32 (!=226)", "readthedocs-sphinx-ext (>=2.1,<3.0)", "service-identity (>=18.1.0)", "sphinx (>=4.1.2,<6)", "sphinx-rtd-theme (>=0.5,<1.0)", "towncrier (>=19.2,<20.0)", "twistedchecker (>=0.7,<1.0)", "types-pyOpenSSL", "types-setuptools"] -osx-platform = ["PyHamcrest (>=1.9.0)", "appdirs (>=1.4.0)", "bcrypt (>=3.0.0)", "contextvars (>=2.4,<3)", "cryptography (>=2.6)", "cython-test-exception-raiser (>=1.0.2,<2)", "h2 (>=3.0,<5.0)", "idna (>=2.4)", "priority (>=1.1.0,<2.0)", "pyasn1", "pyobjc-core", "pyobjc-framework-CFNetwork", "pyobjc-framework-Cocoa", "pyopenssl (>=16.0.0)", "pyserial (>=3.0)", "pywin32 (!=226)", "service-identity (>=18.1.0)"] -serial = ["pyserial (>=3.0)", "pywin32 (!=226)"] -test = ["PyHamcrest (>=1.9.0)", "cython-test-exception-raiser (>=1.0.2,<2)"] -tls = ["idna (>=2.4)", "pyopenssl (>=16.0.0)", "service-identity (>=18.1.0)"] -windows-platform = ["PyHamcrest (>=1.9.0)", "appdirs (>=1.4.0)", "bcrypt (>=3.0.0)", "contextvars (>=2.4,<3)", "cryptography (>=2.6)", "cython-test-exception-raiser (>=1.0.2,<2)", "h2 (>=3.0,<5.0)", "idna (>=2.4)", "priority (>=1.1.0,<2.0)", "pyasn1", "pyopenssl (>=16.0.0)", "pyserial (>=3.0)", "pywin32 (!=226)", "pywin32 (!=226)", "service-identity (>=18.1.0)"] - -[[package]] -name = "twisted-iocpsupport" -version = "1.0.2" -description = "An extension for use in the twisted I/O Completion Ports reactor." -category = "main" -optional = false -python-versions = "*" - -[[package]] -name = "txaio" -version = "22.2.1" -description = "Compatibility API between asyncio/Twisted/Trollius" -category = "main" +category = "dev" optional = false python-versions = ">=3.6" -[package.extras] -all = ["twisted (>=20.3.0)", "zope.interface (>=5.2.0)"] -dev = ["pep8 (>=1.6.2)", "pyenchant (>=1.6.6)", "pytest (>=2.6.4)", "pytest-cov (>=1.8.1)", "sphinx (>=1.2.3)", "sphinx-rtd-theme (>=0.1.9)", "sphinxcontrib-spelling (>=2.1.2)", "tox (>=2.1.1)", "tox-gh-actions (>=2.2.0)", "twine (>=1.6.5)", "wheel"] -twisted = ["twisted (>=20.3.0)", "zope.interface (>=5.2.0)"] - [[package]] name = "typed-ast" -version = "1.5.4" +version = "1.5.2" description = "a fork of Python 2 and 3 ast modules with type comment support" category = "dev" optional = false @@ -1710,7 +1312,7 @@ python-versions = ">=3.6" [[package]] name = "types-aiofiles" -version = "22.1.0.4" +version = "0.8.3" description = "Typing stubs for aiofiles" category = "dev" optional = false @@ -1718,7 +1320,7 @@ python-versions = "*" [[package]] name = "types-certifi" -version = "2021.10.8.3" +version = "2021.10.8.1" description = "Typing stubs for certifi" category = "dev" optional = false @@ -1726,7 +1328,7 @@ python-versions = "*" [[package]] name = "types-chardet" -version = "5.0.4.1" +version = "4.0.3" description = "Typing stubs for chardet" category = "dev" optional = false @@ -1734,7 +1336,7 @@ python-versions = "*" [[package]] name = "types-freezegun" -version = "1.1.10" +version = "1.1.6" description = "Typing stubs for freezegun" category = "dev" optional = false @@ -1742,7 +1344,7 @@ python-versions = "*" [[package]] name = "types-python-dateutil" -version = "2.8.19.6" +version = "2.8.9" description = "Typing stubs for python-dateutil" category = "dev" optional = false @@ -1750,7 +1352,7 @@ python-versions = "*" [[package]] name = "types-requests" -version = "2.28.11.8" +version = "2.27.10" description = "Typing stubs for requests" category = "dev" optional = false @@ -1759,9 +1361,17 @@ python-versions = "*" [package.dependencies] types-urllib3 = "<1.27" +[[package]] +name = "types-setuptools" +version = "57.4.9" +description = "Typing stubs for setuptools" +category = "dev" +optional = false +python-versions = "*" + [[package]] name = "types-toml" -version = "0.10.8.1" +version = "0.10.4" description = "Typing stubs for toml" category = "dev" optional = false @@ -1769,7 +1379,7 @@ python-versions = "*" [[package]] name = "types-typed-ast" -version = "1.5.8.3" +version = "1.5.2" description = "Typing stubs for typed-ast" category = "dev" optional = false @@ -1777,7 +1387,7 @@ python-versions = "*" [[package]] name = "types-ujson" -version = "5.7.0.0" +version = "4.2.1" description = "Typing stubs for ujson" category = "dev" optional = false @@ -1785,7 +1395,7 @@ python-versions = "*" [[package]] name = "types-urllib3" -version = "1.26.25.4" +version = "1.26.9" description = "Typing stubs for urllib3" category = "dev" optional = false @@ -1793,27 +1403,23 @@ python-versions = "*" [[package]] name = "typing-extensions" -version = "4.4.0" -description = "Backported and Experimental Type Hints for Python 3.7+" +version = "3.10.0.2" +description = "Backported and Experimental Type Hints for Python 3.5+" category = "main" optional = false -python-versions = ">=3.7" +python-versions = "*" [[package]] -name = "typing-inspect" -version = "0.8.0" -description = "Runtime inspection utilities for typing module." +name = "tzdata" +version = "2021.5" +description = "Provider of IANA time zone data" category = "main" optional = false -python-versions = "*" - -[package.dependencies] -mypy-extensions = ">=0.3.0" -typing-extensions = ">=3.7.4" +python-versions = ">=2" [[package]] name = "ujson" -version = "5.7.0" +version = "5.1.0" description = "Ultra fast JSON encoder and decoder for Python" category = "main" optional = false @@ -1821,45 +1427,65 @@ python-versions = ">=3.7" [[package]] name = "urllib3" -version = "1.26.13" +version = "1.26.8" description = "HTTP library with thread-safe connection pooling, file post, and more." category = "main" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" [package.extras] -brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"] -secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"] +brotli = ["brotlipy (>=0.6.0)"] +secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] [[package]] name = "uvicorn" -version = "0.20.0" +version = "0.17.5" description = "The lightning-fast ASGI server." category = "main" optional = false python-versions = ">=3.7" [package.dependencies] +asgiref = ">=3.4.0" click = ">=7.0" h11 = ">=0.8" typing-extensions = {version = "*", markers = "python_version < \"3.8\""} [package.extras] -standard = ["colorama (>=0.4)", "httptools (>=0.5.0)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "watchfiles (>=0.13)", "websockets (>=10.4)"] +standard = ["websockets (>=10.0)", "httptools (>=0.2.0,<0.4.0)", "watchgod (>=0.6)", "python-dotenv (>=0.13)", "PyYAML (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "colorama (>=0.4)"] [[package]] name = "uvloop" -version = "0.17.0" +version = "0.16.0" description = "Fast implementation of asyncio event loop on top of libuv" category = "main" optional = false python-versions = ">=3.7" [package.extras] -dev = ["Cython (>=0.29.32,<0.30.0)", "Sphinx (>=4.1.2,<4.2.0)", "aiohttp", "flake8 (>=3.9.2,<3.10.0)", "mypy (>=0.800)", "psutil", "pyOpenSSL (>=22.0.0,<22.1.0)", "pycodestyle (>=2.7.0,<2.8.0)", "pytest (>=3.6.0)", "sphinx-rtd-theme (>=0.5.2,<0.6.0)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)"] -docs = ["Sphinx (>=4.1.2,<4.2.0)", "sphinx-rtd-theme (>=0.5.2,<0.6.0)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)"] -test = ["Cython (>=0.29.32,<0.30.0)", "aiohttp", "flake8 (>=3.9.2,<3.10.0)", "mypy (>=0.800)", "psutil", "pyOpenSSL (>=22.0.0,<22.1.0)", "pycodestyle (>=2.7.0,<2.8.0)"] +dev = ["Cython (>=0.29.24,<0.30.0)", "pytest (>=3.6.0)", "Sphinx (>=4.1.2,<4.2.0)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)", "sphinx-rtd-theme (>=0.5.2,<0.6.0)", "aiohttp", "flake8 (>=3.9.2,<3.10.0)", "psutil", "pycodestyle (>=2.7.0,<2.8.0)", "pyOpenSSL (>=19.0.0,<19.1.0)", "mypy (>=0.800)"] +docs = ["Sphinx (>=4.1.2,<4.2.0)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)", "sphinx-rtd-theme (>=0.5.2,<0.6.0)"] +test = ["aiohttp", "flake8 (>=3.9.2,<3.10.0)", "psutil", "pycodestyle (>=2.7.0,<2.8.0)", "pyOpenSSL (>=19.0.0,<19.1.0)", "mypy (>=0.800)"] + +[[package]] +name = "virtualenv" +version = "20.13.1" +description = "Virtual Python Environment builder" +category = "dev" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" + +[package.dependencies] +distlib = ">=0.3.1,<1" +filelock = ">=3.2,<4" +importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} +platformdirs = ">=2,<3" +six = ">=1.9.0,<2" + +[package.extras] +docs = ["proselint (>=0.10.2)", "sphinx (>=3)", "sphinx-argparse (>=0.2.5)", "sphinx-rtd-theme (>=0.4.3)", "towncrier (>=21.3)"] +testing = ["coverage (>=4)", "coverage-enable-subprocess (>=1)", "flaky (>=3)", "pytest (>=4)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.1)", "pytest-mock (>=2)", "pytest-randomly (>=1)", "pytest-timeout (>=1)", "packaging (>=20.0)"] [[package]] name = "wcwidth" @@ -1871,7 +1497,7 @@ python-versions = "*" [[package]] name = "websockets" -version = "10.4" +version = "10.1" description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" category = "main" optional = false @@ -1879,52 +1505,31 @@ python-versions = ">=3.7" [[package]] name = "werkzeug" -version = "2.2.2" +version = "1.0.1" description = "The comprehensive WSGI web application library." category = "main" optional = false -python-versions = ">=3.7" - -[package.dependencies] -MarkupSafe = ">=2.1.1" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [package.extras] +dev = ["pytest", "pytest-timeout", "coverage", "tox", "sphinx", "pallets-sphinx-themes", "sphinx-issues"] watchdog = ["watchdog"] -[[package]] -name = "wheel" -version = "0.38.4" -description = "A built-package format for Python" -category = "main" -optional = false -python-versions = ">=3.7" - -[package.extras] -test = ["pytest (>=3.0.0)"] - [[package]] name = "wrapt" -version = "1.14.1" +version = "1.13.3" description = "Module for decorators, wrappers and monkey patching." category = "main" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" -[[package]] -name = "xmltodict" -version = "0.13.0" -description = "Makes working with XML feel like you are working with JSON" -category = "dev" -optional = false -python-versions = ">=3.4" - [[package]] name = "yarl" -version = "1.8.2" +version = "1.7.2" description = "Yet another URL library" category = "main" optional = false -python-versions = ">=3.7" +python-versions = ">=3.6" [package.dependencies] idna = ">=2.0" @@ -1933,42 +1538,23 @@ typing-extensions = {version = ">=3.7.4", markers = "python_version < \"3.8\""} [[package]] name = "zipp" -version = "3.11.0" +version = "3.7.0" description = "Backport of pathlib-compatible object wrapper for zip files" category = "main" optional = false python-versions = ">=3.7" [package.extras] -docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)"] -testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] - -[[package]] -name = "zope-interface" -version = "5.5.2" -description = "Interfaces for Python" -category = "main" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" - -[package.dependencies] -setuptools = "*" - -[package.extras] -docs = ["Sphinx", "repoze.sphinx.autointerface"] -test = ["coverage (>=5.0.3)", "zope.event", "zope.testing"] -testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"] +docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] +testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"] [extras] aiohttp = ["aiohttp"] -asgi = ["starlette", "python-multipart"] +asgi = ["starlette"] chalice = ["chalice"] -channels = ["channels", "asgiref"] -cli = ["click", "pygments", "rich", "libcst"] -debug = ["rich", "libcst"] -debug-server = ["starlette", "uvicorn", "python-multipart", "click", "pygments", "rich", "libcst"] +debug-server = ["starlette", "uvicorn"] django = ["Django", "asgiref"] -fastapi = ["fastapi", "python-multipart"] +fastapi = ["fastapi"] flask = ["flask"] opentelemetry = ["opentelemetry-api", "opentelemetry-sdk"] pydantic = ["pydantic"] @@ -1977,117 +1563,102 @@ sanic = ["sanic"] [metadata] lock-version = "1.1" python-versions = "^3.7" -content-hash = "9ab11fb40baefd4531bde445bf9b31500e7910bf58a3e47c176e17bd910bd6f7" +content-hash = "c4284f114029e73000eb677461f673deea778b52027a7b23ca9af72456a4bfa1" [metadata.files] aiofiles = [ - {file = "aiofiles-22.1.0-py3-none-any.whl", hash = "sha256:1142fa8e80dbae46bb6339573ad4c8c0841358f79c6eb50a493dceca14621bad"}, - {file = "aiofiles-22.1.0.tar.gz", hash = "sha256:9107f1ca0b2a5553987a94a3c9959fe5b491fdf731389aa5b7b1bd0733e32de6"}, + {file = "aiofiles-0.8.0-py3-none-any.whl", hash = "sha256:7a973fc22b29e9962d0897805ace5856e6a566ab1f0c8e5c91ff6c866519c937"}, + {file = "aiofiles-0.8.0.tar.gz", hash = "sha256:8334f23235248a3b2e83b2c3a78a22674f39969b96397126cc93664d9a901e59"}, ] aiohttp = [ - {file = "aiohttp-3.8.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ba71c9b4dcbb16212f334126cc3d8beb6af377f6703d9dc2d9fb3874fd667ee9"}, - {file = "aiohttp-3.8.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d24b8bb40d5c61ef2d9b6a8f4528c2f17f1c5d2d31fed62ec860f6006142e83e"}, - {file = "aiohttp-3.8.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f88df3a83cf9df566f171adba39d5bd52814ac0b94778d2448652fc77f9eb491"}, - {file = "aiohttp-3.8.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b97decbb3372d4b69e4d4c8117f44632551c692bb1361b356a02b97b69e18a62"}, - {file = "aiohttp-3.8.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:309aa21c1d54b8ef0723181d430347d7452daaff93e8e2363db8e75c72c2fb2d"}, - {file = "aiohttp-3.8.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ad5383a67514e8e76906a06741febd9126fc7c7ff0f599d6fcce3e82b80d026f"}, - {file = "aiohttp-3.8.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:20acae4f268317bb975671e375493dbdbc67cddb5f6c71eebdb85b34444ac46b"}, - {file = "aiohttp-3.8.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:05a3c31c6d7cd08c149e50dc7aa2568317f5844acd745621983380597f027a18"}, - {file = "aiohttp-3.8.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d6f76310355e9fae637c3162936e9504b4767d5c52ca268331e2756e54fd4ca5"}, - {file = "aiohttp-3.8.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:256deb4b29fe5e47893fa32e1de2d73c3afe7407738bd3c63829874661d4822d"}, - {file = "aiohttp-3.8.3-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:5c59fcd80b9049b49acd29bd3598cada4afc8d8d69bd4160cd613246912535d7"}, - {file = "aiohttp-3.8.3-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:059a91e88f2c00fe40aed9031b3606c3f311414f86a90d696dd982e7aec48142"}, - {file = "aiohttp-3.8.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2feebbb6074cdbd1ac276dbd737b40e890a1361b3cc30b74ac2f5e24aab41f7b"}, - {file = "aiohttp-3.8.3-cp310-cp310-win32.whl", hash = "sha256:5bf651afd22d5f0c4be16cf39d0482ea494f5c88f03e75e5fef3a85177fecdeb"}, - {file = "aiohttp-3.8.3-cp310-cp310-win_amd64.whl", hash = "sha256:653acc3880459f82a65e27bd6526e47ddf19e643457d36a2250b85b41a564715"}, - {file = "aiohttp-3.8.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:86fc24e58ecb32aee09f864cb11bb91bc4c1086615001647dbfc4dc8c32f4008"}, - {file = "aiohttp-3.8.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:75e14eac916f024305db517e00a9252714fce0abcb10ad327fb6dcdc0d060f1d"}, - {file = "aiohttp-3.8.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d1fde0f44029e02d02d3993ad55ce93ead9bb9b15c6b7ccd580f90bd7e3de476"}, - {file = "aiohttp-3.8.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ab94426ddb1ecc6a0b601d832d5d9d421820989b8caa929114811369673235c"}, - {file = "aiohttp-3.8.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:89d2e02167fa95172c017732ed7725bc8523c598757f08d13c5acca308e1a061"}, - {file = "aiohttp-3.8.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:02f9a2c72fc95d59b881cf38a4b2be9381b9527f9d328771e90f72ac76f31ad8"}, - {file = "aiohttp-3.8.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c7149272fb5834fc186328e2c1fa01dda3e1fa940ce18fded6d412e8f2cf76d"}, - {file = "aiohttp-3.8.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:512bd5ab136b8dc0ffe3fdf2dfb0c4b4f49c8577f6cae55dca862cd37a4564e2"}, - {file = "aiohttp-3.8.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:7018ecc5fe97027214556afbc7c502fbd718d0740e87eb1217b17efd05b3d276"}, - {file = "aiohttp-3.8.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:88c70ed9da9963d5496d38320160e8eb7e5f1886f9290475a881db12f351ab5d"}, - {file = "aiohttp-3.8.3-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:da22885266bbfb3f78218dc40205fed2671909fbd0720aedba39b4515c038091"}, - {file = "aiohttp-3.8.3-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:e65bc19919c910127c06759a63747ebe14f386cda573d95bcc62b427ca1afc73"}, - {file = "aiohttp-3.8.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:08c78317e950e0762c2983f4dd58dc5e6c9ff75c8a0efeae299d363d439c8e34"}, - {file = "aiohttp-3.8.3-cp311-cp311-win32.whl", hash = "sha256:45d88b016c849d74ebc6f2b6e8bc17cabf26e7e40c0661ddd8fae4c00f015697"}, - {file = "aiohttp-3.8.3-cp311-cp311-win_amd64.whl", hash = "sha256:96372fc29471646b9b106ee918c8eeb4cca423fcbf9a34daa1b93767a88a2290"}, - {file = "aiohttp-3.8.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:c971bf3786b5fad82ce5ad570dc6ee420f5b12527157929e830f51c55dc8af77"}, - {file = "aiohttp-3.8.3-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ff25f48fc8e623d95eca0670b8cc1469a83783c924a602e0fbd47363bb54aaca"}, - {file = "aiohttp-3.8.3-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e381581b37db1db7597b62a2e6b8b57c3deec95d93b6d6407c5b61ddc98aca6d"}, - {file = "aiohttp-3.8.3-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:db19d60d846283ee275d0416e2a23493f4e6b6028825b51290ac05afc87a6f97"}, - {file = "aiohttp-3.8.3-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:25892c92bee6d9449ffac82c2fe257f3a6f297792cdb18ad784737d61e7a9a85"}, - {file = "aiohttp-3.8.3-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:398701865e7a9565d49189f6c90868efaca21be65c725fc87fc305906be915da"}, - {file = "aiohttp-3.8.3-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:4a4fbc769ea9b6bd97f4ad0b430a6807f92f0e5eb020f1e42ece59f3ecfc4585"}, - {file = "aiohttp-3.8.3-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:b29bfd650ed8e148f9c515474a6ef0ba1090b7a8faeee26b74a8ff3b33617502"}, - {file = "aiohttp-3.8.3-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:1e56b9cafcd6531bab5d9b2e890bb4937f4165109fe98e2b98ef0dcfcb06ee9d"}, - {file = "aiohttp-3.8.3-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:ec40170327d4a404b0d91855d41bfe1fe4b699222b2b93e3d833a27330a87a6d"}, - {file = "aiohttp-3.8.3-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:2df5f139233060578d8c2c975128fb231a89ca0a462b35d4b5fcf7c501ebdbe1"}, - {file = "aiohttp-3.8.3-cp36-cp36m-win32.whl", hash = "sha256:f973157ffeab5459eefe7b97a804987876dd0a55570b8fa56b4e1954bf11329b"}, - {file = "aiohttp-3.8.3-cp36-cp36m-win_amd64.whl", hash = "sha256:437399385f2abcd634865705bdc180c8314124b98299d54fe1d4c8990f2f9494"}, - {file = "aiohttp-3.8.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:09e28f572b21642128ef31f4e8372adb6888846f32fecb288c8b0457597ba61a"}, - {file = "aiohttp-3.8.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f3553510abdbec67c043ca85727396ceed1272eef029b050677046d3387be8d"}, - {file = "aiohttp-3.8.3-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e168a7560b7c61342ae0412997b069753f27ac4862ec7867eff74f0fe4ea2ad9"}, - {file = "aiohttp-3.8.3-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:db4c979b0b3e0fa7e9e69ecd11b2b3174c6963cebadeecfb7ad24532ffcdd11a"}, - {file = "aiohttp-3.8.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e164e0a98e92d06da343d17d4e9c4da4654f4a4588a20d6c73548a29f176abe2"}, - {file = "aiohttp-3.8.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e8a78079d9a39ca9ca99a8b0ac2fdc0c4d25fc80c8a8a82e5c8211509c523363"}, - {file = "aiohttp-3.8.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:21b30885a63c3f4ff5b77a5d6caf008b037cb521a5f33eab445dc566f6d092cc"}, - {file = "aiohttp-3.8.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:4b0f30372cef3fdc262f33d06e7b411cd59058ce9174ef159ad938c4a34a89da"}, - {file = "aiohttp-3.8.3-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:8135fa153a20d82ffb64f70a1b5c2738684afa197839b34cc3e3c72fa88d302c"}, - {file = "aiohttp-3.8.3-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:ad61a9639792fd790523ba072c0555cd6be5a0baf03a49a5dd8cfcf20d56df48"}, - {file = "aiohttp-3.8.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:978b046ca728073070e9abc074b6299ebf3501e8dee5e26efacb13cec2b2dea0"}, - {file = "aiohttp-3.8.3-cp37-cp37m-win32.whl", hash = "sha256:0d2c6d8c6872df4a6ec37d2ede71eff62395b9e337b4e18efd2177de883a5033"}, - {file = "aiohttp-3.8.3-cp37-cp37m-win_amd64.whl", hash = "sha256:21d69797eb951f155026651f7e9362877334508d39c2fc37bd04ff55b2007091"}, - {file = "aiohttp-3.8.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:2ca9af5f8f5812d475c5259393f52d712f6d5f0d7fdad9acdb1107dd9e3cb7eb"}, - {file = "aiohttp-3.8.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d90043c1882067f1bd26196d5d2db9aa6d268def3293ed5fb317e13c9413ea4"}, - {file = "aiohttp-3.8.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d737fc67b9a970f3234754974531dc9afeea11c70791dcb7db53b0cf81b79784"}, - {file = "aiohttp-3.8.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ebf909ea0a3fc9596e40d55d8000702a85e27fd578ff41a5500f68f20fd32e6c"}, - {file = "aiohttp-3.8.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5835f258ca9f7c455493a57ee707b76d2d9634d84d5d7f62e77be984ea80b849"}, - {file = "aiohttp-3.8.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:da37dcfbf4b7f45d80ee386a5f81122501ec75672f475da34784196690762f4b"}, - {file = "aiohttp-3.8.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87f44875f2804bc0511a69ce44a9595d5944837a62caecc8490bbdb0e18b1342"}, - {file = "aiohttp-3.8.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:527b3b87b24844ea7865284aabfab08eb0faf599b385b03c2aa91fc6edd6e4b6"}, - {file = "aiohttp-3.8.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:d5ba88df9aa5e2f806650fcbeedbe4f6e8736e92fc0e73b0400538fd25a4dd96"}, - {file = "aiohttp-3.8.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:e7b8813be97cab8cb52b1375f41f8e6804f6507fe4660152e8ca5c48f0436017"}, - {file = "aiohttp-3.8.3-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:2dea10edfa1a54098703cb7acaa665c07b4e7568472a47f4e64e6319d3821ccf"}, - {file = "aiohttp-3.8.3-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:713d22cd9643ba9025d33c4af43943c7a1eb8547729228de18d3e02e278472b6"}, - {file = "aiohttp-3.8.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2d252771fc85e0cf8da0b823157962d70639e63cb9b578b1dec9868dd1f4f937"}, - {file = "aiohttp-3.8.3-cp38-cp38-win32.whl", hash = "sha256:66bd5f950344fb2b3dbdd421aaa4e84f4411a1a13fca3aeb2bcbe667f80c9f76"}, - {file = "aiohttp-3.8.3-cp38-cp38-win_amd64.whl", hash = "sha256:84b14f36e85295fe69c6b9789b51a0903b774046d5f7df538176516c3e422446"}, - {file = "aiohttp-3.8.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:16c121ba0b1ec2b44b73e3a8a171c4f999b33929cd2397124a8c7fcfc8cd9e06"}, - {file = "aiohttp-3.8.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8d6aaa4e7155afaf994d7924eb290abbe81a6905b303d8cb61310a2aba1c68ba"}, - {file = "aiohttp-3.8.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:43046a319664a04b146f81b40e1545d4c8ac7b7dd04c47e40bf09f65f2437346"}, - {file = "aiohttp-3.8.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:599418aaaf88a6d02a8c515e656f6faf3d10618d3dd95866eb4436520096c84b"}, - {file = "aiohttp-3.8.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:92a2964319d359f494f16011e23434f6f8ef0434acd3cf154a6b7bec511e2fb7"}, - {file = "aiohttp-3.8.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:73a4131962e6d91109bca6536416aa067cf6c4efb871975df734f8d2fd821b37"}, - {file = "aiohttp-3.8.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:598adde339d2cf7d67beaccda3f2ce7c57b3b412702f29c946708f69cf8222aa"}, - {file = "aiohttp-3.8.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:75880ed07be39beff1881d81e4a907cafb802f306efd6d2d15f2b3c69935f6fb"}, - {file = "aiohttp-3.8.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:a0239da9fbafd9ff82fd67c16704a7d1bccf0d107a300e790587ad05547681c8"}, - {file = "aiohttp-3.8.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:4e3a23ec214e95c9fe85a58470b660efe6534b83e6cbe38b3ed52b053d7cb6ad"}, - {file = "aiohttp-3.8.3-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:47841407cc89a4b80b0c52276f3cc8138bbbfba4b179ee3acbd7d77ae33f7ac4"}, - {file = "aiohttp-3.8.3-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:54d107c89a3ebcd13228278d68f1436d3f33f2dd2af5415e3feaeb1156e1a62c"}, - {file = "aiohttp-3.8.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c37c5cce780349d4d51739ae682dec63573847a2a8dcb44381b174c3d9c8d403"}, - {file = "aiohttp-3.8.3-cp39-cp39-win32.whl", hash = "sha256:f178d2aadf0166be4df834c4953da2d7eef24719e8aec9a65289483eeea9d618"}, - {file = "aiohttp-3.8.3-cp39-cp39-win_amd64.whl", hash = "sha256:88e5be56c231981428f4f506c68b6a46fa25c4123a2e86d156c58a8369d31ab7"}, - {file = "aiohttp-3.8.3.tar.gz", hash = "sha256:3828fb41b7203176b82fe5d699e0d845435f2374750a44b480ea6b930f6be269"}, + {file = "aiohttp-3.8.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:1ed0b6477896559f17b9eaeb6d38e07f7f9ffe40b9f0f9627ae8b9926ae260a8"}, + {file = "aiohttp-3.8.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7dadf3c307b31e0e61689cbf9e06be7a867c563d5a63ce9dca578f956609abf8"}, + {file = "aiohttp-3.8.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a79004bb58748f31ae1cbe9fa891054baaa46fb106c2dc7af9f8e3304dc30316"}, + {file = "aiohttp-3.8.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:12de6add4038df8f72fac606dff775791a60f113a725c960f2bab01d8b8e6b15"}, + {file = "aiohttp-3.8.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6f0d5f33feb5f69ddd57a4a4bd3d56c719a141080b445cbf18f238973c5c9923"}, + {file = "aiohttp-3.8.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eaba923151d9deea315be1f3e2b31cc39a6d1d2f682f942905951f4e40200922"}, + {file = "aiohttp-3.8.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:099ebd2c37ac74cce10a3527d2b49af80243e2a4fa39e7bce41617fbc35fa3c1"}, + {file = "aiohttp-3.8.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:2e5d962cf7e1d426aa0e528a7e198658cdc8aa4fe87f781d039ad75dcd52c516"}, + {file = "aiohttp-3.8.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:fa0ffcace9b3aa34d205d8130f7873fcfefcb6a4dd3dd705b0dab69af6712642"}, + {file = "aiohttp-3.8.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:61bfc23df345d8c9716d03717c2ed5e27374e0fe6f659ea64edcd27b4b044cf7"}, + {file = "aiohttp-3.8.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:31560d268ff62143e92423ef183680b9829b1b482c011713ae941997921eebc8"}, + {file = "aiohttp-3.8.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:01d7bdb774a9acc838e6b8f1d114f45303841b89b95984cbb7d80ea41172a9e3"}, + {file = "aiohttp-3.8.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:97ef77eb6b044134c0b3a96e16abcb05ecce892965a2124c566af0fd60f717e2"}, + {file = "aiohttp-3.8.1-cp310-cp310-win32.whl", hash = "sha256:c2aef4703f1f2ddc6df17519885dbfa3514929149d3ff900b73f45998f2532fa"}, + {file = "aiohttp-3.8.1-cp310-cp310-win_amd64.whl", hash = "sha256:713ac174a629d39b7c6a3aa757b337599798da4c1157114a314e4e391cd28e32"}, + {file = "aiohttp-3.8.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:473d93d4450880fe278696549f2e7aed8cd23708c3c1997981464475f32137db"}, + {file = "aiohttp-3.8.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99b5eeae8e019e7aad8af8bb314fb908dd2e028b3cdaad87ec05095394cce632"}, + {file = "aiohttp-3.8.1-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3af642b43ce56c24d063325dd2cf20ee012d2b9ba4c3c008755a301aaea720ad"}, + {file = "aiohttp-3.8.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c3630c3ef435c0a7c549ba170a0633a56e92629aeed0e707fec832dee313fb7a"}, + {file = "aiohttp-3.8.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:4a4a4e30bf1edcad13fb0804300557aedd07a92cabc74382fdd0ba6ca2661091"}, + {file = "aiohttp-3.8.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6f8b01295e26c68b3a1b90efb7a89029110d3a4139270b24fda961893216c440"}, + {file = "aiohttp-3.8.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:a25fa703a527158aaf10dafd956f7d42ac6d30ec80e9a70846253dd13e2f067b"}, + {file = "aiohttp-3.8.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:5bfde62d1d2641a1f5173b8c8c2d96ceb4854f54a44c23102e2ccc7e02f003ec"}, + {file = "aiohttp-3.8.1-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:51467000f3647d519272392f484126aa716f747859794ac9924a7aafa86cd411"}, + {file = "aiohttp-3.8.1-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:03a6d5349c9ee8f79ab3ff3694d6ce1cfc3ced1c9d36200cb8f08ba06bd3b782"}, + {file = "aiohttp-3.8.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:102e487eeb82afac440581e5d7f8f44560b36cf0bdd11abc51a46c1cd88914d4"}, + {file = "aiohttp-3.8.1-cp36-cp36m-win32.whl", hash = "sha256:4aed991a28ea3ce320dc8ce655875e1e00a11bdd29fe9444dd4f88c30d558602"}, + {file = "aiohttp-3.8.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b0e20cddbd676ab8a64c774fefa0ad787cc506afd844de95da56060348021e96"}, + {file = "aiohttp-3.8.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:37951ad2f4a6df6506750a23f7cbabad24c73c65f23f72e95897bb2cecbae676"}, + {file = "aiohttp-3.8.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c23b1ad869653bc818e972b7a3a79852d0e494e9ab7e1a701a3decc49c20d51"}, + {file = "aiohttp-3.8.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:15b09b06dae900777833fe7fc4b4aa426556ce95847a3e8d7548e2d19e34edb8"}, + {file = "aiohttp-3.8.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:477c3ea0ba410b2b56b7efb072c36fa91b1e6fc331761798fa3f28bb224830dd"}, + {file = "aiohttp-3.8.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:2f2f69dca064926e79997f45b2f34e202b320fd3782f17a91941f7eb85502ee2"}, + {file = "aiohttp-3.8.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ef9612483cb35171d51d9173647eed5d0069eaa2ee812793a75373447d487aa4"}, + {file = "aiohttp-3.8.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:6d69f36d445c45cda7b3b26afef2fc34ef5ac0cdc75584a87ef307ee3c8c6d00"}, + {file = "aiohttp-3.8.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:55c3d1072704d27401c92339144d199d9de7b52627f724a949fc7d5fc56d8b93"}, + {file = "aiohttp-3.8.1-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:b9d00268fcb9f66fbcc7cd9fe423741d90c75ee029a1d15c09b22d23253c0a44"}, + {file = "aiohttp-3.8.1-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:07b05cd3305e8a73112103c834e91cd27ce5b4bd07850c4b4dbd1877d3f45be7"}, + {file = "aiohttp-3.8.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:c34dc4958b232ef6188c4318cb7b2c2d80521c9a56c52449f8f93ab7bc2a8a1c"}, + {file = "aiohttp-3.8.1-cp37-cp37m-win32.whl", hash = "sha256:d2f9b69293c33aaa53d923032fe227feac867f81682f002ce33ffae978f0a9a9"}, + {file = "aiohttp-3.8.1-cp37-cp37m-win_amd64.whl", hash = "sha256:6ae828d3a003f03ae31915c31fa684b9890ea44c9c989056fea96e3d12a9fa17"}, + {file = "aiohttp-3.8.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:0c7ebbbde809ff4e970824b2b6cb7e4222be6b95a296e46c03cf050878fc1785"}, + {file = "aiohttp-3.8.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8b7ef7cbd4fec9a1e811a5de813311ed4f7ac7d93e0fda233c9b3e1428f7dd7b"}, + {file = "aiohttp-3.8.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c3d6a4d0619e09dcd61021debf7059955c2004fa29f48788a3dfaf9c9901a7cd"}, + {file = "aiohttp-3.8.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:718626a174e7e467f0558954f94af117b7d4695d48eb980146016afa4b580b2e"}, + {file = "aiohttp-3.8.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:589c72667a5febd36f1315aa6e5f56dd4aa4862df295cb51c769d16142ddd7cd"}, + {file = "aiohttp-3.8.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2ed076098b171573161eb146afcb9129b5ff63308960aeca4b676d9d3c35e700"}, + {file = "aiohttp-3.8.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:086f92daf51a032d062ec5f58af5ca6a44d082c35299c96376a41cbb33034675"}, + {file = "aiohttp-3.8.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:11691cf4dc5b94236ccc609b70fec991234e7ef8d4c02dd0c9668d1e486f5abf"}, + {file = "aiohttp-3.8.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:31d1e1c0dbf19ebccbfd62eff461518dcb1e307b195e93bba60c965a4dcf1ba0"}, + {file = "aiohttp-3.8.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:11a67c0d562e07067c4e86bffc1553f2cf5b664d6111c894671b2b8712f3aba5"}, + {file = "aiohttp-3.8.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:bb01ba6b0d3f6c68b89fce7305080145d4877ad3acaed424bae4d4ee75faa950"}, + {file = "aiohttp-3.8.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:44db35a9e15d6fe5c40d74952e803b1d96e964f683b5a78c3cc64eb177878155"}, + {file = "aiohttp-3.8.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:844a9b460871ee0a0b0b68a64890dae9c415e513db0f4a7e3cab41a0f2fedf33"}, + {file = "aiohttp-3.8.1-cp38-cp38-win32.whl", hash = "sha256:7d08744e9bae2ca9c382581f7dce1273fe3c9bae94ff572c3626e8da5b193c6a"}, + {file = "aiohttp-3.8.1-cp38-cp38-win_amd64.whl", hash = "sha256:04d48b8ce6ab3cf2097b1855e1505181bdd05586ca275f2505514a6e274e8e75"}, + {file = "aiohttp-3.8.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:f5315a2eb0239185af1bddb1abf472d877fede3cc8d143c6cddad37678293237"}, + {file = "aiohttp-3.8.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a996d01ca39b8dfe77440f3cd600825d05841088fd6bc0144cc6c2ec14cc5f74"}, + {file = "aiohttp-3.8.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:13487abd2f761d4be7c8ff9080de2671e53fff69711d46de703c310c4c9317ca"}, + {file = "aiohttp-3.8.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ea302f34477fda3f85560a06d9ebdc7fa41e82420e892fc50b577e35fc6a50b2"}, + {file = "aiohttp-3.8.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a2f635ce61a89c5732537a7896b6319a8fcfa23ba09bec36e1b1ac0ab31270d2"}, + {file = "aiohttp-3.8.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e999f2d0e12eea01caeecb17b653f3713d758f6dcc770417cf29ef08d3931421"}, + {file = "aiohttp-3.8.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0770e2806a30e744b4e21c9d73b7bee18a1cfa3c47991ee2e5a65b887c49d5cf"}, + {file = "aiohttp-3.8.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d15367ce87c8e9e09b0f989bfd72dc641bcd04ba091c68cd305312d00962addd"}, + {file = "aiohttp-3.8.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:6c7cefb4b0640703eb1069835c02486669312bf2f12b48a748e0a7756d0de33d"}, + {file = "aiohttp-3.8.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:71927042ed6365a09a98a6377501af5c9f0a4d38083652bcd2281a06a5976724"}, + {file = "aiohttp-3.8.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:28d490af82bc6b7ce53ff31337a18a10498303fe66f701ab65ef27e143c3b0ef"}, + {file = "aiohttp-3.8.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:b6613280ccedf24354406caf785db748bebbddcf31408b20c0b48cb86af76866"}, + {file = "aiohttp-3.8.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:81e3d8c34c623ca4e36c46524a3530e99c0bc95ed068fd6e9b55cb721d408fb2"}, + {file = "aiohttp-3.8.1-cp39-cp39-win32.whl", hash = "sha256:7187a76598bdb895af0adbd2fb7474d7f6025d170bc0a1130242da817ce9e7d1"}, + {file = "aiohttp-3.8.1-cp39-cp39-win_amd64.whl", hash = "sha256:1c182cb873bc91b411e184dab7a2b664d4fea2743df0e4d57402f7f3fa644bac"}, + {file = "aiohttp-3.8.1.tar.gz", hash = "sha256:fc5471e1a54de15ef71c1bc6ebe80d4dc681ea600e68bfd1cbce40427f0b7578"}, ] aiosignal = [ - {file = "aiosignal-1.3.1-py3-none-any.whl", hash = "sha256:f8376fb07dd1e86a584e4fcdec80b36b7f81aac666ebc724e2c090300dd83b17"}, - {file = "aiosignal-1.3.1.tar.gz", hash = "sha256:54cd96e15e1649b75d6c87526a6ff0b6c1b0dd3459f43d9ca11d48c339b68cfc"}, + {file = "aiosignal-1.2.0-py3-none-any.whl", hash = "sha256:26e62109036cd181df6e6ad646f91f0dcfd05fe16d0cb924138ff2ab75d64e3a"}, + {file = "aiosignal-1.2.0.tar.gz", hash = "sha256:78ed67db6c7b7ced4f98e495e572106d5c432a93e1ddd1bf475e1dc05f5b7df2"}, ] ansicon = [ {file = "ansicon-1.89.0-py2.py3-none-any.whl", hash = "sha256:f1def52d17f65c2c9682cf8370c03f541f410c1752d6a14029f97318e4b9dfec"}, {file = "ansicon-1.89.0.tar.gz", hash = "sha256:e4d039def5768a47e4afec8e89e83ec3ae5a26bf00ad851f914d1240b444d2b1"}, ] anyio = [ - {file = "anyio-3.6.2-py3-none-any.whl", hash = "sha256:fbbe32bd270d2a2ef3ed1c5d45041250284e31fc0a4df4a5a6071842051a51e3"}, - {file = "anyio-3.6.2.tar.gz", hash = "sha256:25ea0d673ae30af41a0c442f81cf3b38c7e79fdc7b60335a4c14e05eb0947421"}, + {file = "anyio-3.5.0-py3-none-any.whl", hash = "sha256:b5fa16c5ff93fa1046f2eeb5bbff2dad4d3514d6cda61d02816dba34fa8c3c2e"}, + {file = "anyio-3.5.0.tar.gz", hash = "sha256:a0aeffe2fb1fdf374a8e4b471444f0f3ac4fb9f5a5b542b48824475e0042a5a6"}, ] asgiref = [ - {file = "asgiref-3.6.0-py3-none-any.whl", hash = "sha256:71e68008da809b957b7ee4b43dbccff33d1b23519fb8344e33f049897077afac"}, - {file = "asgiref-3.6.0.tar.gz", hash = "sha256:9567dfe7bd8d3c8c892227827c41cce860b368104c3431da67a0c5a65a949506"}, + {file = "asgiref-3.5.0-py3-none-any.whl", hash = "sha256:88d59c13d634dcffe0510be048210188edd79aeccb6a6c9028cdad6f31d730a9"}, + {file = "asgiref-3.5.0.tar.gz", hash = "sha256:2f8abc20f7248433085eda803936d98992f1343ddb022065779f37c5da0181d0"}, ] async-timeout = [ {file = "async-timeout-4.0.2.tar.gz", hash = "sha256:2163e1640ddb52b7a8c80d0a67a08587e5d245cc9c553a74a847056bc2976b15"}, @@ -2097,311 +1668,100 @@ asynctest = [ {file = "asynctest-0.13.0-py3-none-any.whl", hash = "sha256:5da6118a7e6d6b54d83a8f7197769d046922a44d2a99c21382f0a6e4fadae676"}, {file = "asynctest-0.13.0.tar.gz", hash = "sha256:c27862842d15d83e6a34eb0b2866c323880eb3a75e4485b079ea11748fd77fac"}, ] +atomicwrites = [ + {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, + {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, +] attrs = [ {file = "attrs-21.4.0-py2.py3-none-any.whl", hash = "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4"}, {file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"}, ] -autobahn = [ - {file = "autobahn-22.12.1.tar.gz", hash = "sha256:43b4e8b1aeaeb20a0cc0a81572e613dc958057c0ab248a7d6b41b2763270f925"}, -] -automat = [ - {file = "Automat-22.10.0-py2.py3-none-any.whl", hash = "sha256:c3164f8742b9dc440f3682482d32aaff7bb53f71740dd018533f9de286b64180"}, - {file = "Automat-22.10.0.tar.gz", hash = "sha256:e56beb84edad19dcc11d30e8d9b895f75deeb5ef5e96b84a467066b3b84bb04e"}, -] -backports-cached-property = [ - {file = "backports.cached-property-1.0.2.tar.gz", hash = "sha256:9306f9eed6ec55fd156ace6bc1094e2c86fae5fb2bf07b6a9c00745c656e75dd"}, - {file = "backports.cached_property-1.0.2-py3-none-any.whl", hash = "sha256:baeb28e1cd619a3c9ab8941431fe34e8490861fb998c6c4590693d50171db0cc"}, +"backports.cached-property" = [ + {file = "backports.cached-property-1.0.1.tar.gz", hash = "sha256:1a5ef1e750f8bc7d0204c807aae8e0f450c655be0cf4b30407a35fd4bb27186c"}, + {file = "backports.cached_property-1.0.1-py3-none-any.whl", hash = "sha256:687b5fe14be40aadcf547cae91337a1fdb84026046a39370274e54d3fe4fb4f9"}, ] black = [ - {file = "black-22.12.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9eedd20838bd5d75b80c9f5487dbcb06836a43833a37846cf1d8c1cc01cef59d"}, - {file = "black-22.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:159a46a4947f73387b4d83e87ea006dbb2337eab6c879620a3ba52699b1f4351"}, - {file = "black-22.12.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d30b212bffeb1e252b31dd269dfae69dd17e06d92b87ad26e23890f3efea366f"}, - {file = "black-22.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:7412e75863aa5c5411886804678b7d083c7c28421210180d67dfd8cf1221e1f4"}, - {file = "black-22.12.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c116eed0efb9ff870ded8b62fe9f28dd61ef6e9ddd28d83d7d264a38417dcee2"}, - {file = "black-22.12.0-cp37-cp37m-win_amd64.whl", hash = "sha256:1f58cbe16dfe8c12b7434e50ff889fa479072096d79f0a7f25e4ab8e94cd8350"}, - {file = "black-22.12.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77d86c9f3db9b1bf6761244bc0b3572a546f5fe37917a044e02f3166d5aafa7d"}, - {file = "black-22.12.0-cp38-cp38-win_amd64.whl", hash = "sha256:82d9fe8fee3401e02e79767016b4907820a7dc28d70d137eb397b92ef3cc5bfc"}, - {file = "black-22.12.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:101c69b23df9b44247bd88e1d7e90154336ac4992502d4197bdac35dd7ee3320"}, - {file = "black-22.12.0-cp39-cp39-win_amd64.whl", hash = "sha256:559c7a1ba9a006226f09e4916060982fd27334ae1998e7a38b3f33a37f7a2148"}, - {file = "black-22.12.0-py3-none-any.whl", hash = "sha256:436cc9167dd28040ad90d3b404aec22cedf24a6e4d7de221bec2730ec0c97bcf"}, - {file = "black-22.12.0.tar.gz", hash = "sha256:229351e5a18ca30f447bf724d007f890f97e13af070bb6ad4c0a441cd7596a2f"}, + {file = "black-21.12b0-py3-none-any.whl", hash = "sha256:a615e69ae185e08fdd73e4715e260e2479c861b5740057fde6e8b4e3b7dd589f"}, + {file = "black-21.12b0.tar.gz", hash = "sha256:77b80f693a569e2e527958459634f18df9b0ba2625ba4e0c2d5da5be42e6f2b3"}, ] blessed = [ {file = "blessed-1.19.1-py2.py3-none-any.whl", hash = "sha256:63b8554ae2e0e7f43749b6715c734cc8f3883010a809bf16790102563e6cf25b"}, {file = "blessed-1.19.1.tar.gz", hash = "sha256:9a0d099695bf621d4680dd6c73f6ad547f6a3442fbdbe80c4b1daa1edbc492fc"}, ] botocore = [ - {file = "botocore-1.29.45-py3-none-any.whl", hash = "sha256:a5c0e13f266ee9a74335a1e5d3e377f2baae27226ae23d78f023bae0d18f3161"}, - {file = "botocore-1.29.45.tar.gz", hash = "sha256:62ae03e591ff25555854aa338da35190ffe18c0b1be2ebf5cfb277164233691f"}, -] -bytecode = [ - {file = "bytecode-0.13.0-py3-none-any.whl", hash = "sha256:e69f92e7d27f99d5d7d76e6a824bd3d9ff857c72b59927aaf87e1a620f67fe50"}, - {file = "bytecode-0.13.0.tar.gz", hash = "sha256:6af3c2f0a31ce05dce41f7eea5cc380e33f5e8fbb7dcee3b52467a00acd52fcd"}, - {file = "bytecode-0.14.0-py3-none-any.whl", hash = "sha256:f7b7cbed3239acee036d6c0f9d04286b100921114601bf844ae569b95bf91a9f"}, - {file = "bytecode-0.14.0.tar.gz", hash = "sha256:d41ad53c657ba0bef1cb4828d9d6e450766e31cb66c6f91fc1851f052889d1b7"}, -] -cattrs = [ - {file = "cattrs-22.2.0-py3-none-any.whl", hash = "sha256:bc12b1f0d000b9f9bee83335887d532a1d3e99a833d1bf0882151c97d3e68c21"}, - {file = "cattrs-22.2.0.tar.gz", hash = "sha256:f0eed5642399423cf656e7b66ce92cdc5b963ecafd041d1b24d136fdde7acf6d"}, + {file = "botocore-1.24.2-py3-none-any.whl", hash = "sha256:d1547d0bd8428df2dc1ad95b227e271914bb01f8edbd16a45766465c2090bc9b"}, + {file = "botocore-1.24.2.tar.gz", hash = "sha256:c017dd174285a07db4e1b844750c43550c1f51e8256e722f1e69030c4f6c78f1"}, ] certifi = [ - {file = "certifi-2022.12.7-py3-none-any.whl", hash = "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18"}, - {file = "certifi-2022.12.7.tar.gz", hash = "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3"}, -] -cffi = [ - {file = "cffi-1.15.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2"}, - {file = "cffi-1.15.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2"}, - {file = "cffi-1.15.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914"}, - {file = "cffi-1.15.1-cp27-cp27m-win32.whl", hash = "sha256:b3bbeb01c2b273cca1e1e0c5df57f12dce9a4dd331b4fa1635b8bec26350bde3"}, - {file = "cffi-1.15.1-cp27-cp27m-win_amd64.whl", hash = "sha256:e00b098126fd45523dd056d2efba6c5a63b71ffe9f2bbe1a4fe1716e1d0c331e"}, - {file = "cffi-1.15.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:d61f4695e6c866a23a21acab0509af1cdfd2c013cf256bbf5b6b5e2695827162"}, - {file = "cffi-1.15.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:ed9cb427ba5504c1dc15ede7d516b84757c3e3d7868ccc85121d9310d27eed0b"}, - {file = "cffi-1.15.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21"}, - {file = "cffi-1.15.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185"}, - {file = "cffi-1.15.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd"}, - {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc"}, - {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f"}, - {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e"}, - {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4"}, - {file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01"}, - {file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e"}, - {file = "cffi-1.15.1-cp310-cp310-win32.whl", hash = "sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2"}, - {file = "cffi-1.15.1-cp310-cp310-win_amd64.whl", hash = "sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d"}, - {file = "cffi-1.15.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac"}, - {file = "cffi-1.15.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83"}, - {file = "cffi-1.15.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9"}, - {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c"}, - {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325"}, - {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c"}, - {file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef"}, - {file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8"}, - {file = "cffi-1.15.1-cp311-cp311-win32.whl", hash = "sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d"}, - {file = "cffi-1.15.1-cp311-cp311-win_amd64.whl", hash = "sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104"}, - {file = "cffi-1.15.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50a74364d85fd319352182ef59c5c790484a336f6db772c1a9231f1c3ed0cbd7"}, - {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e263d77ee3dd201c3a142934a086a4450861778baaeeb45db4591ef65550b0a6"}, - {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cec7d9412a9102bdc577382c3929b337320c4c4c4849f2c5cdd14d7368c5562d"}, - {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4289fc34b2f5316fbb762d75362931e351941fa95fa18789191b33fc4cf9504a"}, - {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:173379135477dc8cac4bc58f45db08ab45d228b3363adb7af79436135d028405"}, - {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6975a3fac6bc83c4a65c9f9fcab9e47019a11d3d2cf7f3c0d03431bf145a941e"}, - {file = "cffi-1.15.1-cp36-cp36m-win32.whl", hash = "sha256:2470043b93ff09bf8fb1d46d1cb756ce6132c54826661a32d4e4d132e1977adf"}, - {file = "cffi-1.15.1-cp36-cp36m-win_amd64.whl", hash = "sha256:30d78fbc8ebf9c92c9b7823ee18eb92f2e6ef79b45ac84db507f52fbe3ec4497"}, - {file = "cffi-1.15.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375"}, - {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e"}, - {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82"}, - {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b"}, - {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c"}, - {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426"}, - {file = "cffi-1.15.1-cp37-cp37m-win32.whl", hash = "sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9"}, - {file = "cffi-1.15.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045"}, - {file = "cffi-1.15.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3"}, - {file = "cffi-1.15.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a"}, - {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5"}, - {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca"}, - {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02"}, - {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192"}, - {file = "cffi-1.15.1-cp38-cp38-win32.whl", hash = "sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314"}, - {file = "cffi-1.15.1-cp38-cp38-win_amd64.whl", hash = "sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5"}, - {file = "cffi-1.15.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585"}, - {file = "cffi-1.15.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0"}, - {file = "cffi-1.15.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415"}, - {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d"}, - {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984"}, - {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35"}, - {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27"}, - {file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76"}, - {file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3"}, - {file = "cffi-1.15.1-cp39-cp39-win32.whl", hash = "sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee"}, - {file = "cffi-1.15.1-cp39-cp39-win_amd64.whl", hash = "sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c"}, - {file = "cffi-1.15.1.tar.gz", hash = "sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9"}, + {file = "certifi-2021.10.8-py2.py3-none-any.whl", hash = "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569"}, + {file = "certifi-2021.10.8.tar.gz", hash = "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872"}, ] -chalice = [ - {file = "chalice-1.27.3-py3-none-any.whl", hash = "sha256:f41d0eaac1edb70ce1904b234bfc6ad4af35b7321f79b998bc6a95ff5f8cb4f6"}, - {file = "chalice-1.27.3.tar.gz", hash = "sha256:58ccf0ada3727de1b93fd1d5c6e9e17d92a3e929ff37cb14cfaa6fc9e729c92e"}, +cfgv = [ + {file = "cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"}, + {file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"}, ] -channels = [ - {file = "channels-3.0.5-py3-none-any.whl", hash = "sha256:3813b8025bf85509769793aca720e6c3b1c5bde1cb253a961252bf0242b60a26"}, - {file = "channels-3.0.5.tar.gz", hash = "sha256:a3dc3339cc033e7c2afe083fb3dedf74fc5009815967e317e080e7bfdc92ea26"}, +chalice = [ + {file = "chalice-1.26.6-py3-none-any.whl", hash = "sha256:eeacdbe8979a13420870c3dd1647e2152727ccc8b6a58e53ddab4d1f93ff63c1"}, + {file = "chalice-1.26.6.tar.gz", hash = "sha256:ad89eb648c6ab3976190385ab423d4e790b9d5e8449a5cde47b392d63599ff43"}, ] charset-normalizer = [ - {file = "charset-normalizer-2.1.1.tar.gz", hash = "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845"}, - {file = "charset_normalizer-2.1.1-py3-none-any.whl", hash = "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f"}, + {file = "charset-normalizer-2.0.12.tar.gz", hash = "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597"}, + {file = "charset_normalizer-2.0.12-py3-none-any.whl", hash = "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df"}, ] chevron = [ {file = "chevron-0.14.0-py3-none-any.whl", hash = "sha256:fbf996a709f8da2e745ef763f482ce2d311aa817d287593a5b990d6d6e4f0443"}, {file = "chevron-0.14.0.tar.gz", hash = "sha256:87613aafdf6d77b6a90ff073165a61ae5086e21ad49057aa0e53681601800ebf"}, ] click = [ - {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, - {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, + {file = "click-7.1.2-py2.py3-none-any.whl", hash = "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"}, + {file = "click-7.1.2.tar.gz", hash = "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a"}, ] colorama = [ - {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, - {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, -] -commonmark = [ - {file = "commonmark-0.9.1-py2.py3-none-any.whl", hash = "sha256:da2f38c92590f83de410ba1a3cbceafbc74fee9def35f9251ba9a971d6d66fd9"}, - {file = "commonmark-0.9.1.tar.gz", hash = "sha256:452f9dc859be7f06631ddcb328b6919c67984aca654e5fefb3914d54691aed60"}, -] -constantly = [ - {file = "constantly-15.1.0-py2.py3-none-any.whl", hash = "sha256:dd2fa9d6b1a51a83f0d7dd76293d734046aa176e384bf6e33b7e44880eb37c5d"}, - {file = "constantly-15.1.0.tar.gz", hash = "sha256:586372eb92059873e29eba4f9dec8381541b4d3834660707faf8ba59146dfc35"}, + {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, + {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, ] coverage = [ - {file = "coverage-7.0.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:daf91db39324e9939a9db919ee4fb42a1a23634a056616dae891a030e89f87ba"}, - {file = "coverage-7.0.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:55121fe140d7e42cb970999b93cf1c2b24484ce028b32bbd00238bb25c13e34a"}, - {file = "coverage-7.0.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c027fbb83a8c78a6e06a0302ea1799fdb70e5cda9845a5e000545b8e2b47ea39"}, - {file = "coverage-7.0.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:caf82db5b7f16b51ec32fe0bd2da0805b177c807aa8bfb478c7e6f893418c284"}, - {file = "coverage-7.0.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ba5cc54baf3c322c4388de2a43cc95f7809366f0600e743e5aae8ea9d1038b2"}, - {file = "coverage-7.0.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:260854160083f8275a9d9d49a05ab0ffc7a1f08f2ccccbfaec94a18aae9f407c"}, - {file = "coverage-7.0.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:ea45f0dba5a993e93b158f1a9dcfff2770e3bcabf2b80dbe7aa15dce0bcb3bf3"}, - {file = "coverage-7.0.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6abc91f6f8b3cc0ae1034e2c03f38769fba1952ab70d0b26953aa01691265c39"}, - {file = "coverage-7.0.4-cp310-cp310-win32.whl", hash = "sha256:053cdc47cae08257051d7e934a0de4d095b60eb8a3024fa9f1b2322fa1547137"}, - {file = "coverage-7.0.4-cp310-cp310-win_amd64.whl", hash = "sha256:1e9e94f2612ee549a4b3ee79cbc61bceed77e69cf38cfa05858bae939a886d16"}, - {file = "coverage-7.0.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5caa9dd91dcc5f054350dc57a02e053d79633907b9ccffff999568d13dcd19f8"}, - {file = "coverage-7.0.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:efc200fa75d9634525b40babc7a16342bd21c101db1a58ef84dc14f4bf6ac0fd"}, - {file = "coverage-7.0.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1791e5f74c5b52f76e83fe9f4bb9571cf76d40ee0c51952ee1e4ee935b7e98b9"}, - {file = "coverage-7.0.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3d9201cfa5a98652b9cef36ab202f17fe3ea83f497b4ba2a8ed39399dfb8fcd4"}, - {file = "coverage-7.0.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22d8ef6865cb6834cab2b72fff20747a55c714b57b675f7e11c9624fe4f7cb45"}, - {file = "coverage-7.0.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b84076e3de192fba0f95e279ac017b64c7c6ecd4f09f36f13420f5bed898a9c7"}, - {file = "coverage-7.0.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:dcfbf8ffc046f20d75fd775a92c378f6fc7b9bded6c6f2ab88b6b9cb5805a184"}, - {file = "coverage-7.0.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:4665a714af31f160403c2e448fb2fef330719d2e04e836b08d60d612707c1041"}, - {file = "coverage-7.0.4-cp311-cp311-win32.whl", hash = "sha256:2e59aef3fba5758059208c9eff10ae7ded3629e797972746ec33b56844f69411"}, - {file = "coverage-7.0.4-cp311-cp311-win_amd64.whl", hash = "sha256:2b854f7985b48122b6fe346631e86d67b63293f8255cb59a93d79e3d9f1574e3"}, - {file = "coverage-7.0.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e44b60b0b49aa85d548d392a2dca2c6a581cd4084e72e9e16bd58bd86ec20816"}, - {file = "coverage-7.0.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2904d7a0388911c61e7e3beefe48c29dfccaba938fc1158f63190101a21e04c2"}, - {file = "coverage-7.0.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bc74b64bfa89e2f862ea45dd6ac1def371d7cc883b76680d20bdd61a6f3daa20"}, - {file = "coverage-7.0.4-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c06046f54e719da21c79f98ecc0962581d1aee0b3798dc6b12b1217da8bf93f4"}, - {file = "coverage-7.0.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:bc9c77004970a364a1e5454cf7cb884e4277592b959c287689b2a0fd027ef552"}, - {file = "coverage-7.0.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:0815a09b32384e8ff00a5939ec9cd10efce8742347e019c2daca1a32f5ac2aae"}, - {file = "coverage-7.0.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:a78a80d131c067d67d8a6f9bd3d3f7ea7eac82c1c7259f97d7ab73f723da9d55"}, - {file = "coverage-7.0.4-cp37-cp37m-win32.whl", hash = "sha256:2b5936b624fbe711ed02dfd86edd678822e5ee68da02b6d231e5c01090b64590"}, - {file = "coverage-7.0.4-cp37-cp37m-win_amd64.whl", hash = "sha256:a63922765ee49d5b4c32afb2cd5516812c8665f3b78e64a0dd005bdfabf991b1"}, - {file = "coverage-7.0.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d68f2f7bddb3acdd3b36ef7f334b9d14f30b93e094f808fbbd8d288b8f9e2f9b"}, - {file = "coverage-7.0.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9dafdba3b2b9010abab08cb8c0dc6549bfca6e1630fe14d47b01dca00d39e694"}, - {file = "coverage-7.0.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0322354757b47640535daabd2d56384ff3cad2896248fc84d328c5fad4922d5c"}, - {file = "coverage-7.0.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4e8267466662aff93d66fa72b9591d02122dfc8a729b0a43dd70e0fb07ed9b37"}, - {file = "coverage-7.0.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f684d88eb4924ed0630cf488fd5606e334c6835594bb5fe36b50a509b10383ed"}, - {file = "coverage-7.0.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:70c294bb15ba576fb96b580db35895bf03749d683df044212b74e938a7f6821f"}, - {file = "coverage-7.0.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:34c0457e1ba450ae8b22dc8ea2fd36ada1010af61291e4c96963cd9d9633366f"}, - {file = "coverage-7.0.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b75aff2c35ceaa299691e772f7bf7c8aeab25f46acea2be3dd04cccb914a9860"}, - {file = "coverage-7.0.4-cp38-cp38-win32.whl", hash = "sha256:6c5554d55668381e131577f20e8f620d4882b04ad558f7e7f3f1f55b3124c379"}, - {file = "coverage-7.0.4-cp38-cp38-win_amd64.whl", hash = "sha256:c82f34fafaf5bc05d222fcf84423d6e156432ca35ca78672d4affd0c09c6ef6c"}, - {file = "coverage-7.0.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b8dfb5fed540f77e814bf4ec79619c241af6b4578fa1093c5e3389bbb7beab3f"}, - {file = "coverage-7.0.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ee32a080bab779b71c4d09a3eb5254bfca43ee88828a683dab27dfe8f582516e"}, - {file = "coverage-7.0.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2dfbee0bf0d633be3a2ab068f5a5731a70adf147d0ba17d9f9932b46c7c5782b"}, - {file = "coverage-7.0.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:32dc010713455ac0fe2fddb0e48aa43875cc7eb7b09768df10bad8ce45f9c430"}, - {file = "coverage-7.0.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9cb88a3019ad042eaa69fc7639ef077793fedbf313e89207aa82fefe92c97ebd"}, - {file = "coverage-7.0.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:73bc6114aab7753ca784f87bcd3b7613bc797aa255b5bca45e5654070ae9acfb"}, - {file = "coverage-7.0.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:92f135d370fcd7a6fb9659fa2eb716dd2ca364719cbb1756f74d90a221bca1a7"}, - {file = "coverage-7.0.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:f3d485e6ec6e09857bf2115ece572d666b7c498377d4c70e66bb06c63ed177c2"}, - {file = "coverage-7.0.4-cp39-cp39-win32.whl", hash = "sha256:c58921fcd9914b56444292e7546fe183d079db99528142c809549ddeaeacd8e9"}, - {file = "coverage-7.0.4-cp39-cp39-win_amd64.whl", hash = "sha256:f092d9f2ddaa30235d33335fbdb61eb8f3657af519ef5f9dd6bdae65272def11"}, - {file = "coverage-7.0.4-pp37.pp38.pp39-none-any.whl", hash = "sha256:cb8cfa3bf3a9f18211279458917fef5edeb5e1fdebe2ea8b11969ec2ebe48884"}, - {file = "coverage-7.0.4.tar.gz", hash = "sha256:f6c4ad409a0caf7e2e12e203348b1a9b19c514e7d078520973147bf2d3dcbc6f"}, -] -cryptography = [ - {file = "cryptography-39.0.0-cp36-abi3-macosx_10_12_universal2.whl", hash = "sha256:c52a1a6f81e738d07f43dab57831c29e57d21c81a942f4602fac7ee21b27f288"}, - {file = "cryptography-39.0.0-cp36-abi3-macosx_10_12_x86_64.whl", hash = "sha256:80ee674c08aaef194bc4627b7f2956e5ba7ef29c3cc3ca488cf15854838a8f72"}, - {file = "cryptography-39.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:887cbc1ea60786e534b00ba8b04d1095f4272d380ebd5f7a7eb4cc274710fad9"}, - {file = "cryptography-39.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f97109336df5c178ee7c9c711b264c502b905c2d2a29ace99ed761533a3460f"}, - {file = "cryptography-39.0.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a6915075c6d3a5e1215eab5d99bcec0da26036ff2102a1038401d6ef5bef25b"}, - {file = "cryptography-39.0.0-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:76c24dd4fd196a80f9f2f5405a778a8ca132f16b10af113474005635fe7e066c"}, - {file = "cryptography-39.0.0-cp36-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:bae6c7f4a36a25291b619ad064a30a07110a805d08dc89984f4f441f6c1f3f96"}, - {file = "cryptography-39.0.0-cp36-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:875aea1039d78557c7c6b4db2fe0e9d2413439f4676310a5f269dd342ca7a717"}, - {file = "cryptography-39.0.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:f6c0db08d81ead9576c4d94bbb27aed8d7a430fa27890f39084c2d0e2ec6b0df"}, - {file = "cryptography-39.0.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:f3ed2d864a2fa1666e749fe52fb8e23d8e06b8012e8bd8147c73797c506e86f1"}, - {file = "cryptography-39.0.0-cp36-abi3-win32.whl", hash = "sha256:f671c1bb0d6088e94d61d80c606d65baacc0d374e67bf895148883461cd848de"}, - {file = "cryptography-39.0.0-cp36-abi3-win_amd64.whl", hash = "sha256:e324de6972b151f99dc078defe8fb1b0a82c6498e37bff335f5bc6b1e3ab5a1e"}, - {file = "cryptography-39.0.0-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:754978da4d0457e7ca176f58c57b1f9de6556591c19b25b8bcce3c77d314f5eb"}, - {file = "cryptography-39.0.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ee1fd0de9851ff32dbbb9362a4d833b579b4a6cc96883e8e6d2ff2a6bc7104f"}, - {file = "cryptography-39.0.0-pp38-pypy38_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:fec8b932f51ae245121c4671b4bbc030880f363354b2f0e0bd1366017d891458"}, - {file = "cryptography-39.0.0-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:407cec680e811b4fc829de966f88a7c62a596faa250fc1a4b520a0355b9bc190"}, - {file = "cryptography-39.0.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:7dacfdeee048814563eaaec7c4743c8aea529fe3dd53127313a792f0dadc1773"}, - {file = "cryptography-39.0.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:ad04f413436b0781f20c52a661660f1e23bcd89a0e9bb1d6d20822d048cf2856"}, - {file = "cryptography-39.0.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50386acb40fbabbceeb2986332f0287f50f29ccf1497bae31cf5c3e7b4f4b34f"}, - {file = "cryptography-39.0.0-pp39-pypy39_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:e5d71c5d5bd5b5c3eebcf7c5c2bb332d62ec68921a8c593bea8c394911a005ce"}, - {file = "cryptography-39.0.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:844ad4d7c3850081dffba91cdd91950038ee4ac525c575509a42d3fc806b83c8"}, - {file = "cryptography-39.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:e0a05aee6a82d944f9b4edd6a001178787d1546ec7c6223ee9a848a7ade92e39"}, - {file = "cryptography-39.0.0.tar.gz", hash = "sha256:f964c7dcf7802d133e8dbd1565914fa0194f9d683d82411989889ecd701e8adf"}, -] -daphne = [ - {file = "daphne-3.0.2-py3-none-any.whl", hash = "sha256:a9af943c79717bc52fe64a3c236ae5d3adccc8b5be19c881b442d2c3db233393"}, - {file = "daphne-3.0.2.tar.gz", hash = "sha256:76ffae916ba3aa66b46996c14fa713e46004788167a4873d647544e750e0e99f"}, -] -ddsketch = [ - {file = "ddsketch-2.0.4-py3-none-any.whl", hash = "sha256:3227a270fd686a29d3a7128f9352ccf852314410380fc11384356f1ae2a75938"}, - {file = "ddsketch-2.0.4.tar.gz", hash = "sha256:32f7314077fec8747d4faebaec2c854b5ffc399c5f552f73fa94024f48d74d64"}, -] -ddtrace = [ - {file = "ddtrace-1.7.3-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:1e726effe78e4bc0d84a0eededf9759d317a7c4512e69e9ab792705f9c6b598e"}, - {file = "ddtrace-1.7.3-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:e2f7bcfd6de024bf07f12df7ce390f42a11e03becadf2897d838fbc5615aa031"}, - {file = "ddtrace-1.7.3-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:58f24ab127db17f9ecf3394d3872841dadc2f324bdafc25a256237153b9b1d2a"}, - {file = "ddtrace-1.7.3-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:d6fc3ba9b31520b443ce1ddafa5324eb6991bbd06fc10e77247e50d0fd2b9b28"}, - {file = "ddtrace-1.7.3-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:0186999705d294ccee23fe542846ff1344c8f292549e8c001e168d43468710e8"}, - {file = "ddtrace-1.7.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3fe3e9a0b93f97fb62d6f1ce0936ba188e8361006e420ddce377d1b3ae0540ae"}, - {file = "ddtrace-1.7.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:02c9b314dbe1e15eac2556a7b4eb516e3db77feadd987f0adb746dcebc1d4198"}, - {file = "ddtrace-1.7.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bcc2512ef6e95fa94de91b51a61b67ea199dfd6443ff534e4b59a70f2b18eca4"}, - {file = "ddtrace-1.7.3-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66f11c8dda7e4fc78a5a9510b1683fcc01ccfe3fd43be3d5c9f22697ca224638"}, - {file = "ddtrace-1.7.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:344e3004fc9ecbf5a75fe577185a8f5cbec22fb8f43a28b8cf0a6a2d873e257c"}, - {file = "ddtrace-1.7.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b37bae36be217d4b842467d9d9846361042ce8662f4851662f92b9ec63122d77"}, - {file = "ddtrace-1.7.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:931c0eb57a03d88d2435443ad8b63f8726e0f354c526e344c8ed7ee3fb865c56"}, - {file = "ddtrace-1.7.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:66ececdf0933b7ae079340f86b01852579aa16718536fe4542c8daf5144c7a27"}, - {file = "ddtrace-1.7.3-cp310-cp310-win32.whl", hash = "sha256:d6da4ec523fd2c4d8fada3833207e7a5d7e3b6de42d6219f9b68bb3d5b7e78e5"}, - {file = "ddtrace-1.7.3-cp310-cp310-win_amd64.whl", hash = "sha256:1fa030752ca7abb9f79d573c0287647defefdcbe1b09d20671674f9e03d815bd"}, - {file = "ddtrace-1.7.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:92bc6211cc15107a17a39954271e41f36123c0e29fb7810870caa239f43dfb34"}, - {file = "ddtrace-1.7.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d516664981edea92483b801a51f4a4de09c3125ab7a76a12f4481a6688839039"}, - {file = "ddtrace-1.7.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b61a3036dd6f22cbda7c18b7211d52703a21acfd6e62da8815d399eaa5eadcda"}, - {file = "ddtrace-1.7.3-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e833f0038dd8854dce0862cb11c0817dd0d72ef6e6660a0c90af525cbf050c02"}, - {file = "ddtrace-1.7.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9020056895f88a8e21918cf242ed265016698a1097edcf1bd503c969b6e579d3"}, - {file = "ddtrace-1.7.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d0fca95990b2ffefce0751142fe443ffff972d89a3e8b7326c76ef832254751a"}, - {file = "ddtrace-1.7.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:556abb226487856ccfe8f748d60473b9e77ed4479c4cdacd018a48a4c7ddf2f5"}, - {file = "ddtrace-1.7.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:97dcbf5979c9824ca797f79db4d0f69a27b7e2d28cfe5582dd0e3f67eaa5007a"}, - {file = "ddtrace-1.7.3-cp311-cp311-win32.whl", hash = "sha256:0889c8750d0a5d2f8f19513ff073b124ccdb4050a36c66c587a6c414dd898845"}, - {file = "ddtrace-1.7.3-cp311-cp311-win_amd64.whl", hash = "sha256:261dc98238ffe866dd023cd1e73ee2484b6b7274ae642990e5a9e5bd80afc1e7"}, - {file = "ddtrace-1.7.3-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:c7df0c8904db4e43d4589b6c230e4615b88a0552fef4aa0f83934128e3ce892f"}, - {file = "ddtrace-1.7.3-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:4a8f925acd1632eb8bd0e718630f080dfea889f23407f0af29ad0df6dba71280"}, - {file = "ddtrace-1.7.3-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:fa9d96e03add3e6a34f9be276e1e7535a9b122b8f39b770369acef41cdbde2b5"}, - {file = "ddtrace-1.7.3-cp35-cp35m-win32.whl", hash = "sha256:742a4d0f3bf548121b21b503b301c57c65f5c317038edbe044e957e6035214fd"}, - {file = "ddtrace-1.7.3-cp35-cp35m-win_amd64.whl", hash = "sha256:51359493c1ba46312067f2b344672630f44232fe63236c1f4bc85ba0ad23dc11"}, - {file = "ddtrace-1.7.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:841e56c6745a79b731c1959bfc6aea4934406cc80885b799d18b18d7d5650874"}, - {file = "ddtrace-1.7.3-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f5f84d989dda7d0c05136b19d2657dd4f49d9e8cc494d3088dd4307b7a41a9a"}, - {file = "ddtrace-1.7.3-cp36-cp36m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:73b8650510d605fbe5e943832e2393b6cf810e0474b75ad955e7709b9d98adc2"}, - {file = "ddtrace-1.7.3-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b890ed56bdb15dba8c1715a82a933d8e1987a48382d8ae68bd7ab97812327d0"}, - {file = "ddtrace-1.7.3-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:9933bfc81f7828a0c51a6eb9685555a0375feacdd464ef1af543fbaba28479d6"}, - {file = "ddtrace-1.7.3-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:258fd26a63062a3dea873ff276b01bbc7a6f12b393be9d2f3efbac4cb1f130c5"}, - {file = "ddtrace-1.7.3-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:732ed05e303c3f154940cf4747a38feb916aa6feb5dd35597a0a73e4fdc78dc7"}, - {file = "ddtrace-1.7.3-cp36-cp36m-win32.whl", hash = "sha256:659ad91e93ea884aa599fdc5a3156d2069064b58e788515f924ccc73f0f4b2c2"}, - {file = "ddtrace-1.7.3-cp36-cp36m-win_amd64.whl", hash = "sha256:41c64de7f1dc748054ba5253c7d13acc60e997e6ceb9f1dc781e61d8f97a8ff2"}, - {file = "ddtrace-1.7.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:7fb92f407784ef0dea0893c26d7a343955bb273e40508a21265b2c6e7626cd68"}, - {file = "ddtrace-1.7.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f9a2f871e8aedf84e958c84ab2d77dd0e2f7b3b8e1a56aa22c12646a9ecac96"}, - {file = "ddtrace-1.7.3-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9fbeac2f45b88c8257eb597c1cb5eee29e880f55749dcf9d62e3e5c992d2fdb6"}, - {file = "ddtrace-1.7.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d74ab36f8a1996a30775b4a73f53ddcba3cb4b25767162eab51d5fd28c06996"}, - {file = "ddtrace-1.7.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:a7602aa71321ddeb93443373ca0bccff14862b9435c40fbc10fdd8bbac3b29e5"}, - {file = "ddtrace-1.7.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:0c529ad2e918a5a2424383222f962f8efbc8630ac5e20c84bd3368e721d18cce"}, - {file = "ddtrace-1.7.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:99e5d63f7f71d382028220dab7a79ae430fc6e440d4623f48dab043e87f16c45"}, - {file = "ddtrace-1.7.3-cp37-cp37m-win32.whl", hash = "sha256:b51d3a00b9cebcd09c59e088d2c4742100b2fdd3cfd0eb097d570fe33d356cb8"}, - {file = "ddtrace-1.7.3-cp37-cp37m-win_amd64.whl", hash = "sha256:b372213026359b21b3c0b656e3ede3a92d29a21c5d2beece1f753f0315a15aa1"}, - {file = "ddtrace-1.7.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:9b0adb25ba7b373ac4d7066f0c6e3a5106e324834a1d62c2f58910a299900551"}, - {file = "ddtrace-1.7.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:32d424fbc907f95ddedfcc4cb9aeec123184933845269681facf723ea4076a22"}, - {file = "ddtrace-1.7.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:567e282f2ccf0d841477ae10b9c9b41b04259fd45a6a761e75a8b94e4e1a3b18"}, - {file = "ddtrace-1.7.3-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:43035451055051e3716742faceea314363d83d774fecc5f81717e10a4e86bb84"}, - {file = "ddtrace-1.7.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:354d9845b4ddf9590fff64f304dc3b75f4b37f41d1eee8dd61ab50382807d692"}, - {file = "ddtrace-1.7.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:67e41b0d041ccc3ad8da7911220c019318853122453eb07706c7467685639f52"}, - {file = "ddtrace-1.7.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:d7876ef74684c12687d3d32ba27dd7c14ef76871a71f4a0af807a2aa26733b77"}, - {file = "ddtrace-1.7.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:1ab948c969c0036b56e767a3228aa68f2fdef2ee06b7a0e7282377e0beea2159"}, - {file = "ddtrace-1.7.3-cp38-cp38-win32.whl", hash = "sha256:29c9b7fbe7b62bf7c0b48a120a65a7c2b54a3ac3ded372cf65d5c06e60c87839"}, - {file = "ddtrace-1.7.3-cp38-cp38-win_amd64.whl", hash = "sha256:b78c0f37d1004bb2b8bab0e17e923110ffed0d01c4b1739bbeb0ccd574b42269"}, - {file = "ddtrace-1.7.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:981ca5385826415dd4f88e23a88e23b7fe9b7c19cb07da9885b16e17c721a514"}, - {file = "ddtrace-1.7.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:44a63a453d18e47536f4a15ff3a477b077a1f3bfb4eee4a76e56813d659d62cf"}, - {file = "ddtrace-1.7.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:57ecbd85e33ae7f9598f6c4432cc2dee4bff2f566e32c179a199c702267b287c"}, - {file = "ddtrace-1.7.3-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:df20802776f35832124fad61d8d6166fba1b6a8258db54a4988446545b559d05"}, - {file = "ddtrace-1.7.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f5ce56610d234368cbdd41b5b7a584394f2d1f162b5919fb47d51fc67cad5e5"}, - {file = "ddtrace-1.7.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:adcc7e5173aece83947f7f39457cdb5d552e5938e89aff5d7850a33a17372ccb"}, - {file = "ddtrace-1.7.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:ed583b97a1ce941953053d07c0ad3ee6cd1a871d498efd5bb7ae92baaaf04ae7"}, - {file = "ddtrace-1.7.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:88496de4d1e746a6a80c850e13e4c86dbf80c0df1f5de1d7f96ee26292290ac8"}, - {file = "ddtrace-1.7.3-cp39-cp39-win32.whl", hash = "sha256:e482694dedfc457b946c955dcfd9bbff09469da7c45ce8188a39821a18d291c5"}, - {file = "ddtrace-1.7.3-cp39-cp39-win_amd64.whl", hash = "sha256:1849ce4dc99884e2562478483aa2a0e339fe27bbc76fa83bd02c5a6074137d8a"}, - {file = "ddtrace-1.7.3.tar.gz", hash = "sha256:cfa935d6eaaf774b40c46f74fdbc1c4125f4111a0f8dc49fc8c1d6a68724427b"}, + {file = "coverage-6.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:eeffd96882d8c06d31b65dddcf51db7c612547babc1c4c5db6a011abe9798525"}, + {file = "coverage-6.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:621f6ea7260ea2ffdaec64fe5cb521669984f567b66f62f81445221d4754df4c"}, + {file = "coverage-6.3.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:84f2436d6742c01136dd940ee158bfc7cf5ced3da7e4c949662b8703b5cd8145"}, + {file = "coverage-6.3.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:de73fca6fb403dd72d4da517cfc49fcf791f74eee697d3219f6be29adf5af6ce"}, + {file = "coverage-6.3.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78fbb2be068a13a5d99dce9e1e7d168db880870f7bc73f876152130575bd6167"}, + {file = "coverage-6.3.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:f5a4551dfd09c3bd12fca8144d47fe7745275adf3229b7223c2f9e29a975ebda"}, + {file = "coverage-6.3.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7bff3a98f63b47464480de1b5bdd80c8fade0ba2832c9381253c9b74c4153c27"}, + {file = "coverage-6.3.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a06c358f4aed05fa1099c39decc8022261bb07dfadc127c08cfbd1391b09689e"}, + {file = "coverage-6.3.1-cp310-cp310-win32.whl", hash = "sha256:9fff3ff052922cb99f9e52f63f985d4f7a54f6b94287463bc66b7cdf3eb41217"}, + {file = "coverage-6.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:276b13cc085474e482566c477c25ed66a097b44c6e77132f3304ac0b039f83eb"}, + {file = "coverage-6.3.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:56c4a409381ddd7bbff134e9756077860d4e8a583d310a6f38a2315b9ce301d0"}, + {file = "coverage-6.3.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9eb494070aa060ceba6e4bbf44c1bc5fa97bfb883a0d9b0c9049415f9e944793"}, + {file = "coverage-6.3.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5e15d424b8153756b7c903bde6d4610be0c3daca3986173c18dd5c1a1625e4cd"}, + {file = "coverage-6.3.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61d47a897c1e91f33f177c21de897267b38fbb45f2cd8e22a710bcef1df09ac1"}, + {file = "coverage-6.3.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:25e73d4c81efa8ea3785274a2f7f3bfbbeccb6fcba2a0bdd3be9223371c37554"}, + {file = "coverage-6.3.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:fac0bcc5b7e8169bffa87f0dcc24435446d329cbc2b5486d155c2e0f3b493ae1"}, + {file = "coverage-6.3.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:72128176fea72012063200b7b395ed8a57849282b207321124d7ff14e26988e8"}, + {file = "coverage-6.3.1-cp37-cp37m-win32.whl", hash = "sha256:1bc6d709939ff262fd1432f03f080c5042dc6508b6e0d3d20e61dd045456a1a0"}, + {file = "coverage-6.3.1-cp37-cp37m-win_amd64.whl", hash = "sha256:618eeba986cea7f621d8607ee378ecc8c2504b98b3fdc4952b30fe3578304687"}, + {file = "coverage-6.3.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d5ed164af5c9078596cfc40b078c3b337911190d3faeac830c3f1274f26b8320"}, + {file = "coverage-6.3.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:352c68e233409c31048a3725c446a9e48bbff36e39db92774d4f2380d630d8f8"}, + {file = "coverage-6.3.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:448d7bde7ceb6c69e08474c2ddbc5b4cd13c9e4aa4a717467f716b5fc938a734"}, + {file = "coverage-6.3.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9fde6b90889522c220dd56a670102ceef24955d994ff7af2cb786b4ba8fe11e4"}, + {file = "coverage-6.3.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e647a0be741edbb529a72644e999acb09f2ad60465f80757da183528941ff975"}, + {file = "coverage-6.3.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6a5cdc3adb4f8bb8d8f5e64c2e9e282bc12980ef055ec6da59db562ee9bdfefa"}, + {file = "coverage-6.3.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:2dd70a167843b4b4b2630c0c56f1b586fe965b4f8ac5da05b6690344fd065c6b"}, + {file = "coverage-6.3.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:9ad0a117b8dc2061ce9461ea4c1b4799e55edceb236522c5b8f958ce9ed8fa9a"}, + {file = "coverage-6.3.1-cp38-cp38-win32.whl", hash = "sha256:e92c7a5f7d62edff50f60a045dc9542bf939758c95b2fcd686175dd10ce0ed10"}, + {file = "coverage-6.3.1-cp38-cp38-win_amd64.whl", hash = "sha256:482fb42eea6164894ff82abbcf33d526362de5d1a7ed25af7ecbdddd28fc124f"}, + {file = "coverage-6.3.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c5b81fb37db76ebea79aa963b76d96ff854e7662921ce742293463635a87a78d"}, + {file = "coverage-6.3.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a4f923b9ab265136e57cc14794a15b9dcea07a9c578609cd5dbbfff28a0d15e6"}, + {file = "coverage-6.3.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:56d296cbc8254a7dffdd7bcc2eb70be5a233aae7c01856d2d936f5ac4e8ac1f1"}, + {file = "coverage-6.3.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1245ab82e8554fa88c4b2ab1e098ae051faac5af829efdcf2ce6b34dccd5567c"}, + {file = "coverage-6.3.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f2b05757c92ad96b33dbf8e8ec8d4ccb9af6ae3c9e9bd141c7cc44d20c6bcba"}, + {file = "coverage-6.3.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9e3dd806f34de38d4c01416344e98eab2437ac450b3ae39c62a0ede2f8b5e4ed"}, + {file = "coverage-6.3.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:d651fde74a4d3122e5562705824507e2f5b2d3d57557f1916c4b27635f8fbe3f"}, + {file = "coverage-6.3.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:704f89b87c4f4737da2860695a18c852b78ec7279b24eedacab10b29067d3a38"}, + {file = "coverage-6.3.1-cp39-cp39-win32.whl", hash = "sha256:2aed4761809640f02e44e16b8b32c1a5dee5e80ea30a0ff0912158bde9c501f2"}, + {file = "coverage-6.3.1-cp39-cp39-win_amd64.whl", hash = "sha256:9976fb0a5709988778ac9bc44f3d50fccd989987876dfd7716dee28beed0a9fa"}, + {file = "coverage-6.3.1-pp36.pp37.pp38-none-any.whl", hash = "sha256:463e52616ea687fd323888e86bf25e864a3cc6335a043fad6bbb037dbf49bbe2"}, + {file = "coverage-6.3.1.tar.gz", hash = "sha256:6c3f6158b02ac403868eea390930ae64e9a9a2a5bbfafefbb920d29258d9f2f8"}, ] decorator = [ {file = "decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186"}, @@ -2411,669 +1771,501 @@ deprecated = [ {file = "Deprecated-1.2.13-py2.py3-none-any.whl", hash = "sha256:64756e3e14c8c5eea9795d93c524551432a0be75629f8f29e67ab8caf076c76d"}, {file = "Deprecated-1.2.13.tar.gz", hash = "sha256:43ac5335da90c31c24ba028af536a91d41d53f9e6901ddb021bcc572ce44e38d"}, ] +distlib = [ + {file = "distlib-0.3.4-py2.py3-none-any.whl", hash = "sha256:6564fe0a8f51e734df6333d08b8b94d4ea8ee6b99b5ed50613f731fd4089f34b"}, + {file = "distlib-0.3.4.zip", hash = "sha256:e4b58818180336dc9c529bfb9a0b58728ffc09ad92027a3f30b7cd91e3458579"}, +] django = [ - {file = "Django-3.2.16-py3-none-any.whl", hash = "sha256:18ba8efa36b69cfcd4b670d0fa187c6fe7506596f0ababe580e16909bcdec121"}, - {file = "Django-3.2.16.tar.gz", hash = "sha256:3adc285124244724a394fa9b9839cc8cd116faf7d159554c43ecdaa8cdf0b94d"}, + {file = "Django-3.2.12-py3-none-any.whl", hash = "sha256:9b06c289f9ba3a8abea16c9c9505f25107809fb933676f6c891ded270039d965"}, + {file = "Django-3.2.12.tar.gz", hash = "sha256:9772e6935703e59e993960832d66a614cf0233a1c5123bc6224ecc6ad69e41e2"}, + {file = "Django-4.0.2-py3-none-any.whl", hash = "sha256:996495c58bff749232426c88726d8cd38d24c94d7c1d80835aafffa9bc52985a"}, + {file = "Django-4.0.2.tar.gz", hash = "sha256:110fb58fb12eca59e072ad59fc42d771cd642dd7a2f2416582aa9da7a8ef954a"}, ] dnspython = [ - {file = "dnspython-2.2.1-py3-none-any.whl", hash = "sha256:a851e51367fb93e9e1361732c1d60dab63eff98712e503ea7d92e6eccb109b4f"}, - {file = "dnspython-2.2.1.tar.gz", hash = "sha256:0f7569a4a6ff151958b64304071d370daa3243d15941a7beedf0c9fe5105603e"}, + {file = "dnspython-2.2.0-py3-none-any.whl", hash = "sha256:081649da27ced5e75709a1ee542136eaba9842a0fe4c03da4fb0a3d3ed1f3c44"}, + {file = "dnspython-2.2.0.tar.gz", hash = "sha256:e79351e032d0b606b98d38a4b0e6e2275b31a5b85c873e587cc11b73aca026d6"}, ] email-validator = [ - {file = "email_validator-1.3.1-py2.py3-none-any.whl", hash = "sha256:49a72f5fa6ed26be1c964f0567d931d10bf3fdeeacdf97bc26ef1cd2a44e0bda"}, - {file = "email_validator-1.3.1.tar.gz", hash = "sha256:d178c5c6fa6c6824e9b04f199cf23e79ac15756786573c190d2ad13089411ad2"}, -] -envier = [ - {file = "envier-0.4.0-py3-none-any.whl", hash = "sha256:7b91af0f16ea3e56d91ec082f038987e81b441fc19c657a8b8afe0909740a706"}, - {file = "envier-0.4.0.tar.gz", hash = "sha256:e68dcd1ed67d8b6313883e27dff3e701b7fba944d2ed4b7f53d0cc2e12364a82"}, -] -exceptiongroup = [ - {file = "exceptiongroup-1.1.0-py3-none-any.whl", hash = "sha256:327cbda3da756e2de031a3107b81ab7b3770a602c4d16ca618298c526f4bec1e"}, - {file = "exceptiongroup-1.1.0.tar.gz", hash = "sha256:bcb67d800a4497e1b404c2dd44fca47d3b7a5e5433dbab67f96c1a685cdfdf23"}, -] -execnet = [ - {file = "execnet-1.9.0-py2.py3-none-any.whl", hash = "sha256:a295f7cc774947aac58dde7fdc85f4aa00c42adf5d8f5468fc630c1acf30a142"}, - {file = "execnet-1.9.0.tar.gz", hash = "sha256:8f694f3ba9cc92cab508b152dcfe322153975c29bda272e2fd7f3f00f36e47c5"}, -] -faker = [ - {file = "Faker-16.6.1-py3-none-any.whl", hash = "sha256:2375d0bbaf405dc4f1cbc771485a78ad952c776798e5c228eef3e7b337f78868"}, - {file = "Faker-16.6.1.tar.gz", hash = "sha256:b76e5d2405470e3d38d37d1bfaa9d9bbf171bdf41c814f5bbd8117b121f6bccb"}, -] -fast-query-parsers = [ - {file = "fast_query_parsers-0.3.0-cp38-abi3-macosx_10_7_x86_64.whl", hash = "sha256:e01e9b794d6fad11207341a8e0a7b179f085c9874b3981a77f862a5e44cb241d"}, - {file = "fast_query_parsers-0.3.0-cp38-abi3-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:f2ffa7f1298db8ff8e013733e32edb77509a468843f5641875386f1df9bdecb4"}, - {file = "fast_query_parsers-0.3.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1becd3a0a06b14c57ee34976bf54d08fc910d4b410170ee5c28ac8d16ef23575"}, - {file = "fast_query_parsers-0.3.0-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:dd94f7715b6c1ec57b50e63c22298253648d864c9e78f90a258ebf2c07d13a38"}, - {file = "fast_query_parsers-0.3.0-cp38-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:52ef0241bcadc93ef47f650070c5f31f616cd5185a3af95f8b365e1fb135d991"}, - {file = "fast_query_parsers-0.3.0-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:189eed1007229c1e4a032076d503e92869eebd4a1ebd2498e6bb1b1b0c525708"}, - {file = "fast_query_parsers-0.3.0-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97d13dcc6cad29c8ef7c6f4bb6240847b476e4eb0445ac7f59d49a11db1644e2"}, - {file = "fast_query_parsers-0.3.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:befa91ff707f77e9948759bafdfe2b743d4b587e3f7767f2b5887b1ffa37f41d"}, - {file = "fast_query_parsers-0.3.0-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:00d3d519cfd020c7a3bd961c9568b6042f853f25e204b8a307ff5b569dee9c38"}, - {file = "fast_query_parsers-0.3.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:a659eb2acc0aa44f4cce7a79c4e0c76f14a26026fc13db8f094a29a38cf66248"}, - {file = "fast_query_parsers-0.3.0-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:e9b277e8281d7fe9b8e9450767cf1c1158ed729d81e03c1dfa2fe2118b22ffe0"}, - {file = "fast_query_parsers-0.3.0-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:0395ef9cacf49318a0f3caac4fde8a01dd7b7315c14f8a7d084965dedd764868"}, - {file = "fast_query_parsers-0.3.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:47639fc331e668b1be405c4e24af70ad21fc01a6847d2730803346ab9586e788"}, - {file = "fast_query_parsers-0.3.0-cp38-abi3-win32.whl", hash = "sha256:89ee582ab4b331f078eca2fbf51ce82396f749879fc30f20668b85a2896ea165"}, - {file = "fast_query_parsers-0.3.0-cp38-abi3-win_amd64.whl", hash = "sha256:4e9dd270f085a2337fab407d230a252664f952561cb83acece343a5c6a7d8e6b"}, - {file = "fast_query_parsers-0.3.0.tar.gz", hash = "sha256:df972c0b58d0bf51fa43b67d2604ab795984015d47552d02175ebcc685e4852b"}, + {file = "email_validator-1.1.3-py2.py3-none-any.whl", hash = "sha256:5675c8ceb7106a37e40e2698a57c056756bf3f272cfa8682a4f87ebd95d8440b"}, + {file = "email_validator-1.1.3.tar.gz", hash = "sha256:aa237a65f6f4da067119b7df3f13e89c25c051327b2b5b66dc075f33d62480d7"}, +] +eradicate = [ + {file = "eradicate-2.0.0.tar.gz", hash = "sha256:27434596f2c5314cc9b31410c93d8f7e8885747399773cd088d3adea647a60c8"}, ] fastapi = [ - {file = "fastapi-0.89.1-py3-none-any.whl", hash = "sha256:f9773ea22290635b2f48b4275b2bf69a8fa721fda2e38228bed47139839dc877"}, - {file = "fastapi-0.89.1.tar.gz", hash = "sha256:15d9271ee52b572a015ca2ae5c72e1ce4241dd8532a534ad4f7ec70c376a580f"}, + {file = "fastapi-0.70.1-py3-none-any.whl", hash = "sha256:5367226c7bcd7bfb2e17edaf225fd9a983095b1372281e9a3eb661336fb93748"}, + {file = "fastapi-0.70.1.tar.gz", hash = "sha256:21d03979b5336375c66fa5d1f3126c6beca650d5d2166fbb78345a30d33c8d06"}, +] +filelock = [ + {file = "filelock-3.6.0-py3-none-any.whl", hash = "sha256:f8314284bfffbdcfa0ff3d7992b023d4c628ced6feb957351d4c48d059f56bc0"}, + {file = "filelock-3.6.0.tar.gz", hash = "sha256:9cd540a9352e432c7246a48fe4e8712b10acb1df2ad1f30e8c070b82ae1fed85"}, +] +flake8 = [ + {file = "flake8-4.0.1-py2.py3-none-any.whl", hash = "sha256:479b1304f72536a55948cb40a32dce8bb0ffe3501e26eaf292c7e60eb5e0428d"}, + {file = "flake8-4.0.1.tar.gz", hash = "sha256:806e034dda44114815e23c16ef92f95c91e4c71100ff52813adf7132a6ad870d"}, +] +flake8-black = [ + {file = "flake8-black-0.2.4.tar.gz", hash = "sha256:a7871bfd1cbff431a1fc91ba60ae154510c80f575e6b9a2bbb13dfb4650afd22"}, + {file = "flake8_black-0.2.4-py3-none-any.whl", hash = "sha256:0a70dfd97c8439827f365dc6dbc6c8c9cc087f0833625c6cc6848ff7876256be"}, +] +flake8-bugbear = [ + {file = "flake8-bugbear-22.1.11.tar.gz", hash = "sha256:4c2a4136bd4ecb8bf02d5159af302ffc067642784c9d0488b33ce4610da825ee"}, + {file = "flake8_bugbear-22.1.11-py3-none-any.whl", hash = "sha256:ce7ae44aaaf67ef192b8a6de94a5ac617144e1675ad0654fdea556f48dc18d9b"}, +] +flake8-eradicate = [ + {file = "flake8-eradicate-1.2.0.tar.gz", hash = "sha256:acaa1b6839ff00d284b805c432fdfa6047262bd15a5504ec945797e87b4de1fa"}, + {file = "flake8_eradicate-1.2.0-py3-none-any.whl", hash = "sha256:51dc660d0c1c1ed93af0f813540bbbf72ab2d3466c14e3f3bac371c618b6042f"}, +] +flake8-isort = [ + {file = "flake8-isort-4.1.1.tar.gz", hash = "sha256:d814304ab70e6e58859bc5c3e221e2e6e71c958e7005239202fee19c24f82717"}, + {file = "flake8_isort-4.1.1-py3-none-any.whl", hash = "sha256:c4e8b6dcb7be9b71a02e6e5d4196cefcef0f3447be51e82730fb336fff164949"}, ] flask = [ - {file = "Flask-2.2.2-py3-none-any.whl", hash = "sha256:b9c46cc36662a7949f34b52d8ec7bb59c0d74ba08ba6cb9ce9adc1d8676d9526"}, - {file = "Flask-2.2.2.tar.gz", hash = "sha256:642c450d19c4ad482f96729bd2a8f6d32554aa1e231f4f6b4e7e5264b16cca2b"}, + {file = "Flask-1.1.4-py2.py3-none-any.whl", hash = "sha256:c34f04500f2cbbea882b1acb02002ad6fe6b7ffa64a6164577995657f50aed22"}, + {file = "Flask-1.1.4.tar.gz", hash = "sha256:0fbeb6180d383a9186d0d6ed954e0042ad9f18e0e8de088b2b419d526927d196"}, ] freezegun = [ - {file = "freezegun-1.2.2-py3-none-any.whl", hash = "sha256:ea1b963b993cb9ea195adbd893a48d573fda951b0da64f60883d7e988b606c9f"}, - {file = "freezegun-1.2.2.tar.gz", hash = "sha256:cd22d1ba06941384410cd967d8a99d5ae2442f57dfafeff2fda5de8dc5c05446"}, + {file = "freezegun-1.1.0-py2.py3-none-any.whl", hash = "sha256:2ae695f7eb96c62529f03a038461afe3c692db3465e215355e1bb4b0ab408712"}, + {file = "freezegun-1.1.0.tar.gz", hash = "sha256:177f9dd59861d871e27a484c3332f35a6e3f5d14626f2bf91be37891f18927f3"}, ] frozenlist = [ - {file = "frozenlist-1.3.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ff8bf625fe85e119553b5383ba0fb6aa3d0ec2ae980295aaefa552374926b3f4"}, - {file = "frozenlist-1.3.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dfbac4c2dfcc082fcf8d942d1e49b6aa0766c19d3358bd86e2000bf0fa4a9cf0"}, - {file = "frozenlist-1.3.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b1c63e8d377d039ac769cd0926558bb7068a1f7abb0f003e3717ee003ad85530"}, - {file = "frozenlist-1.3.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7fdfc24dcfce5b48109867c13b4cb15e4660e7bd7661741a391f821f23dfdca7"}, - {file = "frozenlist-1.3.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2c926450857408e42f0bbc295e84395722ce74bae69a3b2aa2a65fe22cb14b99"}, - {file = "frozenlist-1.3.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1841e200fdafc3d51f974d9d377c079a0694a8f06de2e67b48150328d66d5483"}, - {file = "frozenlist-1.3.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f470c92737afa7d4c3aacc001e335062d582053d4dbe73cda126f2d7031068dd"}, - {file = "frozenlist-1.3.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:783263a4eaad7c49983fe4b2e7b53fa9770c136c270d2d4bbb6d2192bf4d9caf"}, - {file = "frozenlist-1.3.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:924620eef691990dfb56dc4709f280f40baee568c794b5c1885800c3ecc69816"}, - {file = "frozenlist-1.3.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:ae4dc05c465a08a866b7a1baf360747078b362e6a6dbeb0c57f234db0ef88ae0"}, - {file = "frozenlist-1.3.3-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:bed331fe18f58d844d39ceb398b77d6ac0b010d571cba8267c2e7165806b00ce"}, - {file = "frozenlist-1.3.3-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:02c9ac843e3390826a265e331105efeab489ffaf4dd86384595ee8ce6d35ae7f"}, - {file = "frozenlist-1.3.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:9545a33965d0d377b0bc823dcabf26980e77f1b6a7caa368a365a9497fb09420"}, - {file = "frozenlist-1.3.3-cp310-cp310-win32.whl", hash = "sha256:d5cd3ab21acbdb414bb6c31958d7b06b85eeb40f66463c264a9b343a4e238642"}, - {file = "frozenlist-1.3.3-cp310-cp310-win_amd64.whl", hash = "sha256:b756072364347cb6aa5b60f9bc18e94b2f79632de3b0190253ad770c5df17db1"}, - {file = "frozenlist-1.3.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b4395e2f8d83fbe0c627b2b696acce67868793d7d9750e90e39592b3626691b7"}, - {file = "frozenlist-1.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:14143ae966a6229350021384870458e4777d1eae4c28d1a7aa47f24d030e6678"}, - {file = "frozenlist-1.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5d8860749e813a6f65bad8285a0520607c9500caa23fea6ee407e63debcdbef6"}, - {file = "frozenlist-1.3.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23d16d9f477bb55b6154654e0e74557040575d9d19fe78a161bd33d7d76808e8"}, - {file = "frozenlist-1.3.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eb82dbba47a8318e75f679690190c10a5e1f447fbf9df41cbc4c3afd726d88cb"}, - {file = "frozenlist-1.3.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9309869032abb23d196cb4e4db574232abe8b8be1339026f489eeb34a4acfd91"}, - {file = "frozenlist-1.3.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a97b4fe50b5890d36300820abd305694cb865ddb7885049587a5678215782a6b"}, - {file = "frozenlist-1.3.3-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c188512b43542b1e91cadc3c6c915a82a5eb95929134faf7fd109f14f9892ce4"}, - {file = "frozenlist-1.3.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:303e04d422e9b911a09ad499b0368dc551e8c3cd15293c99160c7f1f07b59a48"}, - {file = "frozenlist-1.3.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:0771aed7f596c7d73444c847a1c16288937ef988dc04fb9f7be4b2aa91db609d"}, - {file = "frozenlist-1.3.3-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:66080ec69883597e4d026f2f71a231a1ee9887835902dbe6b6467d5a89216cf6"}, - {file = "frozenlist-1.3.3-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:41fe21dc74ad3a779c3d73a2786bdf622ea81234bdd4faf90b8b03cad0c2c0b4"}, - {file = "frozenlist-1.3.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f20380df709d91525e4bee04746ba612a4df0972c1b8f8e1e8af997e678c7b81"}, - {file = "frozenlist-1.3.3-cp311-cp311-win32.whl", hash = "sha256:f30f1928162e189091cf4d9da2eac617bfe78ef907a761614ff577ef4edfb3c8"}, - {file = "frozenlist-1.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:a6394d7dadd3cfe3f4b3b186e54d5d8504d44f2d58dcc89d693698e8b7132b32"}, - {file = "frozenlist-1.3.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8df3de3a9ab8325f94f646609a66cbeeede263910c5c0de0101079ad541af332"}, - {file = "frozenlist-1.3.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0693c609e9742c66ba4870bcee1ad5ff35462d5ffec18710b4ac89337ff16e27"}, - {file = "frozenlist-1.3.3-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cd4210baef299717db0a600d7a3cac81d46ef0e007f88c9335db79f8979c0d3d"}, - {file = "frozenlist-1.3.3-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:394c9c242113bfb4b9aa36e2b80a05ffa163a30691c7b5a29eba82e937895d5e"}, - {file = "frozenlist-1.3.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6327eb8e419f7d9c38f333cde41b9ae348bec26d840927332f17e887a8dcb70d"}, - {file = "frozenlist-1.3.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2e24900aa13212e75e5b366cb9065e78bbf3893d4baab6052d1aca10d46d944c"}, - {file = "frozenlist-1.3.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:3843f84a6c465a36559161e6c59dce2f2ac10943040c2fd021cfb70d58c4ad56"}, - {file = "frozenlist-1.3.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:84610c1502b2461255b4c9b7d5e9c48052601a8957cd0aea6ec7a7a1e1fb9420"}, - {file = "frozenlist-1.3.3-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:c21b9aa40e08e4f63a2f92ff3748e6b6c84d717d033c7b3438dd3123ee18f70e"}, - {file = "frozenlist-1.3.3-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:efce6ae830831ab6a22b9b4091d411698145cb9b8fc869e1397ccf4b4b6455cb"}, - {file = "frozenlist-1.3.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:40de71985e9042ca00b7953c4f41eabc3dc514a2d1ff534027f091bc74416401"}, - {file = "frozenlist-1.3.3-cp37-cp37m-win32.whl", hash = "sha256:180c00c66bde6146a860cbb81b54ee0df350d2daf13ca85b275123bbf85de18a"}, - {file = "frozenlist-1.3.3-cp37-cp37m-win_amd64.whl", hash = "sha256:9bbbcedd75acdfecf2159663b87f1bb5cfc80e7cd99f7ddd9d66eb98b14a8411"}, - {file = "frozenlist-1.3.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:034a5c08d36649591be1cbb10e09da9f531034acfe29275fc5454a3b101ce41a"}, - {file = "frozenlist-1.3.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ba64dc2b3b7b158c6660d49cdb1d872d1d0bf4e42043ad8d5006099479a194e5"}, - {file = "frozenlist-1.3.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:47df36a9fe24054b950bbc2db630d508cca3aa27ed0566c0baf661225e52c18e"}, - {file = "frozenlist-1.3.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:008a054b75d77c995ea26629ab3a0c0d7281341f2fa7e1e85fa6153ae29ae99c"}, - {file = "frozenlist-1.3.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:841ea19b43d438a80b4de62ac6ab21cfe6827bb8a9dc62b896acc88eaf9cecba"}, - {file = "frozenlist-1.3.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e235688f42b36be2b6b06fc37ac2126a73b75fb8d6bc66dd632aa35286238703"}, - {file = "frozenlist-1.3.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca713d4af15bae6e5d79b15c10c8522859a9a89d3b361a50b817c98c2fb402a2"}, - {file = "frozenlist-1.3.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ac5995f2b408017b0be26d4a1d7c61bce106ff3d9e3324374d66b5964325448"}, - {file = "frozenlist-1.3.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:a4ae8135b11652b08a8baf07631d3ebfe65a4c87909dbef5fa0cdde440444ee4"}, - {file = "frozenlist-1.3.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:4ea42116ceb6bb16dbb7d526e242cb6747b08b7710d9782aa3d6732bd8d27649"}, - {file = "frozenlist-1.3.3-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:810860bb4bdce7557bc0febb84bbd88198b9dbc2022d8eebe5b3590b2ad6c842"}, - {file = "frozenlist-1.3.3-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:ee78feb9d293c323b59a6f2dd441b63339a30edf35abcb51187d2fc26e696d13"}, - {file = "frozenlist-1.3.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:0af2e7c87d35b38732e810befb9d797a99279cbb85374d42ea61c1e9d23094b3"}, - {file = "frozenlist-1.3.3-cp38-cp38-win32.whl", hash = "sha256:899c5e1928eec13fd6f6d8dc51be23f0d09c5281e40d9cf4273d188d9feeaf9b"}, - {file = "frozenlist-1.3.3-cp38-cp38-win_amd64.whl", hash = "sha256:7f44e24fa70f6fbc74aeec3e971f60a14dde85da364aa87f15d1be94ae75aeef"}, - {file = "frozenlist-1.3.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2b07ae0c1edaa0a36339ec6cce700f51b14a3fc6545fdd32930d2c83917332cf"}, - {file = "frozenlist-1.3.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ebb86518203e12e96af765ee89034a1dbb0c3c65052d1b0c19bbbd6af8a145e1"}, - {file = "frozenlist-1.3.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5cf820485f1b4c91e0417ea0afd41ce5cf5965011b3c22c400f6d144296ccbc0"}, - {file = "frozenlist-1.3.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c11e43016b9024240212d2a65043b70ed8dfd3b52678a1271972702d990ac6d"}, - {file = "frozenlist-1.3.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8fa3c6e3305aa1146b59a09b32b2e04074945ffcfb2f0931836d103a2c38f936"}, - {file = "frozenlist-1.3.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:352bd4c8c72d508778cf05ab491f6ef36149f4d0cb3c56b1b4302852255d05d5"}, - {file = "frozenlist-1.3.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:65a5e4d3aa679610ac6e3569e865425b23b372277f89b5ef06cf2cdaf1ebf22b"}, - {file = "frozenlist-1.3.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1e2c1185858d7e10ff045c496bbf90ae752c28b365fef2c09cf0fa309291669"}, - {file = "frozenlist-1.3.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f163d2fd041c630fed01bc48d28c3ed4a3b003c00acd396900e11ee5316b56bb"}, - {file = "frozenlist-1.3.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:05cdb16d09a0832eedf770cb7bd1fe57d8cf4eaf5aced29c4e41e3f20b30a784"}, - {file = "frozenlist-1.3.3-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:8bae29d60768bfa8fb92244b74502b18fae55a80eac13c88eb0b496d4268fd2d"}, - {file = "frozenlist-1.3.3-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:eedab4c310c0299961ac285591acd53dc6723a1ebd90a57207c71f6e0c2153ab"}, - {file = "frozenlist-1.3.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3bbdf44855ed8f0fbcd102ef05ec3012d6a4fd7c7562403f76ce6a52aeffb2b1"}, - {file = "frozenlist-1.3.3-cp39-cp39-win32.whl", hash = "sha256:efa568b885bca461f7c7b9e032655c0c143d305bf01c30caf6db2854a4532b38"}, - {file = "frozenlist-1.3.3-cp39-cp39-win_amd64.whl", hash = "sha256:cfe33efc9cb900a4c46f91a5ceba26d6df370ffddd9ca386eb1d4f0ad97b9ea9"}, - {file = "frozenlist-1.3.3.tar.gz", hash = "sha256:58bcc55721e8a90b88332d6cd441261ebb22342e238296bb330968952fbb3a6a"}, + {file = "frozenlist-1.3.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d2257aaba9660f78c7b1d8fea963b68f3feffb1a9d5d05a18401ca9eb3e8d0a3"}, + {file = "frozenlist-1.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4a44ebbf601d7bac77976d429e9bdb5a4614f9f4027777f9e54fd765196e9d3b"}, + {file = "frozenlist-1.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:45334234ec30fc4ea677f43171b18a27505bfb2dba9aca4398a62692c0ea8868"}, + {file = "frozenlist-1.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:47be22dc27ed933d55ee55845d34a3e4e9f6fee93039e7f8ebadb0c2f60d403f"}, + {file = "frozenlist-1.3.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:03a7dd1bfce30216a3f51a84e6dd0e4a573d23ca50f0346634916ff105ba6e6b"}, + {file = "frozenlist-1.3.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:691ddf6dc50480ce49f68441f1d16a4c3325887453837036e0fb94736eae1e58"}, + {file = "frozenlist-1.3.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bde99812f237f79eaf3f04ebffd74f6718bbd216101b35ac7955c2d47c17da02"}, + {file = "frozenlist-1.3.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6a202458d1298ced3768f5a7d44301e7c86defac162ace0ab7434c2e961166e8"}, + {file = "frozenlist-1.3.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b9e3e9e365991f8cc5f5edc1fd65b58b41d0514a6a7ad95ef5c7f34eb49b3d3e"}, + {file = "frozenlist-1.3.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:04cb491c4b1c051734d41ea2552fde292f5f3a9c911363f74f39c23659c4af78"}, + {file = "frozenlist-1.3.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:436496321dad302b8b27ca955364a439ed1f0999311c393dccb243e451ff66aa"}, + {file = "frozenlist-1.3.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:754728d65f1acc61e0f4df784456106e35afb7bf39cfe37227ab00436fb38676"}, + {file = "frozenlist-1.3.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6eb275c6385dd72594758cbe96c07cdb9bd6becf84235f4a594bdf21e3596c9d"}, + {file = "frozenlist-1.3.0-cp310-cp310-win32.whl", hash = "sha256:e30b2f9683812eb30cf3f0a8e9f79f8d590a7999f731cf39f9105a7c4a39489d"}, + {file = "frozenlist-1.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:f7353ba3367473d1d616ee727945f439e027f0bb16ac1a750219a8344d1d5d3c"}, + {file = "frozenlist-1.3.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:88aafd445a233dbbf8a65a62bc3249a0acd0d81ab18f6feb461cc5a938610d24"}, + {file = "frozenlist-1.3.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4406cfabef8f07b3b3af0f50f70938ec06d9f0fc26cbdeaab431cbc3ca3caeaa"}, + {file = "frozenlist-1.3.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8cf829bd2e2956066dd4de43fd8ec881d87842a06708c035b37ef632930505a2"}, + {file = "frozenlist-1.3.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:603b9091bd70fae7be28bdb8aa5c9990f4241aa33abb673390a7f7329296695f"}, + {file = "frozenlist-1.3.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:25af28b560e0c76fa41f550eacb389905633e7ac02d6eb3c09017fa1c8cdfde1"}, + {file = "frozenlist-1.3.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94c7a8a9fc9383b52c410a2ec952521906d355d18fccc927fca52ab575ee8b93"}, + {file = "frozenlist-1.3.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:65bc6e2fece04e2145ab6e3c47428d1bbc05aede61ae365b2c1bddd94906e478"}, + {file = "frozenlist-1.3.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:3f7c935c7b58b0d78c0beea0c7358e165f95f1fd8a7e98baa40d22a05b4a8141"}, + {file = "frozenlist-1.3.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd89acd1b8bb4f31b47072615d72e7f53a948d302b7c1d1455e42622de180eae"}, + {file = "frozenlist-1.3.0-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:6983a31698490825171be44ffbafeaa930ddf590d3f051e397143a5045513b01"}, + {file = "frozenlist-1.3.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:adac9700675cf99e3615eb6a0eb5e9f5a4143c7d42c05cea2e7f71c27a3d0846"}, + {file = "frozenlist-1.3.0-cp37-cp37m-win32.whl", hash = "sha256:0c36e78b9509e97042ef869c0e1e6ef6429e55817c12d78245eb915e1cca7468"}, + {file = "frozenlist-1.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:57f4d3f03a18facacb2a6bcd21bccd011e3b75d463dc49f838fd699d074fabd1"}, + {file = "frozenlist-1.3.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:8c905a5186d77111f02144fab5b849ab524f1e876a1e75205cd1386a9be4b00a"}, + {file = "frozenlist-1.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b5009062d78a8c6890d50b4e53b0ddda31841b3935c1937e2ed8c1bda1c7fb9d"}, + {file = "frozenlist-1.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2fdc3cd845e5a1f71a0c3518528bfdbfe2efaf9886d6f49eacc5ee4fd9a10953"}, + {file = "frozenlist-1.3.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:92e650bd09b5dda929523b9f8e7f99b24deac61240ecc1a32aeba487afcd970f"}, + {file = "frozenlist-1.3.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:40dff8962b8eba91fd3848d857203f0bd704b5f1fa2b3fc9af64901a190bba08"}, + {file = "frozenlist-1.3.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:768efd082074bb203c934e83a61654ed4931ef02412c2fbdecea0cff7ecd0274"}, + {file = "frozenlist-1.3.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:006d3595e7d4108a12025ddf415ae0f6c9e736e726a5db0183326fd191b14c5e"}, + {file = "frozenlist-1.3.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:871d42623ae15eb0b0e9df65baeee6976b2e161d0ba93155411d58ff27483ad8"}, + {file = "frozenlist-1.3.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:aff388be97ef2677ae185e72dc500d19ecaf31b698986800d3fc4f399a5e30a5"}, + {file = "frozenlist-1.3.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:9f892d6a94ec5c7b785e548e42722e6f3a52f5f32a8461e82ac3e67a3bd073f1"}, + {file = "frozenlist-1.3.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:e982878792c971cbd60ee510c4ee5bf089a8246226dea1f2138aa0bb67aff148"}, + {file = "frozenlist-1.3.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:c6c321dd013e8fc20735b92cb4892c115f5cdb82c817b1e5b07f6b95d952b2f0"}, + {file = "frozenlist-1.3.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:30530930410855c451bea83f7b272fb1c495ed9d5cc72895ac29e91279401db3"}, + {file = "frozenlist-1.3.0-cp38-cp38-win32.whl", hash = "sha256:40ec383bc194accba825fbb7d0ef3dda5736ceab2375462f1d8672d9f6b68d07"}, + {file = "frozenlist-1.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:f20baa05eaa2bcd5404c445ec51aed1c268d62600362dc6cfe04fae34a424bd9"}, + {file = "frozenlist-1.3.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:0437fe763fb5d4adad1756050cbf855bbb2bf0d9385c7bb13d7a10b0dd550486"}, + {file = "frozenlist-1.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b684c68077b84522b5c7eafc1dc735bfa5b341fb011d5552ebe0968e22ed641c"}, + {file = "frozenlist-1.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:93641a51f89473837333b2f8100f3f89795295b858cd4c7d4a1f18e299dc0a4f"}, + {file = "frozenlist-1.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6d32ff213aef0fd0bcf803bffe15cfa2d4fde237d1d4838e62aec242a8362fa"}, + {file = "frozenlist-1.3.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:31977f84828b5bb856ca1eb07bf7e3a34f33a5cddce981d880240ba06639b94d"}, + {file = "frozenlist-1.3.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3c62964192a1c0c30b49f403495911298810bada64e4f03249ca35a33ca0417a"}, + {file = "frozenlist-1.3.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4eda49bea3602812518765810af732229b4291d2695ed24a0a20e098c45a707b"}, + {file = "frozenlist-1.3.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acb267b09a509c1df5a4ca04140da96016f40d2ed183cdc356d237286c971b51"}, + {file = "frozenlist-1.3.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:e1e26ac0a253a2907d654a37e390904426d5ae5483150ce3adedb35c8c06614a"}, + {file = "frozenlist-1.3.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f96293d6f982c58ebebb428c50163d010c2f05de0cde99fd681bfdc18d4b2dc2"}, + {file = "frozenlist-1.3.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:e84cb61b0ac40a0c3e0e8b79c575161c5300d1d89e13c0e02f76193982f066ed"}, + {file = "frozenlist-1.3.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:ff9310f05b9d9c5c4dd472983dc956901ee6cb2c3ec1ab116ecdde25f3ce4951"}, + {file = "frozenlist-1.3.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d26b650b71fdc88065b7a21f8ace70175bcf3b5bdba5ea22df4bfd893e795a3b"}, + {file = "frozenlist-1.3.0-cp39-cp39-win32.whl", hash = "sha256:01a73627448b1f2145bddb6e6c2259988bb8aee0fb361776ff8604b99616cd08"}, + {file = "frozenlist-1.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:772965f773757a6026dea111a15e6e2678fbd6216180f82a48a40b27de1ee2ab"}, + {file = "frozenlist-1.3.0.tar.gz", hash = "sha256:ce6f2ba0edb7b0c1d8976565298ad2deba6f8064d2bebb6ffce2ca896eb35b0b"}, ] graphql-core = [ - {file = "graphql-core-3.2.3.tar.gz", hash = "sha256:06d2aad0ac723e35b1cb47885d3e5c45e956a53bc1b209a9fc5369007fe46676"}, - {file = "graphql_core-3.2.3-py3-none-any.whl", hash = "sha256:5766780452bd5ec8ba133f8bf287dc92713e3868ddd83aee4faab9fc3e303dc3"}, + {file = "graphql-core-3.1.7.tar.gz", hash = "sha256:62ec192150ccecd9a18cfb79e3e72eb7d1fd68fb594ef19c40099b6deec8ef0c"}, + {file = "graphql_core-3.1.7-py3-none-any.whl", hash = "sha256:9b460f60320be01c7f3b1766cf3e406933003008055079b9d983b8f3988f4400"}, ] h11 = [ - {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, - {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, + {file = "h11-0.12.0-py3-none-any.whl", hash = "sha256:36a3cb8c0a032f56e2da7084577878a035d3b61d104230d4bd49c0c6b555a9c6"}, + {file = "h11-0.12.0.tar.gz", hash = "sha256:47222cb6067e4a307d535814917cd98fd0a57b6788ce715755fa2b6c28b56042"}, ] httpcore = [ - {file = "httpcore-0.16.3-py3-none-any.whl", hash = "sha256:da1fb708784a938aa084bde4feb8317056c55037247c787bd7e19eb2c2949dc0"}, - {file = "httpcore-0.16.3.tar.gz", hash = "sha256:c5d6f04e2fc530f39e0c077e6a30caa53f1451096120f1f38b954afd0b17c0cb"}, + {file = "httpcore-0.14.7-py3-none-any.whl", hash = "sha256:47d772f754359e56dd9d892d9593b6f9870a37aeb8ba51e9a88b09b3d68cfade"}, + {file = "httpcore-0.14.7.tar.gz", hash = "sha256:7503ec1c0f559066e7e39bc4003fd2ce023d01cf51793e3c173b864eb456ead1"}, ] httptools = [ - {file = "httptools-0.5.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:8f470c79061599a126d74385623ff4744c4e0f4a0997a353a44923c0b561ee51"}, - {file = "httptools-0.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e90491a4d77d0cb82e0e7a9cb35d86284c677402e4ce7ba6b448ccc7325c5421"}, - {file = "httptools-0.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1d2357f791b12d86faced7b5736dea9ef4f5ecdc6c3f253e445ee82da579449"}, - {file = "httptools-0.5.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f90cd6fd97c9a1b7fe9215e60c3bd97336742a0857f00a4cb31547bc22560c2"}, - {file = "httptools-0.5.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:5230a99e724a1bdbbf236a1b58d6e8504b912b0552721c7c6b8570925ee0ccde"}, - {file = "httptools-0.5.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:3a47a34f6015dd52c9eb629c0f5a8a5193e47bf2a12d9a3194d231eaf1bc451a"}, - {file = "httptools-0.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:24bb4bb8ac3882f90aa95403a1cb48465de877e2d5298ad6ddcfdebec060787d"}, - {file = "httptools-0.5.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e67d4f8734f8054d2c4858570cc4b233bf753f56e85217de4dfb2495904cf02e"}, - {file = "httptools-0.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7e5eefc58d20e4c2da82c78d91b2906f1a947ef42bd668db05f4ab4201a99f49"}, - {file = "httptools-0.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0297822cea9f90a38df29f48e40b42ac3d48a28637368f3ec6d15eebefd182f9"}, - {file = "httptools-0.5.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:557be7fbf2bfa4a2ec65192c254e151684545ebab45eca5d50477d562c40f986"}, - {file = "httptools-0.5.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:54465401dbbec9a6a42cf737627fb0f014d50dc7365a6b6cd57753f151a86ff0"}, - {file = "httptools-0.5.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:4d9ebac23d2de960726ce45f49d70eb5466725c0087a078866043dad115f850f"}, - {file = "httptools-0.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:e8a34e4c0ab7b1ca17b8763613783e2458e77938092c18ac919420ab8655c8c1"}, - {file = "httptools-0.5.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f659d7a48401158c59933904040085c200b4be631cb5f23a7d561fbae593ec1f"}, - {file = "httptools-0.5.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ef1616b3ba965cd68e6f759eeb5d34fbf596a79e84215eeceebf34ba3f61fdc7"}, - {file = "httptools-0.5.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3625a55886257755cb15194efbf209584754e31d336e09e2ffe0685a76cb4b60"}, - {file = "httptools-0.5.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:72ad589ba5e4a87e1d404cc1cb1b5780bfcb16e2aec957b88ce15fe879cc08ca"}, - {file = "httptools-0.5.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:850fec36c48df5a790aa735417dca8ce7d4b48d59b3ebd6f83e88a8125cde324"}, - {file = "httptools-0.5.0-cp36-cp36m-win_amd64.whl", hash = "sha256:f222e1e9d3f13b68ff8a835574eda02e67277d51631d69d7cf7f8e07df678c86"}, - {file = "httptools-0.5.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3cb8acf8f951363b617a8420768a9f249099b92e703c052f9a51b66342eea89b"}, - {file = "httptools-0.5.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:550059885dc9c19a072ca6d6735739d879be3b5959ec218ba3e013fd2255a11b"}, - {file = "httptools-0.5.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a04fe458a4597aa559b79c7f48fe3dceabef0f69f562daf5c5e926b153817281"}, - {file = "httptools-0.5.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7d0c1044bce274ec6711f0770fd2d5544fe392591d204c68328e60a46f88843b"}, - {file = "httptools-0.5.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:c6eeefd4435055a8ebb6c5cc36111b8591c192c56a95b45fe2af22d9881eee25"}, - {file = "httptools-0.5.0-cp37-cp37m-win_amd64.whl", hash = "sha256:5b65be160adcd9de7a7e6413a4966665756e263f0d5ddeffde277ffeee0576a5"}, - {file = "httptools-0.5.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:fe9c766a0c35b7e3d6b6939393c8dfdd5da3ac5dec7f971ec9134f284c6c36d6"}, - {file = "httptools-0.5.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:85b392aba273566c3d5596a0a490978c085b79700814fb22bfd537d381dd230c"}, - {file = "httptools-0.5.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f5e3088f4ed33947e16fd865b8200f9cfae1144f41b64a8cf19b599508e096bc"}, - {file = "httptools-0.5.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c2a56b6aad7cc8f5551d8e04ff5a319d203f9d870398b94702300de50190f63"}, - {file = "httptools-0.5.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:9b571b281a19762adb3f48a7731f6842f920fa71108aff9be49888320ac3e24d"}, - {file = "httptools-0.5.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:aa47ffcf70ba6f7848349b8a6f9b481ee0f7637931d91a9860a1838bfc586901"}, - {file = "httptools-0.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:bede7ee075e54b9a5bde695b4fc8f569f30185891796b2e4e09e2226801d09bd"}, - {file = "httptools-0.5.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:64eba6f168803a7469866a9c9b5263a7463fa8b7a25b35e547492aa7322036b6"}, - {file = "httptools-0.5.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4b098e4bb1174096a93f48f6193e7d9aa7071506a5877da09a783509ca5fff42"}, - {file = "httptools-0.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9423a2de923820c7e82e18980b937893f4aa8251c43684fa1772e341f6e06887"}, - {file = "httptools-0.5.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca1b7becf7d9d3ccdbb2f038f665c0f4857e08e1d8481cbcc1a86a0afcfb62b2"}, - {file = "httptools-0.5.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:50d4613025f15f4b11f1c54bbed4761c0020f7f921b95143ad6d58c151198142"}, - {file = "httptools-0.5.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8ffce9d81c825ac1deaa13bc9694c0562e2840a48ba21cfc9f3b4c922c16f372"}, - {file = "httptools-0.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:1af91b3650ce518d226466f30bbba5b6376dbd3ddb1b2be8b0658c6799dd450b"}, - {file = "httptools-0.5.0.tar.gz", hash = "sha256:295874861c173f9101960bba332429bb77ed4dcd8cdf5cee9922eb00e4f6bc09"}, + {file = "httptools-0.3.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4137137de8976511a392e27bfdcf231bd926ac13d375e0414e927b08217d779e"}, + {file = "httptools-0.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9f475b642c48b1b78584bdd12a5143e2c512485664331eade9c29ef769a17598"}, + {file = "httptools-0.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4687dfc116a9f1eb22a7d797f0dc6f6e17190d406ca4e729634b38aa98044b17"}, + {file = "httptools-0.3.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:72ee0e3fb9c6437ab3ae34e9abee67fcee6876f4f58504e3f613dd5882aafdb7"}, + {file = "httptools-0.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:3787c1f46e9722ef7f07ea5c76b0103037483d1b12e34a02c53ceca5afa4e09a"}, + {file = "httptools-0.3.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:c0ac2e0ce6733c55858932e7d37fcc7b67ba6bb23e9648593c55f663de031b93"}, + {file = "httptools-0.3.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79717080dc3f8b1eeb7f820b9b81528acbc04be6041f323fdd97550da2062575"}, + {file = "httptools-0.3.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:eda95634027200f4b2a6d499e7c2e7fa9b8ee57e045dfda26958ea0af27c070b"}, + {file = "httptools-0.3.0-cp36-cp36m-win_amd64.whl", hash = "sha256:3f82eb106e1474c63dba36a176067e65b48385f4cecddf3616411aa5d1fbdfec"}, + {file = "httptools-0.3.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c14576b737d9e6e4f2a86af04918dbe9b62f57ce8102a8695c9a382dbe405c7f"}, + {file = "httptools-0.3.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:113816f9af7dcfc4aa71ebb5354d77365f666ecf96ac7ff2aa1d24b6bca44165"}, + {file = "httptools-0.3.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:b8ac7dee63af4346e02b1e6d32202e3b5b3706a9928bec6da6d7a5b066217422"}, + {file = "httptools-0.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:04114db99605c9b56ea22a8ec4d7b1485b908128ed4f4a8f6438489c428da794"}, + {file = "httptools-0.3.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6e676bc3bb911b11f3d7e2144b9a53600bf6b9b21e0e4437aa308e1eef094d97"}, + {file = "httptools-0.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cdc3975db86c29817e6d13df14e037c931fc893a710fb71097777a4147090068"}, + {file = "httptools-0.3.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8ac842df4fc3952efa7820b277961ea55e068bbc54cb59a0820400de7ae358d8"}, + {file = "httptools-0.3.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:47dba2345aaa01b87e4981e8756af441349340708d5b60712c98c55a4d28f4af"}, + {file = "httptools-0.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:5a836bd85ae1fb4304f674808488dae403e136d274aa5bafd0e6ee456f11c371"}, + {file = "httptools-0.3.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:1a8f26327023fa1a947d36e60a0582149e182fbbc949c8a65ec8665754dbbe69"}, + {file = "httptools-0.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:32a10a5903b5bc0eb647d01cd1e95bec3bb614a9bf53f0af1e01360b2debdf81"}, + {file = "httptools-0.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21e948034f70e47c8abfa2d5e6f1a5661f87a2cddc7bcc70f61579cc87897c70"}, + {file = "httptools-0.3.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:074afd8afdeec0fa6786cd4a1676e0c0be23dc9a017a86647efa6b695168104f"}, + {file = "httptools-0.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:2119fa619a4c53311f594f25c0205d619350fcb32140ec5057f861952e9b2b4f"}, + {file = "httptools-0.3.0.tar.gz", hash = "sha256:3f9b4856d46ba1f0c850f4e84b264a9a8b4460acb20e865ec00978ad9fbaa4cf"}, ] httpx = [ - {file = "httpx-0.23.3-py3-none-any.whl", hash = "sha256:a211fcce9b1254ea24f0cd6af9869b3d29aba40154e947d2a07bb499b3e310d6"}, - {file = "httpx-0.23.3.tar.gz", hash = "sha256:9818458eb565bb54898ccb9b8b251a28785dd4a55afbc23d0eb410754fe7d0f9"}, + {file = "httpx-0.21.3-py3-none-any.whl", hash = "sha256:df9a0fd43fa79dbab411d83eb1ea6f7a525c96ad92e60c2d7f40388971b25777"}, + {file = "httpx-0.21.3.tar.gz", hash = "sha256:7a3eb67ef0b8abbd6d9402248ef2f84a76080fa1c839f8662e6eb385640e445a"}, ] -hyperlink = [ - {file = "hyperlink-21.0.0-py2.py3-none-any.whl", hash = "sha256:e6b14c37ecb73e89c77d78cdb4c2cc8f3fb59a885c5b3f819ff4ed80f25af1b4"}, - {file = "hyperlink-21.0.0.tar.gz", hash = "sha256:427af957daa58bc909471c6c40f74c5450fa123dd093fc53efd2e91d2705a56b"}, +identify = [ + {file = "identify-2.4.10-py2.py3-none-any.whl", hash = "sha256:7d10baf6ba6f1912a0a49f4c1c2c49fa1718765c3a37d72d13b07779567c5b85"}, + {file = "identify-2.4.10.tar.gz", hash = "sha256:e12b2aea3cf108de73ae055c2260783bde6601de09718f6768cf8e9f6f6322a6"}, ] idna = [ - {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, - {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, + {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"}, + {file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"}, ] importlib-metadata = [ - {file = "importlib_metadata-6.0.0-py3-none-any.whl", hash = "sha256:7efb448ec9a5e313a57655d35aa54cd3e01b7e1fbcf72dce1bf06119420f5bad"}, - {file = "importlib_metadata-6.0.0.tar.gz", hash = "sha256:e354bedeb60efa6affdcc8ae121b73544a7aa74156d047311948f6d711cd378d"}, -] -importlib-resources = [ - {file = "importlib_resources-5.10.2-py3-none-any.whl", hash = "sha256:7d543798b0beca10b6a01ac7cafda9f822c54db9e8376a6bf57e0cbd74d486b6"}, - {file = "importlib_resources-5.10.2.tar.gz", hash = "sha256:e4a96c8cc0339647ff9a5e0550d9f276fc5a01ffa276012b58ec108cfd7b8484"}, -] -incremental = [ - {file = "incremental-22.10.0-py2.py3-none-any.whl", hash = "sha256:b864a1f30885ee72c5ac2835a761b8fe8aa9c28b9395cacf27286602688d3e51"}, - {file = "incremental-22.10.0.tar.gz", hash = "sha256:912feeb5e0f7e0188e6f42241d2f450002e11bbc0937c65865045854c24c0bd0"}, + {file = "importlib_metadata-4.2.0-py3-none-any.whl", hash = "sha256:057e92c15bc8d9e8109738a48db0ccb31b4d9d5cfbee5a8670879a30be66304b"}, + {file = "importlib_metadata-4.2.0.tar.gz", hash = "sha256:b7e52a1f8dec14a75ea73e0891f3060099ca1d8e6a462a4dff11c3e119ea1b31"}, ] iniconfig = [ - {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, - {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, + {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, + {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, ] inquirer = [ - {file = "inquirer-2.10.1-py3-none-any.whl", hash = "sha256:7a01977602214d6d86e8ddef3a1300927c4e58223eab69893e550604a0ac9477"}, - {file = "inquirer-2.10.1.tar.gz", hash = "sha256:e9876258183e24f6e8c44136b04f6f2e18dd6684aee59b86a8057c50601a6523"}, + {file = "inquirer-2.9.1-py3-none-any.whl", hash = "sha256:f50876f5073c8c5fc482b44b8ef4e9720061498abeb352f62b5c94cacf0e43e2"}, + {file = "inquirer-2.9.1.tar.gz", hash = "sha256:65f0a8eaa8bcabd7ec65771c9f872bb2700753c23bbe8b8ff12efd264a9dbf5d"}, +] +isort = [ + {file = "isort-5.10.1-py3-none-any.whl", hash = "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7"}, + {file = "isort-5.10.1.tar.gz", hash = "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"}, ] itsdangerous = [ - {file = "itsdangerous-2.1.2-py3-none-any.whl", hash = "sha256:2c2349112351b88699d8d4b6b075022c0808887cb7ad10069318a8b0bc88db44"}, - {file = "itsdangerous-2.1.2.tar.gz", hash = "sha256:5dbbc68b317e5e42f327f9021763545dc3fc3bfe22e6deb96aaf1fc38874156a"}, + {file = "itsdangerous-1.1.0-py2.py3-none-any.whl", hash = "sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749"}, + {file = "itsdangerous-1.1.0.tar.gz", hash = "sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19"}, ] jinja2 = [ - {file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"}, - {file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"}, + {file = "Jinja2-2.11.3-py2.py3-none-any.whl", hash = "sha256:03e47ad063331dd6a3f04a43eddca8a966a26ba0c5b7207a9a9e4e08f1b29419"}, + {file = "Jinja2-2.11.3.tar.gz", hash = "sha256:a6d58433de0ae800347cab1fa3043cebbabe8baa9d29e668f1c768cb87a333c6"}, ] jinxed = [ - {file = "jinxed-1.2.0-py2.py3-none-any.whl", hash = "sha256:cfc2b2e4e3b4326954d546ba6d6b9a7a796ddcb0aef8d03161d005177eb0d48b"}, - {file = "jinxed-1.2.0.tar.gz", hash = "sha256:032acda92d5c57cd216033cbbd53de731e6ed50deb63eb4781336ca55f72cda5"}, + {file = "jinxed-1.1.0-py2.py3-none-any.whl", hash = "sha256:6a61ccf963c16aa885304f27e6e5693783676897cea0c7f223270c8b8e78baf8"}, + {file = "jinxed-1.1.0.tar.gz", hash = "sha256:d8f1731f134e9e6b04d95095845ae6c10eb15cb223a5f0cabdea87d4a279c305"}, ] jmespath = [ - {file = "jmespath-1.0.1-py3-none-any.whl", hash = "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980"}, - {file = "jmespath-1.0.1.tar.gz", hash = "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe"}, -] -jsonschema = [ - {file = "jsonschema-4.17.3-py3-none-any.whl", hash = "sha256:a870ad254da1a8ca84b6a2905cac29d265f805acc57af304784962a2aa6508f6"}, - {file = "jsonschema-4.17.3.tar.gz", hash = "sha256:0f864437ab8b6076ba6707453ef8f98a6a0d512a80e93f8abdb676f737ecb60d"}, -] -libcst = [ - {file = "libcst-0.4.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4f9e42085c403e22201e5c41e707ef73e4ea910ad9fc67983ceee2368097f54e"}, - {file = "libcst-0.4.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1266530bf840cc40633a04feb578bb4cac1aa3aea058cc3729e24eab09a8e996"}, - {file = "libcst-0.4.9-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9679177391ccb9b0cdde3185c22bf366cb672457c4b7f4031fcb3b5e739fbd6"}, - {file = "libcst-0.4.9-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d67bc87e0d8db9434f2ea063734938a320f541f4c6da1074001e372f840f385d"}, - {file = "libcst-0.4.9-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e316da5a126f2a9e1d7680f95f907b575f082a35e2f8bd5620c59b2aaaebfe0a"}, - {file = "libcst-0.4.9-cp310-cp310-win_amd64.whl", hash = "sha256:7415569ab998a85b0fc9af3a204611ea7fadb2d719a12532c448f8fc98f5aca4"}, - {file = "libcst-0.4.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:15ded11ff7f4572f91635e02b519ae959f782689fdb4445bbebb7a3cc5c71d75"}, - {file = "libcst-0.4.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5b266867b712a120fad93983de432ddb2ccb062eb5fd2bea748c9a94cb200c36"}, - {file = "libcst-0.4.9-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:045b3b0b06413cdae6e9751b5f417f789ffa410f2cb2815e3e0e0ea6bef10ec0"}, - {file = "libcst-0.4.9-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e799add8fba4976628b9c1a6768d73178bf898f0ed1bd1322930c2d3db9063ba"}, - {file = "libcst-0.4.9-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:10479371d04ee8dc978c889c1774bbf6a83df88fa055fcb0159a606f6679c565"}, - {file = "libcst-0.4.9-cp311-cp311-win_amd64.whl", hash = "sha256:7a98286cbbfa90a42d376900c875161ad02a5a2a6b7c94c0f7afd9075e329ce4"}, - {file = "libcst-0.4.9-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:400166fc4efb9aa06ce44498d443aa78519082695b1894202dd73cd507d2d712"}, - {file = "libcst-0.4.9-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:46123863fba35cc84f7b54dd68826419cabfd9504d8a101c7fe3313ea03776f9"}, - {file = "libcst-0.4.9-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:27be8db54c0e5fe440021a771a38b81a7dbc23cd630eb8b0e9828b7717f9b702"}, - {file = "libcst-0.4.9-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:132bec627b064bd567e7e4cd6c89524d02842151eb0d8f5f3f7ffd2579ec1b09"}, - {file = "libcst-0.4.9-cp37-cp37m-win_amd64.whl", hash = "sha256:596860090aeed3ee6ad1e59c35c6c4110a57e4e896abf51b91cae003ec720a11"}, - {file = "libcst-0.4.9-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f4487608258109f774300466d4ca97353df29ae6ac23d1502e13e5509423c9d5"}, - {file = "libcst-0.4.9-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:aa53993e9a2853efb3ed3605da39f2e7125df6430f613eb67ef886c1ce4f94b5"}, - {file = "libcst-0.4.9-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f6ce794483d4c605ef0f5b199a49fb6996f9586ca938b7bfef213bd13858d7ab"}, - {file = "libcst-0.4.9-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:786e562b54bbcd17a060d1244deeef466b7ee07fe544074c252c4a169e38f1ee"}, - {file = "libcst-0.4.9-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:794250d2359edd518fb698e5d21c38a5bdfc5e4a75d0407b4c19818271ce6742"}, - {file = "libcst-0.4.9-cp38-cp38-win_amd64.whl", hash = "sha256:76491f67431318c3145442e97dddcead7075b074c59eac51be7cc9e3fffec6ee"}, - {file = "libcst-0.4.9-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3cf48d7aec6dc54b02aec0b1bb413c5bb3b02d852fd6facf1f05c7213e61a176"}, - {file = "libcst-0.4.9-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9b3348c6b7711a5235b133bd8e11d22e903c388db42485b8ceb5f2aa0fae9b9f"}, - {file = "libcst-0.4.9-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7e33b66762efaa014c38819efae5d8f726dd823e32d5d691035484411d2a2a69"}, - {file = "libcst-0.4.9-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1350d375d3fb9b20a6cf10c09b2964baca9be753a033dde7c1aced49d8e58387"}, - {file = "libcst-0.4.9-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3822056dc13326082362db35b3f649e0f4a97e36ddb4e487441da8e0fb9db7b3"}, - {file = "libcst-0.4.9-cp39-cp39-win_amd64.whl", hash = "sha256:183636141b839aa35b639e100883813744523bc7c12528906621121731b28443"}, - {file = "libcst-0.4.9.tar.gz", hash = "sha256:01786c403348f76f274dbaf3888ae237ffb73e6ed6973e65eba5c1fc389861dd"}, -] -mako = [ - {file = "Mako-1.2.4-py3-none-any.whl", hash = "sha256:c97c79c018b9165ac9922ae4f32da095ffd3c4e6872b45eded42926deea46818"}, - {file = "Mako-1.2.4.tar.gz", hash = "sha256:d60a3903dc3bb01a18ad6a89cdbe2e4eadc69c0bc8ef1e3773ba53d44c3f7a34"}, + {file = "jmespath-0.10.0-py2.py3-none-any.whl", hash = "sha256:cdf6525904cc597730141d61b36f2e4b8ecc257c420fa2f4549bac2c2d0cb72f"}, + {file = "jmespath-0.10.0.tar.gz", hash = "sha256:b85d0567b8666149a93172712e68920734333c0ce7e89b78b3e987f71e5ed4f9"}, ] markupsafe = [ - {file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:86b1f75c4e7c2ac2ccdaec2b9022845dbb81880ca318bb7a0a01fbf7813e3812"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f121a1420d4e173a5d96e47e9a0c0dcff965afdf1626d28de1460815f7c4ee7a"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a49907dd8420c5685cfa064a1335b6754b74541bbb3706c259c02ed65b644b3e"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10c1bfff05d95783da83491be968e8fe789263689c02724e0c691933c52994f5"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b7bd98b796e2b6553da7225aeb61f447f80a1ca64f41d83612e6139ca5213aa4"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b09bf97215625a311f669476f44b8b318b075847b49316d3e28c08e41a7a573f"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:694deca8d702d5db21ec83983ce0bb4b26a578e71fbdbd4fdcd387daa90e4d5e"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:efc1913fd2ca4f334418481c7e595c00aad186563bbc1ec76067848c7ca0a933"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-win32.whl", hash = "sha256:4a33dea2b688b3190ee12bd7cfa29d39c9ed176bda40bfa11099a3ce5d3a7ac6"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:dda30ba7e87fbbb7eab1ec9f58678558fd9a6b8b853530e176eabd064da81417"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:671cd1187ed5e62818414afe79ed29da836dde67166a9fac6d435873c44fdd02"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3799351e2336dc91ea70b034983ee71cf2f9533cdff7c14c90ea126bfd95d65a"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e72591e9ecd94d7feb70c1cbd7be7b3ebea3f548870aa91e2732960fa4d57a37"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6fbf47b5d3728c6aea2abb0589b5d30459e369baa772e0f37a0320185e87c980"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d5ee4f386140395a2c818d149221149c54849dfcfcb9f1debfe07a8b8bd63f9a"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:bcb3ed405ed3222f9904899563d6fc492ff75cce56cba05e32eff40e6acbeaa3"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:e1c0b87e09fa55a220f058d1d49d3fb8df88fbfab58558f1198e08c1e1de842a"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-win32.whl", hash = "sha256:8dc1c72a69aa7e082593c4a203dcf94ddb74bb5c8a731e4e1eb68d031e8498ff"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:97a68e6ada378df82bc9f16b800ab77cbf4b2fada0081794318520138c088e4a"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e8c843bbcda3a2f1e3c2ab25913c80a3c5376cd00c6e8c4a86a89a28c8dc5452"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0212a68688482dc52b2d45013df70d169f542b7394fc744c02a57374a4207003"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e576a51ad59e4bfaac456023a78f6b5e6e7651dcd383bcc3e18d06f9b55d6d1"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b9fe39a2ccc108a4accc2676e77da025ce383c108593d65cc909add5c3bd601"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:96e37a3dc86e80bf81758c152fe66dbf60ed5eca3d26305edf01892257049925"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6d0072fea50feec76a4c418096652f2c3238eaa014b2f94aeb1d56a66b41403f"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:089cf3dbf0cd6c100f02945abeb18484bd1ee57a079aefd52cffd17fba910b88"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6a074d34ee7a5ce3effbc526b7083ec9731bb3cbf921bbe1d3005d4d2bdb3a63"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-win32.whl", hash = "sha256:421be9fbf0ffe9ffd7a378aafebbf6f4602d564d34be190fc19a193232fd12b1"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:fc7b548b17d238737688817ab67deebb30e8073c95749d55538ed473130ec0c7"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e04e26803c9c3851c931eac40c695602c6295b8d432cbe78609649ad9bd2da8a"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b87db4360013327109564f0e591bd2a3b318547bcef31b468a92ee504d07ae4f"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99a2a507ed3ac881b975a2976d59f38c19386d128e7a9a18b7df6fff1fd4c1d6"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56442863ed2b06d19c37f94d999035e15ee982988920e12a5b4ba29b62ad1f77"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3ce11ee3f23f79dbd06fb3d63e2f6af7b12db1d46932fe7bd8afa259a5996603"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:33b74d289bd2f5e527beadcaa3f401e0df0a89927c1559c8566c066fa4248ab7"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:43093fb83d8343aac0b1baa75516da6092f58f41200907ef92448ecab8825135"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8e3dcf21f367459434c18e71b2a9532d96547aef8a871872a5bd69a715c15f96"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-win32.whl", hash = "sha256:d4306c36ca495956b6d568d276ac11fdd9c30a36f1b6eb928070dc5360b22e1c"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:46d00d6cfecdde84d40e572d63735ef81423ad31184100411e6e3388d405e247"}, - {file = "MarkupSafe-2.1.1.tar.gz", hash = "sha256:7f91197cc9e48f989d12e4e6fbc46495c446636dfc81b9ccf50bb0ec74b91d4b"}, -] -maturin = [ - {file = "maturin-0.14.10-py3-none-linux_armv6l.whl", hash = "sha256:ec8269c02cc435893308dfd50f57f14fb1be3554e4e61c5bf49b97363b289775"}, - {file = "maturin-0.14.10-py3-none-macosx_10_7_x86_64.whl", hash = "sha256:e9c19dc0a28109280f7d091ca7b78e25f3fc340fcfac92801829a21198fa20eb"}, - {file = "maturin-0.14.10-py3-none-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:cf950ebfe449a97617b91d75e09766509e21a389ce3f7b6ef15130ad8a95430a"}, - {file = "maturin-0.14.10-py3-none-manylinux_2_12_i686.manylinux2010_i686.musllinux_1_1_i686.whl", hash = "sha256:c0d25e82cb6e5de9f1c028fcf069784be4165b083e79412371edce05010b68f3"}, - {file = "maturin-0.14.10-py3-none-manylinux_2_12_x86_64.manylinux2010_x86_64.musllinux_1_1_x86_64.whl", hash = "sha256:9da98bee0a548ecaaa924cc8cb94e49075d5e71511c62a1633a6962c7831a29b"}, - {file = "maturin-0.14.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:2f097a63f3bed20a7da56fc7ce4d44ef8376ee9870604da16b685f2d02c87c79"}, - {file = "maturin-0.14.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:4946ad7545ba5fc0ad08bc98bc8e9f6ffabb6ded71db9ed282ad4596b998d42a"}, - {file = "maturin-0.14.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.musllinux_1_1_ppc64le.whl", hash = "sha256:98bfed21c3498857b3381efeb041d77e004a93b22261bf9690fe2b9fbb4c210f"}, - {file = "maturin-0.14.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b157e2e8a0216d02df1d0451201fcb977baf0dcd223890abfbfbfd01e0b44630"}, - {file = "maturin-0.14.10-py3-none-win32.whl", hash = "sha256:5abf311d4618b673efa30cacdac5ae2d462e49da58db9a5bf0d8bde16d9c16be"}, - {file = "maturin-0.14.10-py3-none-win_amd64.whl", hash = "sha256:11b8550ceba5b81465a18d06f0d3a4cfc1cd6cbf68eda117c253bbf3324b1264"}, - {file = "maturin-0.14.10-py3-none-win_arm64.whl", hash = "sha256:6cc9afb89f28bd591b62f8f3c29736c81c322cffe88f9ab8eb1749377bbc3521"}, - {file = "maturin-0.14.10.tar.gz", hash = "sha256:895c48cbe56ae994c2a1eeeef19475ca4819aa4c6412af727a63a772e8ef2d87"}, -] -msgspec = [ - {file = "msgspec-0.12.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3b3b193fc6e5399040f2c657f2fe77962b8d39bddb9923d4e4850e2e8111ef83"}, - {file = "msgspec-0.12.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6b90c8aa5b029f8fb8f9a4e71429cb37b4110382731058f7c4dfa125a005c459"}, - {file = "msgspec-0.12.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78cbcabfa413edc281f0f9bb652b42a3092cb289c31dc4489e7d896e615581fb"}, - {file = "msgspec-0.12.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be9e4eeea7f47c0a7c522afb4697d9618cb38e81e52130c9b15ad5279a69d153"}, - {file = "msgspec-0.12.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e581459710a53d240ad579bb7fbe2b64767003c3d06254f17c0cd106fab03b20"}, - {file = "msgspec-0.12.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:237ebaeb409269a001ba29dbb3f9fe760db63bc82d013b989733e6ec59ef2cf4"}, - {file = "msgspec-0.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:e010bab6128d1700d7bf70cbe7ce33a54cfeedf15c11f015712dcc0c062ca571"}, - {file = "msgspec-0.12.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ebe1cd8c42e85dbf59ede8ef1e4f8f73897664a3a3341f16a7616bb31fe21f2c"}, - {file = "msgspec-0.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:72b55df12dbcd89f636165bc1b76ac174917e7756105496b685a7970f5e9d70c"}, - {file = "msgspec-0.12.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bffa75be17ec2d4953c8068cbe6cd9b064dd0403ec6b04dc45d0dfdd9ca2cf36"}, - {file = "msgspec-0.12.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad43ccaf17deee41ed84dacc6619d2ccd3847fdebe9fc5f2b887bbf4b938724f"}, - {file = "msgspec-0.12.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f3177bd78b5a4e1663ee9279889d89b574acf619aa29aee84f86c00ca259016d"}, - {file = "msgspec-0.12.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:98bc70226b858218920a25b85906e61ada41898b8f2fc1f41af31d9628353e04"}, - {file = "msgspec-0.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:c9e3adf5f7a8aa6a1359ebe9e738d6b7b25389c942b1d7f8849981ff62ed3d8e"}, - {file = "msgspec-0.12.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d5a6a08fa1bd2b4e29b076c84ae6159a33f4256b88d6c6c55df9de04e225a5a"}, - {file = "msgspec-0.12.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b25a98e7f99dcb86ffec7b462222703fe87bc6e299be31d1a68a657dc7317498"}, - {file = "msgspec-0.12.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce9bda08bb1f9372d7da112cd697993f238fc22fbc72accd1dfb50eb22b68c23"}, - {file = "msgspec-0.12.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c905ba72400a0593c6244691d78e450036b8f54a05b9544740e47ed35f739af"}, - {file = "msgspec-0.12.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:04aff2b6404d54637170235983c67a231326a2b73a96a93f63c903f4a3e5701a"}, - {file = "msgspec-0.12.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8e784500de56c89db90f0b5c8043999dd128260aa4fd111fb3b65566140b7830"}, - {file = "msgspec-0.12.0-cp38-cp38-win_amd64.whl", hash = "sha256:318583cfad415d5c6bbb9e87a8a998de353146b64ac202c90a3d9396a5ea6b97"}, - {file = "msgspec-0.12.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:769e6d442969c0238c65b078b4962af19f4c1d875a4dc93267ed6cad4d887b47"}, - {file = "msgspec-0.12.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f71332e8969436ebc8bc3bb455d5c47a65ccaf236f7267e369959f2fcaf88bf3"}, - {file = "msgspec-0.12.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:faf7510ff87d086e21503f8504ca0550161fdfb1a025d9060a90a3e58d727be4"}, - {file = "msgspec-0.12.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2886e45bef04db383649e30fba56f2124c84ce6455deff6689e7dc9dc4926329"}, - {file = "msgspec-0.12.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7c281bd01931456cf01d553bdce315cf148bfa3565be01390f12800c39f75797"}, - {file = "msgspec-0.12.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:54c061e764f80c915fd86db8bfe48c88fc8c6047649fc8a5900a03dda745e600"}, - {file = "msgspec-0.12.0-cp39-cp39-win_amd64.whl", hash = "sha256:cc7f0555b4cb8c74cca6d13040b3fd9e7adafc0fff273203a13064453c28d32f"}, - {file = "msgspec-0.12.0.tar.gz", hash = "sha256:d8fe529a2414a1a5d3ccb1e875b164cc4f56614750c7b27c85006808f5658489"}, + {file = "MarkupSafe-2.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3028252424c72b2602a323f70fbf50aa80a5d3aa616ea6add4ba21ae9cc9da4c"}, + {file = "MarkupSafe-2.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:290b02bab3c9e216da57c1d11d2ba73a9f73a614bbdcc027d299a60cdfabb11a"}, + {file = "MarkupSafe-2.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e104c0c2b4cd765b4e83909cde7ec61a1e313f8a75775897db321450e928cce"}, + {file = "MarkupSafe-2.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24c3be29abb6b34052fd26fc7a8e0a49b1ee9d282e3665e8ad09a0a68faee5b3"}, + {file = "MarkupSafe-2.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:204730fd5fe2fe3b1e9ccadb2bd18ba8712b111dcabce185af0b3b5285a7c989"}, + {file = "MarkupSafe-2.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d3b64c65328cb4cd252c94f83e66e3d7acf8891e60ebf588d7b493a55a1dbf26"}, + {file = "MarkupSafe-2.1.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:96de1932237abe0a13ba68b63e94113678c379dca45afa040a17b6e1ad7ed076"}, + {file = "MarkupSafe-2.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:75bb36f134883fdbe13d8e63b8675f5f12b80bb6627f7714c7d6c5becf22719f"}, + {file = "MarkupSafe-2.1.0-cp310-cp310-win32.whl", hash = "sha256:4056f752015dfa9828dce3140dbadd543b555afb3252507348c493def166d454"}, + {file = "MarkupSafe-2.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:d4e702eea4a2903441f2735799d217f4ac1b55f7d8ad96ab7d4e25417cb0827c"}, + {file = "MarkupSafe-2.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:f0eddfcabd6936558ec020130f932d479930581171368fd728efcfb6ef0dd357"}, + {file = "MarkupSafe-2.1.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ddea4c352a488b5e1069069f2f501006b1a4362cb906bee9a193ef1245a7a61"}, + {file = "MarkupSafe-2.1.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:09c86c9643cceb1d87ca08cdc30160d1b7ab49a8a21564868921959bd16441b8"}, + {file = "MarkupSafe-2.1.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a0a0abef2ca47b33fb615b491ce31b055ef2430de52c5b3fb19a4042dbc5cadb"}, + {file = "MarkupSafe-2.1.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:736895a020e31b428b3382a7887bfea96102c529530299f426bf2e636aacec9e"}, + {file = "MarkupSafe-2.1.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:679cbb78914ab212c49c67ba2c7396dc599a8479de51b9a87b174700abd9ea49"}, + {file = "MarkupSafe-2.1.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:84ad5e29bf8bab3ad70fd707d3c05524862bddc54dc040982b0dbcff36481de7"}, + {file = "MarkupSafe-2.1.0-cp37-cp37m-win32.whl", hash = "sha256:8da5924cb1f9064589767b0f3fc39d03e3d0fb5aa29e0cb21d43106519bd624a"}, + {file = "MarkupSafe-2.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:454ffc1cbb75227d15667c09f164a0099159da0c1f3d2636aa648f12675491ad"}, + {file = "MarkupSafe-2.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:142119fb14a1ef6d758912b25c4e803c3ff66920635c44078666fe7cc3f8f759"}, + {file = "MarkupSafe-2.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b2a5a856019d2833c56a3dcac1b80fe795c95f401818ea963594b345929dffa7"}, + {file = "MarkupSafe-2.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d1fb9b2eec3c9714dd936860850300b51dbaa37404209c8d4cb66547884b7ed"}, + {file = "MarkupSafe-2.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:62c0285e91414f5c8f621a17b69fc0088394ccdaa961ef469e833dbff64bd5ea"}, + {file = "MarkupSafe-2.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fc3150f85e2dbcf99e65238c842d1cfe69d3e7649b19864c1cc043213d9cd730"}, + {file = "MarkupSafe-2.1.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f02cf7221d5cd915d7fa58ab64f7ee6dd0f6cddbb48683debf5d04ae9b1c2cc1"}, + {file = "MarkupSafe-2.1.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:d5653619b3eb5cbd35bfba3c12d575db2a74d15e0e1c08bf1db788069d410ce8"}, + {file = "MarkupSafe-2.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:7d2f5d97fcbd004c03df8d8fe2b973fe2b14e7bfeb2cfa012eaa8759ce9a762f"}, + {file = "MarkupSafe-2.1.0-cp38-cp38-win32.whl", hash = "sha256:3cace1837bc84e63b3fd2dfce37f08f8c18aeb81ef5cf6bb9b51f625cb4e6cd8"}, + {file = "MarkupSafe-2.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:fabbe18087c3d33c5824cb145ffca52eccd053061df1d79d4b66dafa5ad2a5ea"}, + {file = "MarkupSafe-2.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:023af8c54fe63530545f70dd2a2a7eed18d07a9a77b94e8bf1e2ff7f252db9a3"}, + {file = "MarkupSafe-2.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d66624f04de4af8bbf1c7f21cc06649c1c69a7f84109179add573ce35e46d448"}, + {file = "MarkupSafe-2.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c532d5ab79be0199fa2658e24a02fce8542df196e60665dd322409a03db6a52c"}, + {file = "MarkupSafe-2.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e67ec74fada3841b8c5f4c4f197bea916025cb9aa3fe5abf7d52b655d042f956"}, + {file = "MarkupSafe-2.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30c653fde75a6e5eb814d2a0a89378f83d1d3f502ab710904ee585c38888816c"}, + {file = "MarkupSafe-2.1.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:961eb86e5be7d0973789f30ebcf6caab60b844203f4396ece27310295a6082c7"}, + {file = "MarkupSafe-2.1.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:598b65d74615c021423bd45c2bc5e9b59539c875a9bdb7e5f2a6b92dfcfc268d"}, + {file = "MarkupSafe-2.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:599941da468f2cf22bf90a84f6e2a65524e87be2fce844f96f2dd9a6c9d1e635"}, + {file = "MarkupSafe-2.1.0-cp39-cp39-win32.whl", hash = "sha256:e6f7f3f41faffaea6596da86ecc2389672fa949bd035251eab26dc6697451d05"}, + {file = "MarkupSafe-2.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:b8811d48078d1cf2a6863dafb896e68406c5f513048451cd2ded0473133473c7"}, + {file = "MarkupSafe-2.1.0.tar.gz", hash = "sha256:80beaf63ddfbc64a0452b841d8036ca0611e049650e20afcb882f5d3c266d65f"}, +] +mccabe = [ + {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, + {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, ] multidict = [ - {file = "multidict-6.0.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0b1a97283e0c85772d613878028fec909f003993e1007eafa715b24b377cb9b8"}, - {file = "multidict-6.0.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:eeb6dcc05e911516ae3d1f207d4b0520d07f54484c49dfc294d6e7d63b734171"}, - {file = "multidict-6.0.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d6d635d5209b82a3492508cf5b365f3446afb65ae7ebd755e70e18f287b0adf7"}, - {file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c048099e4c9e9d615545e2001d3d8a4380bd403e1a0578734e0d31703d1b0c0b"}, - {file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ea20853c6dbbb53ed34cb4d080382169b6f4554d394015f1bef35e881bf83547"}, - {file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:16d232d4e5396c2efbbf4f6d4df89bfa905eb0d4dc5b3549d872ab898451f569"}, - {file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:36c63aaa167f6c6b04ef2c85704e93af16c11d20de1d133e39de6a0e84582a93"}, - {file = "multidict-6.0.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:64bdf1086b6043bf519869678f5f2757f473dee970d7abf6da91ec00acb9cb98"}, - {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:43644e38f42e3af682690876cff722d301ac585c5b9e1eacc013b7a3f7b696a0"}, - {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7582a1d1030e15422262de9f58711774e02fa80df0d1578995c76214f6954988"}, - {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:ddff9c4e225a63a5afab9dd15590432c22e8057e1a9a13d28ed128ecf047bbdc"}, - {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:ee2a1ece51b9b9e7752e742cfb661d2a29e7bcdba2d27e66e28a99f1890e4fa0"}, - {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a2e4369eb3d47d2034032a26c7a80fcb21a2cb22e1173d761a162f11e562caa5"}, - {file = "multidict-6.0.4-cp310-cp310-win32.whl", hash = "sha256:574b7eae1ab267e5f8285f0fe881f17efe4b98c39a40858247720935b893bba8"}, - {file = "multidict-6.0.4-cp310-cp310-win_amd64.whl", hash = "sha256:4dcbb0906e38440fa3e325df2359ac6cb043df8e58c965bb45f4e406ecb162cc"}, - {file = "multidict-6.0.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0dfad7a5a1e39c53ed00d2dd0c2e36aed4650936dc18fd9a1826a5ae1cad6f03"}, - {file = "multidict-6.0.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:64da238a09d6039e3bd39bb3aee9c21a5e34f28bfa5aa22518581f910ff94af3"}, - {file = "multidict-6.0.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ff959bee35038c4624250473988b24f846cbeb2c6639de3602c073f10410ceba"}, - {file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:01a3a55bd90018c9c080fbb0b9f4891db37d148a0a18722b42f94694f8b6d4c9"}, - {file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c5cb09abb18c1ea940fb99360ea0396f34d46566f157122c92dfa069d3e0e982"}, - {file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:666daae833559deb2d609afa4490b85830ab0dfca811a98b70a205621a6109fe"}, - {file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11bdf3f5e1518b24530b8241529d2050014c884cf18b6fc69c0c2b30ca248710"}, - {file = "multidict-6.0.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7d18748f2d30f94f498e852c67d61261c643b349b9d2a581131725595c45ec6c"}, - {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:458f37be2d9e4c95e2d8866a851663cbc76e865b78395090786f6cd9b3bbf4f4"}, - {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:b1a2eeedcead3a41694130495593a559a668f382eee0727352b9a41e1c45759a"}, - {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:7d6ae9d593ef8641544d6263c7fa6408cc90370c8cb2bbb65f8d43e5b0351d9c"}, - {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:5979b5632c3e3534e42ca6ff856bb24b2e3071b37861c2c727ce220d80eee9ed"}, - {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:dcfe792765fab89c365123c81046ad4103fcabbc4f56d1c1997e6715e8015461"}, - {file = "multidict-6.0.4-cp311-cp311-win32.whl", hash = "sha256:3601a3cece3819534b11d4efc1eb76047488fddd0c85a3948099d5da4d504636"}, - {file = "multidict-6.0.4-cp311-cp311-win_amd64.whl", hash = "sha256:81a4f0b34bd92df3da93315c6a59034df95866014ac08535fc819f043bfd51f0"}, - {file = "multidict-6.0.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:67040058f37a2a51ed8ea8f6b0e6ee5bd78ca67f169ce6122f3e2ec80dfe9b78"}, - {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:853888594621e6604c978ce2a0444a1e6e70c8d253ab65ba11657659dcc9100f"}, - {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:39ff62e7d0f26c248b15e364517a72932a611a9b75f35b45be078d81bdb86603"}, - {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:af048912e045a2dc732847d33821a9d84ba553f5c5f028adbd364dd4765092ac"}, - {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1e8b901e607795ec06c9e42530788c45ac21ef3aaa11dbd0c69de543bfb79a9"}, - {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62501642008a8b9871ddfccbf83e4222cf8ac0d5aeedf73da36153ef2ec222d2"}, - {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:99b76c052e9f1bc0721f7541e5e8c05db3941eb9ebe7b8553c625ef88d6eefde"}, - {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:509eac6cf09c794aa27bcacfd4d62c885cce62bef7b2c3e8b2e49d365b5003fe"}, - {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:21a12c4eb6ddc9952c415f24eef97e3e55ba3af61f67c7bc388dcdec1404a067"}, - {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:5cad9430ab3e2e4fa4a2ef4450f548768400a2ac635841bc2a56a2052cdbeb87"}, - {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ab55edc2e84460694295f401215f4a58597f8f7c9466faec545093045476327d"}, - {file = "multidict-6.0.4-cp37-cp37m-win32.whl", hash = "sha256:5a4dcf02b908c3b8b17a45fb0f15b695bf117a67b76b7ad18b73cf8e92608775"}, - {file = "multidict-6.0.4-cp37-cp37m-win_amd64.whl", hash = "sha256:6ed5f161328b7df384d71b07317f4d8656434e34591f20552c7bcef27b0ab88e"}, - {file = "multidict-6.0.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5fc1b16f586f049820c5c5b17bb4ee7583092fa0d1c4e28b5239181ff9532e0c"}, - {file = "multidict-6.0.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1502e24330eb681bdaa3eb70d6358e818e8e8f908a22a1851dfd4e15bc2f8161"}, - {file = "multidict-6.0.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b692f419760c0e65d060959df05f2a531945af31fda0c8a3b3195d4efd06de11"}, - {file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45e1ecb0379bfaab5eef059f50115b54571acfbe422a14f668fc8c27ba410e7e"}, - {file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ddd3915998d93fbcd2566ddf9cf62cdb35c9e093075f862935573d265cf8f65d"}, - {file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:59d43b61c59d82f2effb39a93c48b845efe23a3852d201ed2d24ba830d0b4cf2"}, - {file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc8e1d0c705233c5dd0c5e6460fbad7827d5d36f310a0fadfd45cc3029762258"}, - {file = "multidict-6.0.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d6aa0418fcc838522256761b3415822626f866758ee0bc6632c9486b179d0b52"}, - {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6748717bb10339c4760c1e63da040f5f29f5ed6e59d76daee30305894069a660"}, - {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:4d1a3d7ef5e96b1c9e92f973e43aa5e5b96c659c9bc3124acbbd81b0b9c8a951"}, - {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4372381634485bec7e46718edc71528024fcdc6f835baefe517b34a33c731d60"}, - {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:fc35cb4676846ef752816d5be2193a1e8367b4c1397b74a565a9d0389c433a1d"}, - {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:4b9d9e4e2b37daddb5c23ea33a3417901fa7c7b3dee2d855f63ee67a0b21e5b1"}, - {file = "multidict-6.0.4-cp38-cp38-win32.whl", hash = "sha256:e41b7e2b59679edfa309e8db64fdf22399eec4b0b24694e1b2104fb789207779"}, - {file = "multidict-6.0.4-cp38-cp38-win_amd64.whl", hash = "sha256:d6c254ba6e45d8e72739281ebc46ea5eb5f101234f3ce171f0e9f5cc86991480"}, - {file = "multidict-6.0.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:16ab77bbeb596e14212e7bab8429f24c1579234a3a462105cda4a66904998664"}, - {file = "multidict-6.0.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bc779e9e6f7fda81b3f9aa58e3a6091d49ad528b11ed19f6621408806204ad35"}, - {file = "multidict-6.0.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4ceef517eca3e03c1cceb22030a3e39cb399ac86bff4e426d4fc6ae49052cc60"}, - {file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:281af09f488903fde97923c7744bb001a9b23b039a909460d0f14edc7bf59706"}, - {file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:52f2dffc8acaba9a2f27174c41c9e57f60b907bb9f096b36b1a1f3be71c6284d"}, - {file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b41156839806aecb3641f3208c0dafd3ac7775b9c4c422d82ee2a45c34ba81ca"}, - {file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d5e3fc56f88cc98ef8139255cf8cd63eb2c586531e43310ff859d6bb3a6b51f1"}, - {file = "multidict-6.0.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8316a77808c501004802f9beebde51c9f857054a0c871bd6da8280e718444449"}, - {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f70b98cd94886b49d91170ef23ec5c0e8ebb6f242d734ed7ed677b24d50c82cf"}, - {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bf6774e60d67a9efe02b3616fee22441d86fab4c6d335f9d2051d19d90a40063"}, - {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:e69924bfcdda39b722ef4d9aa762b2dd38e4632b3641b1d9a57ca9cd18f2f83a"}, - {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:6b181d8c23da913d4ff585afd1155a0e1194c0b50c54fcfe286f70cdaf2b7176"}, - {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:52509b5be062d9eafc8170e53026fbc54cf3b32759a23d07fd935fb04fc22d95"}, - {file = "multidict-6.0.4-cp39-cp39-win32.whl", hash = "sha256:27c523fbfbdfd19c6867af7346332b62b586eed663887392cff78d614f9ec313"}, - {file = "multidict-6.0.4-cp39-cp39-win_amd64.whl", hash = "sha256:33029f5734336aa0d4c0384525da0387ef89148dc7191aae00ca5fb23d7aafc2"}, - {file = "multidict-6.0.4.tar.gz", hash = "sha256:3666906492efb76453c0e7b97f2cf459b0682e7402c0489a95484965dbc1da49"}, + {file = "multidict-5.2.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3822c5894c72e3b35aae9909bef66ec83e44522faf767c0ad39e0e2de11d3b55"}, + {file = "multidict-5.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:28e6d883acd8674887d7edc896b91751dc2d8e87fbdca8359591a13872799e4e"}, + {file = "multidict-5.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b61f85101ef08cbbc37846ac0e43f027f7844f3fade9b7f6dd087178caedeee7"}, + {file = "multidict-5.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d9b668c065968c5979fe6b6fa6760bb6ab9aeb94b75b73c0a9c1acf6393ac3bf"}, + {file = "multidict-5.2.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:517d75522b7b18a3385726b54a081afd425d4f41144a5399e5abd97ccafdf36b"}, + {file = "multidict-5.2.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1b4ac3ba7a97b35a5ccf34f41b5a8642a01d1e55454b699e5e8e7a99b5a3acf5"}, + {file = "multidict-5.2.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:df23c83398715b26ab09574217ca21e14694917a0c857e356fd39e1c64f8283f"}, + {file = "multidict-5.2.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e58a9b5cc96e014ddf93c2227cbdeca94b56a7eb77300205d6e4001805391747"}, + {file = "multidict-5.2.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:f76440e480c3b2ca7f843ff8a48dc82446b86ed4930552d736c0bac507498a52"}, + {file = "multidict-5.2.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:cfde464ca4af42a629648c0b0d79b8f295cf5b695412451716531d6916461628"}, + {file = "multidict-5.2.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:0fed465af2e0eb6357ba95795d003ac0bdb546305cc2366b1fc8f0ad67cc3fda"}, + {file = "multidict-5.2.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:b70913cbf2e14275013be98a06ef4b412329fe7b4f83d64eb70dce8269ed1e1a"}, + {file = "multidict-5.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a5635bcf1b75f0f6ef3c8a1ad07b500104a971e38d3683167b9454cb6465ac86"}, + {file = "multidict-5.2.0-cp310-cp310-win32.whl", hash = "sha256:77f0fb7200cc7dedda7a60912f2059086e29ff67cefbc58d2506638c1a9132d7"}, + {file = "multidict-5.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:9416cf11bcd73c861267e88aea71e9fcc35302b3943e45e1dbb4317f91a4b34f"}, + {file = "multidict-5.2.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:fd77c8f3cba815aa69cb97ee2b2ef385c7c12ada9c734b0f3b32e26bb88bbf1d"}, + {file = "multidict-5.2.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:98ec9aea6223adf46999f22e2c0ab6cf33f5914be604a404f658386a8f1fba37"}, + {file = "multidict-5.2.0-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e5283c0a00f48e8cafcecadebfa0ed1dac8b39e295c7248c44c665c16dc1138b"}, + {file = "multidict-5.2.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5f79c19c6420962eb17c7e48878a03053b7ccd7b69f389d5831c0a4a7f1ac0a1"}, + {file = "multidict-5.2.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:e4a67f1080123de76e4e97a18d10350df6a7182e243312426d508712e99988d4"}, + {file = "multidict-5.2.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:94b117e27efd8e08b4046c57461d5a114d26b40824995a2eb58372b94f9fca02"}, + {file = "multidict-5.2.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:2e77282fd1d677c313ffcaddfec236bf23f273c4fba7cdf198108f5940ae10f5"}, + {file = "multidict-5.2.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:116347c63ba049c1ea56e157fa8aa6edaf5e92925c9b64f3da7769bdfa012858"}, + {file = "multidict-5.2.0-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:dc3a866cf6c13d59a01878cd806f219340f3e82eed514485e094321f24900677"}, + {file = "multidict-5.2.0-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:ac42181292099d91217a82e3fa3ce0e0ddf3a74fd891b7c2b347a7f5aa0edded"}, + {file = "multidict-5.2.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:f0bb0973f42ffcb5e3537548e0767079420aefd94ba990b61cf7bb8d47f4916d"}, + {file = "multidict-5.2.0-cp36-cp36m-win32.whl", hash = "sha256:ea21d4d5104b4f840b91d9dc8cbc832aba9612121eaba503e54eaab1ad140eb9"}, + {file = "multidict-5.2.0-cp36-cp36m-win_amd64.whl", hash = "sha256:e6453f3cbeb78440747096f239d282cc57a2997a16b5197c9bc839099e1633d0"}, + {file = "multidict-5.2.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:d3def943bfd5f1c47d51fd324df1e806d8da1f8e105cc7f1c76a1daf0f7e17b0"}, + {file = "multidict-5.2.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:35591729668a303a02b06e8dba0eb8140c4a1bfd4c4b3209a436a02a5ac1de11"}, + {file = "multidict-5.2.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce8cacda0b679ebc25624d5de66c705bc53dcc7c6f02a7fb0f3ca5e227d80422"}, + {file = "multidict-5.2.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:baf1856fab8212bf35230c019cde7c641887e3fc08cadd39d32a421a30151ea3"}, + {file = "multidict-5.2.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:a43616aec0f0d53c411582c451f5d3e1123a68cc7b3475d6f7d97a626f8ff90d"}, + {file = "multidict-5.2.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:25cbd39a9029b409167aa0a20d8a17f502d43f2efebfe9e3ac019fe6796c59ac"}, + {file = "multidict-5.2.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:0a2cbcfbea6dc776782a444db819c8b78afe4db597211298dd8b2222f73e9cd0"}, + {file = "multidict-5.2.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:3d2d7d1fff8e09d99354c04c3fd5b560fb04639fd45926b34e27cfdec678a704"}, + {file = "multidict-5.2.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:a37e9a68349f6abe24130846e2f1d2e38f7ddab30b81b754e5a1fde32f782b23"}, + {file = "multidict-5.2.0-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:637c1896497ff19e1ee27c1c2c2ddaa9f2d134bbb5e0c52254361ea20486418d"}, + {file = "multidict-5.2.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:9815765f9dcda04921ba467957be543423e5ec6a1136135d84f2ae092c50d87b"}, + {file = "multidict-5.2.0-cp37-cp37m-win32.whl", hash = "sha256:8b911d74acdc1fe2941e59b4f1a278a330e9c34c6c8ca1ee21264c51ec9b67ef"}, + {file = "multidict-5.2.0-cp37-cp37m-win_amd64.whl", hash = "sha256:380b868f55f63d048a25931a1632818f90e4be71d2081c2338fcf656d299949a"}, + {file = "multidict-5.2.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e7d81ce5744757d2f05fc41896e3b2ae0458464b14b5a2c1e87a6a9d69aefaa8"}, + {file = "multidict-5.2.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2d1d55cdf706ddc62822d394d1df53573d32a7a07d4f099470d3cb9323b721b6"}, + {file = "multidict-5.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a4771d0d0ac9d9fe9e24e33bed482a13dfc1256d008d101485fe460359476065"}, + {file = "multidict-5.2.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da7d57ea65744d249427793c042094c4016789eb2562576fb831870f9c878d9e"}, + {file = "multidict-5.2.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cdd68778f96216596218b4e8882944d24a634d984ee1a5a049b300377878fa7c"}, + {file = "multidict-5.2.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ecc99bce8ee42dcad15848c7885197d26841cb24fa2ee6e89d23b8993c871c64"}, + {file = "multidict-5.2.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:067150fad08e6f2dd91a650c7a49ba65085303fcc3decbd64a57dc13a2733031"}, + {file = "multidict-5.2.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:78c106b2b506b4d895ddc801ff509f941119394b89c9115580014127414e6c2d"}, + {file = "multidict-5.2.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:e6c4fa1ec16e01e292315ba76eb1d012c025b99d22896bd14a66628b245e3e01"}, + {file = "multidict-5.2.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:b227345e4186809d31f22087d0265655114af7cda442ecaf72246275865bebe4"}, + {file = "multidict-5.2.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:06560fbdcf22c9387100979e65b26fba0816c162b888cb65b845d3def7a54c9b"}, + {file = "multidict-5.2.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:7878b61c867fb2df7a95e44b316f88d5a3742390c99dfba6c557a21b30180cac"}, + {file = "multidict-5.2.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:246145bff76cc4b19310f0ad28bd0769b940c2a49fc601b86bfd150cbd72bb22"}, + {file = "multidict-5.2.0-cp38-cp38-win32.whl", hash = "sha256:c30ac9f562106cd9e8071c23949a067b10211917fdcb75b4718cf5775356a940"}, + {file = "multidict-5.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:f19001e790013ed580abfde2a4465388950728861b52f0da73e8e8a9418533c0"}, + {file = "multidict-5.2.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c1ff762e2ee126e6f1258650ac641e2b8e1f3d927a925aafcfde943b77a36d24"}, + {file = "multidict-5.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bd6c9c50bf2ad3f0448edaa1a3b55b2e6866ef8feca5d8dbec10ec7c94371d21"}, + {file = "multidict-5.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fc66d4016f6e50ed36fb39cd287a3878ffcebfa90008535c62e0e90a7ab713ae"}, + {file = "multidict-5.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9acb76d5f3dd9421874923da2ed1e76041cb51b9337fd7f507edde1d86535d6"}, + {file = "multidict-5.2.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dfc924a7e946dd3c6360e50e8f750d51e3ef5395c95dc054bc9eab0f70df4f9c"}, + {file = "multidict-5.2.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:32fdba7333eb2351fee2596b756d730d62b5827d5e1ab2f84e6cbb287cc67fe0"}, + {file = "multidict-5.2.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:b9aad49466b8d828b96b9e3630006234879c8d3e2b0a9d99219b3121bc5cdb17"}, + {file = "multidict-5.2.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:93de39267c4c676c9ebb2057e98a8138bade0d806aad4d864322eee0803140a0"}, + {file = "multidict-5.2.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f9bef5cff994ca3026fcc90680e326d1a19df9841c5e3d224076407cc21471a1"}, + {file = "multidict-5.2.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:5f841c4f14331fd1e36cbf3336ed7be2cb2a8f110ce40ea253e5573387db7621"}, + {file = "multidict-5.2.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:38ba256ee9b310da6a1a0f013ef4e422fca30a685bcbec86a969bd520504e341"}, + {file = "multidict-5.2.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:3bc3b1621b979621cee9f7b09f024ec76ec03cc365e638126a056317470bde1b"}, + {file = "multidict-5.2.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:6ee908c070020d682e9b42c8f621e8bb10c767d04416e2ebe44e37d0f44d9ad5"}, + {file = "multidict-5.2.0-cp39-cp39-win32.whl", hash = "sha256:1c7976cd1c157fa7ba5456ae5d31ccdf1479680dc9b8d8aa28afabc370df42b8"}, + {file = "multidict-5.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:c9631c642e08b9fff1c6255487e62971d8b8e821808ddd013d8ac058087591ac"}, + {file = "multidict-5.2.0.tar.gz", hash = "sha256:0dd1c93edb444b33ba2274b66f63def8a327d607c6c790772f448a53b6ea59ce"}, ] mypy = [ - {file = "mypy-0.991-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7d17e0a9707d0772f4a7b878f04b4fd11f6f5bcb9b3813975a9b13c9332153ab"}, - {file = "mypy-0.991-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0714258640194d75677e86c786e80ccf294972cc76885d3ebbb560f11db0003d"}, - {file = "mypy-0.991-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0c8f3be99e8a8bd403caa8c03be619544bc2c77a7093685dcf308c6b109426c6"}, - {file = "mypy-0.991-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc9ec663ed6c8f15f4ae9d3c04c989b744436c16d26580eaa760ae9dd5d662eb"}, - {file = "mypy-0.991-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4307270436fd7694b41f913eb09210faff27ea4979ecbcd849e57d2da2f65305"}, - {file = "mypy-0.991-cp310-cp310-win_amd64.whl", hash = "sha256:901c2c269c616e6cb0998b33d4adbb4a6af0ac4ce5cd078afd7bc95830e62c1c"}, - {file = "mypy-0.991-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d13674f3fb73805ba0c45eb6c0c3053d218aa1f7abead6e446d474529aafc372"}, - {file = "mypy-0.991-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1c8cd4fb70e8584ca1ed5805cbc7c017a3d1a29fb450621089ffed3e99d1857f"}, - {file = "mypy-0.991-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:209ee89fbb0deed518605edddd234af80506aec932ad28d73c08f1400ef80a33"}, - {file = "mypy-0.991-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:37bd02ebf9d10e05b00d71302d2c2e6ca333e6c2a8584a98c00e038db8121f05"}, - {file = "mypy-0.991-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:26efb2fcc6b67e4d5a55561f39176821d2adf88f2745ddc72751b7890f3194ad"}, - {file = "mypy-0.991-cp311-cp311-win_amd64.whl", hash = "sha256:3a700330b567114b673cf8ee7388e949f843b356a73b5ab22dd7cff4742a5297"}, - {file = "mypy-0.991-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:1f7d1a520373e2272b10796c3ff721ea1a0712288cafaa95931e66aa15798813"}, - {file = "mypy-0.991-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:641411733b127c3e0dab94c45af15fea99e4468f99ac88b39efb1ad677da5711"}, - {file = "mypy-0.991-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:3d80e36b7d7a9259b740be6d8d906221789b0d836201af4234093cae89ced0cd"}, - {file = "mypy-0.991-cp37-cp37m-win_amd64.whl", hash = "sha256:e62ebaad93be3ad1a828a11e90f0e76f15449371ffeecca4a0a0b9adc99abcef"}, - {file = "mypy-0.991-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:b86ce2c1866a748c0f6faca5232059f881cda6dda2a893b9a8373353cfe3715a"}, - {file = "mypy-0.991-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ac6e503823143464538efda0e8e356d871557ef60ccd38f8824a4257acc18d93"}, - {file = "mypy-0.991-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:0cca5adf694af539aeaa6ac633a7afe9bbd760df9d31be55ab780b77ab5ae8bf"}, - {file = "mypy-0.991-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a12c56bf73cdab116df96e4ff39610b92a348cc99a1307e1da3c3768bbb5b135"}, - {file = "mypy-0.991-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:652b651d42f155033a1967739788c436491b577b6a44e4c39fb340d0ee7f0d70"}, - {file = "mypy-0.991-cp38-cp38-win_amd64.whl", hash = "sha256:4175593dc25d9da12f7de8de873a33f9b2b8bdb4e827a7cae952e5b1a342e243"}, - {file = "mypy-0.991-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:98e781cd35c0acf33eb0295e8b9c55cdbef64fcb35f6d3aa2186f289bed6e80d"}, - {file = "mypy-0.991-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6d7464bac72a85cb3491c7e92b5b62f3dcccb8af26826257760a552a5e244aa5"}, - {file = "mypy-0.991-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c9166b3f81a10cdf9b49f2d594b21b31adadb3d5e9db9b834866c3258b695be3"}, - {file = "mypy-0.991-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8472f736a5bfb159a5e36740847808f6f5b659960115ff29c7cecec1741c648"}, - {file = "mypy-0.991-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5e80e758243b97b618cdf22004beb09e8a2de1af481382e4d84bc52152d1c476"}, - {file = "mypy-0.991-cp39-cp39-win_amd64.whl", hash = "sha256:74e259b5c19f70d35fcc1ad3d56499065c601dfe94ff67ae48b85596b9ec1461"}, - {file = "mypy-0.991-py3-none-any.whl", hash = "sha256:de32edc9b0a7e67c2775e574cb061a537660e51210fbf6006b0b36ea695ae9bb"}, - {file = "mypy-0.991.tar.gz", hash = "sha256:3c0165ba8f354a6d9881809ef29f1a9318a236a6d81c690094c5df32107bde06"}, + {file = "mypy-0.931-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3c5b42d0815e15518b1f0990cff7a705805961613e701db60387e6fb663fe78a"}, + {file = "mypy-0.931-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c89702cac5b302f0c5d33b172d2b55b5df2bede3344a2fbed99ff96bddb2cf00"}, + {file = "mypy-0.931-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:300717a07ad09525401a508ef5d105e6b56646f7942eb92715a1c8d610149714"}, + {file = "mypy-0.931-cp310-cp310-win_amd64.whl", hash = "sha256:7b3f6f557ba4afc7f2ce6d3215d5db279bcf120b3cfd0add20a5d4f4abdae5bc"}, + {file = "mypy-0.931-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:1bf752559797c897cdd2c65f7b60c2b6969ffe458417b8d947b8340cc9cec08d"}, + {file = "mypy-0.931-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4365c60266b95a3f216a3047f1d8e3f895da6c7402e9e1ddfab96393122cc58d"}, + {file = "mypy-0.931-cp36-cp36m-win_amd64.whl", hash = "sha256:1b65714dc296a7991000b6ee59a35b3f550e0073411ac9d3202f6516621ba66c"}, + {file = "mypy-0.931-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e839191b8da5b4e5d805f940537efcaa13ea5dd98418f06dc585d2891d228cf0"}, + {file = "mypy-0.931-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:50c7346a46dc76a4ed88f3277d4959de8a2bd0a0fa47fa87a4cde36fe247ac05"}, + {file = "mypy-0.931-cp37-cp37m-win_amd64.whl", hash = "sha256:d8f1ff62f7a879c9fe5917b3f9eb93a79b78aad47b533911b853a757223f72e7"}, + {file = "mypy-0.931-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f9fe20d0872b26c4bba1c1be02c5340de1019530302cf2dcc85c7f9fc3252ae0"}, + {file = "mypy-0.931-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:1b06268df7eb53a8feea99cbfff77a6e2b205e70bf31743e786678ef87ee8069"}, + {file = "mypy-0.931-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8c11003aaeaf7cc2d0f1bc101c1cc9454ec4cc9cb825aef3cafff8a5fdf4c799"}, + {file = "mypy-0.931-cp38-cp38-win_amd64.whl", hash = "sha256:d9d2b84b2007cea426e327d2483238f040c49405a6bf4074f605f0156c91a47a"}, + {file = "mypy-0.931-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ff3bf387c14c805ab1388185dd22d6b210824e164d4bb324b195ff34e322d166"}, + {file = "mypy-0.931-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5b56154f8c09427bae082b32275a21f500b24d93c88d69a5e82f3978018a0266"}, + {file = "mypy-0.931-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8ca7f8c4b1584d63c9a0f827c37ba7a47226c19a23a753d52e5b5eddb201afcd"}, + {file = "mypy-0.931-cp39-cp39-win_amd64.whl", hash = "sha256:74f7eccbfd436abe9c352ad9fb65872cc0f1f0a868e9d9c44db0893440f0c697"}, + {file = "mypy-0.931-py3-none-any.whl", hash = "sha256:1171f2e0859cfff2d366da2c7092b06130f232c636a3f7301e3feb8b41f6377d"}, + {file = "mypy-0.931.tar.gz", hash = "sha256:0038b21890867793581e4cb0d810829f5fd4441aa75796b53033af3aa30430ce"}, ] mypy-extensions = [ {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, ] +nodeenv = [ + {file = "nodeenv-1.6.0-py2.py3-none-any.whl", hash = "sha256:621e6b7076565ddcacd2db0294c0381e01fd28945ab36bcf00f41c5daf63bef7"}, + {file = "nodeenv-1.6.0.tar.gz", hash = "sha256:3ef13ff90291ba2a4a7a4ff9a979b63ffdd00a464dbe04acf0ea6471517a4c2b"}, +] opentelemetry-api = [ - {file = "opentelemetry_api-1.15.0-py3-none-any.whl", hash = "sha256:e6c2d2e42140fd396e96edf75a7ceb11073f4efb4db87565a431cc9d0f93f2e0"}, - {file = "opentelemetry_api-1.15.0.tar.gz", hash = "sha256:79ab791b4aaad27acc3dc3ba01596db5b5aac2ef75c70622c6038051d6c2cded"}, + {file = "opentelemetry-api-1.9.1.tar.gz", hash = "sha256:67d6685effc8507aae9ef1f5947a7a9cc3ad6c7734fa0876179f40802225fc32"}, + {file = "opentelemetry_api-1.9.1-py3-none-any.whl", hash = "sha256:36441aa25d2a17ac2033b5e609cde563f9cd8171f7cf433670140626337a04db"}, ] opentelemetry-sdk = [ - {file = "opentelemetry_sdk-1.15.0-py3-none-any.whl", hash = "sha256:555c533e9837766119bbccc7a80458c9971d853a6f1da683a2246cd5e53b4645"}, - {file = "opentelemetry_sdk-1.15.0.tar.gz", hash = "sha256:98dbffcfeebcbff12c0c974292d6ea603180a145904cf838b1fe4d5c99078425"}, + {file = "opentelemetry-sdk-1.9.1.tar.gz", hash = "sha256:bb064e6ed867c819d152b2efe64bde296e34c222257df76c3d8bb4385d1559a9"}, + {file = "opentelemetry_sdk-1.9.1-py3-none-any.whl", hash = "sha256:aeeffd090f1ffee1a1f9fbafde61eece7aaceaaeeca6e128bd942cb8650b8968"}, ] opentelemetry-semantic-conventions = [ - {file = "opentelemetry_semantic_conventions-0.36b0-py3-none-any.whl", hash = "sha256:adc05635e87b9d3e007c9f530eed487fc3ef2177d02f82f674f28ebf9aff8243"}, - {file = "opentelemetry_semantic_conventions-0.36b0.tar.gz", hash = "sha256:829dc221795467d98b773c04096e29be038d77526dc8d6ac76f546fb6279bf01"}, + {file = "opentelemetry-semantic-conventions-0.28b1.tar.gz", hash = "sha256:9dbc89ca091aba6dcd5f48566242f9063b7f272bc46271f804c707348516cce7"}, + {file = "opentelemetry_semantic_conventions-0.28b1-py3-none-any.whl", hash = "sha256:f1e2c0e1e445f19c166a9888025823af8a02d00358e138e84cf8d63b4859ef47"}, ] packaging = [ - {file = "packaging-23.0-py3-none-any.whl", hash = "sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2"}, - {file = "packaging-23.0.tar.gz", hash = "sha256:b6ad297f8907de0fa2fe1ccbd26fdaf387f5f47c7275fedf8cce89f99446cf97"}, + {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, + {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, ] pathspec = [ - {file = "pathspec-0.10.3-py3-none-any.whl", hash = "sha256:3c95343af8b756205e2aba76e843ba9520a24dd84f68c22b9f93251507509dd6"}, - {file = "pathspec-0.10.3.tar.gz", hash = "sha256:56200de4077d9d0791465aa9095a01d421861e405b5096955051deefd697d6f6"}, -] -pip = [ - {file = "pip-22.2.2-py3-none-any.whl", hash = "sha256:b61a374b5bc40a6e982426aede40c9b5a08ff20e640f5b56977f4f91fed1e39a"}, - {file = "pip-22.2.2.tar.gz", hash = "sha256:3fd1929db052f056d7a998439176d3333fa1b3f6c1ad881de1885c0717608a4b"}, -] -pkgutil-resolve-name = [ - {file = "pkgutil_resolve_name-1.3.10-py3-none-any.whl", hash = "sha256:ca27cc078d25c5ad71a9de0a7a330146c4e014c2462d9af19c6b828280649c5e"}, - {file = "pkgutil_resolve_name-1.3.10.tar.gz", hash = "sha256:357d6c9e6a755653cfd78893817c0853af365dd51ec97f3d358a819373bbd174"}, + {file = "pathspec-0.9.0-py2.py3-none-any.whl", hash = "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a"}, + {file = "pathspec-0.9.0.tar.gz", hash = "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"}, ] platformdirs = [ - {file = "platformdirs-2.6.2-py3-none-any.whl", hash = "sha256:83c8f6d04389165de7c9b6f0c682439697887bca0aa2f1c87ef1826be3584490"}, - {file = "platformdirs-2.6.2.tar.gz", hash = "sha256:e1fea1fe471b9ff8332e229df3cb7de4f53eeea4998d3b6bfff542115e998bd2"}, + {file = "platformdirs-2.5.0-py3-none-any.whl", hash = "sha256:30671902352e97b1eafd74ade8e4a694782bd3471685e78c32d0fdfd3aa7e7bb"}, + {file = "platformdirs-2.5.0.tar.gz", hash = "sha256:8ec11dfba28ecc0715eb5fb0147a87b1bf325f349f3da9aab2cd6b50b96b692b"}, ] pluggy = [ {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, ] -protobuf = [ - {file = "protobuf-4.21.12-cp310-abi3-win32.whl", hash = "sha256:b135410244ebe777db80298297a97fbb4c862c881b4403b71bac9d4107d61fd1"}, - {file = "protobuf-4.21.12-cp310-abi3-win_amd64.whl", hash = "sha256:89f9149e4a0169cddfc44c74f230d7743002e3aa0b9472d8c28f0388102fc4c2"}, - {file = "protobuf-4.21.12-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:299ea899484ee6f44604deb71f424234f654606b983cb496ea2a53e3c63ab791"}, - {file = "protobuf-4.21.12-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:d1736130bce8cf131ac7957fa26880ca19227d4ad68b4888b3be0dea1f95df97"}, - {file = "protobuf-4.21.12-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:78a28c9fa223998472886c77042e9b9afb6fe4242bd2a2a5aced88e3f4422aa7"}, - {file = "protobuf-4.21.12-cp37-cp37m-win32.whl", hash = "sha256:3d164928ff0727d97022957c2b849250ca0e64777ee31efd7d6de2e07c494717"}, - {file = "protobuf-4.21.12-cp37-cp37m-win_amd64.whl", hash = "sha256:f45460f9ee70a0ec1b6694c6e4e348ad2019275680bd68a1d9314b8c7e01e574"}, - {file = "protobuf-4.21.12-cp38-cp38-win32.whl", hash = "sha256:6ab80df09e3208f742c98443b6166bcb70d65f52cfeb67357d52032ea1ae9bec"}, - {file = "protobuf-4.21.12-cp38-cp38-win_amd64.whl", hash = "sha256:1f22ac0ca65bb70a876060d96d914dae09ac98d114294f77584b0d2644fa9c30"}, - {file = "protobuf-4.21.12-cp39-cp39-win32.whl", hash = "sha256:27f4d15021da6d2b706ddc3860fac0a5ddaba34ab679dc182b60a8bb4e1121cc"}, - {file = "protobuf-4.21.12-cp39-cp39-win_amd64.whl", hash = "sha256:237216c3326d46808a9f7c26fd1bd4b20015fb6867dc5d263a493ef9a539293b"}, - {file = "protobuf-4.21.12-py2.py3-none-any.whl", hash = "sha256:a53fd3f03e578553623272dc46ac2f189de23862e68565e83dde203d41b76fc5"}, - {file = "protobuf-4.21.12-py3-none-any.whl", hash = "sha256:b98d0148f84e3a3c569e19f52103ca1feacdac0d2df8d6533cf983d1fda28462"}, - {file = "protobuf-4.21.12.tar.gz", hash = "sha256:7cd532c4566d0e6feafecc1059d04c7915aec8e182d1cf7adee8b24ef1e2e6ab"}, +pre-commit = [ + {file = "pre_commit-2.17.0-py2.py3-none-any.whl", hash = "sha256:725fa7459782d7bec5ead072810e47351de01709be838c2ce1726b9591dad616"}, + {file = "pre_commit-2.17.0.tar.gz", hash = "sha256:c1a8040ff15ad3d648c70cc3e55b93e4d2d5b687320955505587fd79bbaed06a"}, ] psutil = [ - {file = "psutil-5.9.4-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:c1ca331af862803a42677c120aff8a814a804e09832f166f226bfd22b56feee8"}, - {file = "psutil-5.9.4-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:68908971daf802203f3d37e78d3f8831b6d1014864d7a85937941bb35f09aefe"}, - {file = "psutil-5.9.4-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:3ff89f9b835100a825b14c2808a106b6fdcc4b15483141482a12c725e7f78549"}, - {file = "psutil-5.9.4-cp27-cp27m-win32.whl", hash = "sha256:852dd5d9f8a47169fe62fd4a971aa07859476c2ba22c2254d4a1baa4e10b95ad"}, - {file = "psutil-5.9.4-cp27-cp27m-win_amd64.whl", hash = "sha256:9120cd39dca5c5e1c54b59a41d205023d436799b1c8c4d3ff71af18535728e94"}, - {file = "psutil-5.9.4-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:6b92c532979bafc2df23ddc785ed116fced1f492ad90a6830cf24f4d1ea27d24"}, - {file = "psutil-5.9.4-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:efeae04f9516907be44904cc7ce08defb6b665128992a56957abc9b61dca94b7"}, - {file = "psutil-5.9.4-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:54d5b184728298f2ca8567bf83c422b706200bcbbfafdc06718264f9393cfeb7"}, - {file = "psutil-5.9.4-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:16653106f3b59386ffe10e0bad3bb6299e169d5327d3f187614b1cb8f24cf2e1"}, - {file = "psutil-5.9.4-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54c0d3d8e0078b7666984e11b12b88af2db11d11249a8ac8920dd5ef68a66e08"}, - {file = "psutil-5.9.4-cp36-abi3-win32.whl", hash = "sha256:149555f59a69b33f056ba1c4eb22bb7bf24332ce631c44a319cec09f876aaeff"}, - {file = "psutil-5.9.4-cp36-abi3-win_amd64.whl", hash = "sha256:fd8522436a6ada7b4aad6638662966de0d61d241cb821239b2ae7013d41a43d4"}, - {file = "psutil-5.9.4-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:6001c809253a29599bc0dfd5179d9f8a5779f9dffea1da0f13c53ee568115e1e"}, - {file = "psutil-5.9.4.tar.gz", hash = "sha256:3d7f9739eb435d4b1338944abe23f49584bde5395f27487d2ee25ad9a8774a62"}, + {file = "psutil-5.9.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:55ce319452e3d139e25d6c3f85a1acf12d1607ddedea5e35fb47a552c051161b"}, + {file = "psutil-5.9.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:7336292a13a80eb93c21f36bde4328aa748a04b68c13d01dfddd67fc13fd0618"}, + {file = "psutil-5.9.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:cb8d10461c1ceee0c25a64f2dd54872b70b89c26419e147a05a10b753ad36ec2"}, + {file = "psutil-5.9.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:7641300de73e4909e5d148e90cc3142fb890079e1525a840cf0dfd39195239fd"}, + {file = "psutil-5.9.0-cp27-none-win32.whl", hash = "sha256:ea42d747c5f71b5ccaa6897b216a7dadb9f52c72a0fe2b872ef7d3e1eacf3ba3"}, + {file = "psutil-5.9.0-cp27-none-win_amd64.whl", hash = "sha256:ef216cc9feb60634bda2f341a9559ac594e2eeaadd0ba187a4c2eb5b5d40b91c"}, + {file = "psutil-5.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:90a58b9fcae2dbfe4ba852b57bd4a1dded6b990a33d6428c7614b7d48eccb492"}, + {file = "psutil-5.9.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ff0d41f8b3e9ebb6b6110057e40019a432e96aae2008951121ba4e56040b84f3"}, + {file = "psutil-5.9.0-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:742c34fff804f34f62659279ed5c5b723bb0195e9d7bd9907591de9f8f6558e2"}, + {file = "psutil-5.9.0-cp310-cp310-win32.whl", hash = "sha256:8293942e4ce0c5689821f65ce6522ce4786d02af57f13c0195b40e1edb1db61d"}, + {file = "psutil-5.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:9b51917c1af3fa35a3f2dabd7ba96a2a4f19df3dec911da73875e1edaf22a40b"}, + {file = "psutil-5.9.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:e9805fed4f2a81de98ae5fe38b75a74c6e6ad2df8a5c479594c7629a1fe35f56"}, + {file = "psutil-5.9.0-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c51f1af02334e4b516ec221ee26b8fdf105032418ca5a5ab9737e8c87dafe203"}, + {file = "psutil-5.9.0-cp36-cp36m-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:32acf55cb9a8cbfb29167cd005951df81b567099295291bcfd1027365b36591d"}, + {file = "psutil-5.9.0-cp36-cp36m-win32.whl", hash = "sha256:e5c783d0b1ad6ca8a5d3e7b680468c9c926b804be83a3a8e95141b05c39c9f64"}, + {file = "psutil-5.9.0-cp36-cp36m-win_amd64.whl", hash = "sha256:d62a2796e08dd024b8179bd441cb714e0f81226c352c802fca0fd3f89eeacd94"}, + {file = "psutil-5.9.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3d00a664e31921009a84367266b35ba0aac04a2a6cad09c550a89041034d19a0"}, + {file = "psutil-5.9.0-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7779be4025c540d1d65a2de3f30caeacc49ae7a2152108adeaf42c7534a115ce"}, + {file = "psutil-5.9.0-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:072664401ae6e7c1bfb878c65d7282d4b4391f1bc9a56d5e03b5a490403271b5"}, + {file = "psutil-5.9.0-cp37-cp37m-win32.whl", hash = "sha256:df2c8bd48fb83a8408c8390b143c6a6fa10cb1a674ca664954de193fdcab36a9"}, + {file = "psutil-5.9.0-cp37-cp37m-win_amd64.whl", hash = "sha256:1d7b433519b9a38192dfda962dd8f44446668c009833e1429a52424624f408b4"}, + {file = "psutil-5.9.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c3400cae15bdb449d518545cbd5b649117de54e3596ded84aacabfbb3297ead2"}, + {file = "psutil-5.9.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b2237f35c4bbae932ee98902a08050a27821f8f6dfa880a47195e5993af4702d"}, + {file = "psutil-5.9.0-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1070a9b287846a21a5d572d6dddd369517510b68710fca56b0e9e02fd24bed9a"}, + {file = "psutil-5.9.0-cp38-cp38-win32.whl", hash = "sha256:76cebf84aac1d6da5b63df11fe0d377b46b7b500d892284068bacccf12f20666"}, + {file = "psutil-5.9.0-cp38-cp38-win_amd64.whl", hash = "sha256:3151a58f0fbd8942ba94f7c31c7e6b310d2989f4da74fcbf28b934374e9bf841"}, + {file = "psutil-5.9.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:539e429da49c5d27d5a58e3563886057f8fc3868a5547b4f1876d9c0f007bccf"}, + {file = "psutil-5.9.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:58c7d923dc209225600aec73aa2c4ae8ea33b1ab31bc11ef8a5933b027476f07"}, + {file = "psutil-5.9.0-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3611e87eea393f779a35b192b46a164b1d01167c9d323dda9b1e527ea69d697d"}, + {file = "psutil-5.9.0-cp39-cp39-win32.whl", hash = "sha256:4e2fb92e3aeae3ec3b7b66c528981fd327fb93fd906a77215200404444ec1845"}, + {file = "psutil-5.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:7d190ee2eaef7831163f254dc58f6d2e2a22e27382b936aab51c835fc080c3d3"}, + {file = "psutil-5.9.0.tar.gz", hash = "sha256:869842dbd66bb80c3217158e629d6fceaecc3a3166d3d1faee515b05dd26ca25"}, +] +py = [ + {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, + {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, ] py-cpuinfo = [ - {file = "py-cpuinfo-9.0.0.tar.gz", hash = "sha256:3cdbbf3fac90dc6f118bfd64384f309edeadd902d7c8fb17f02ffa1fc3f49690"}, - {file = "py_cpuinfo-9.0.0-py3-none-any.whl", hash = "sha256:859625bc251f64e21f077d099d4162689c762b5d6a4c3c97553d56241c9674d5"}, + {file = "py-cpuinfo-8.0.0.tar.gz", hash = "sha256:5f269be0e08e33fd959de96b34cd4aeeeacac014dd8305f70eb28d06de2345c5"}, ] -pyasn1 = [ - {file = "pyasn1-0.4.8-py2.py3-none-any.whl", hash = "sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d"}, - {file = "pyasn1-0.4.8.tar.gz", hash = "sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba"}, -] -pyasn1-modules = [ - {file = "pyasn1-modules-0.2.8.tar.gz", hash = "sha256:905f84c712230b2c592c19470d3ca8d552de726050d1d1716282a1f6146be65e"}, - {file = "pyasn1_modules-0.2.8-py2.py3-none-any.whl", hash = "sha256:a50b808ffeb97cb3601dd25981f6b016cbb3d31fbf57a8b8a87428e6158d0c74"}, -] -pycparser = [ - {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, - {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, +pycodestyle = [ + {file = "pycodestyle-2.8.0-py2.py3-none-any.whl", hash = "sha256:720f8b39dde8b293825e7ff02c475f3077124006db4f440dcbc9a20b76548a20"}, + {file = "pycodestyle-2.8.0.tar.gz", hash = "sha256:eddd5847ef438ea1c7870ca7eb78a9d47ce0cdb4851a5523949f2601d0cbbe7f"}, ] pydantic = [ - {file = "pydantic-1.10.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b5635de53e6686fe7a44b5cf25fcc419a0d5e5c1a1efe73d49d48fe7586db854"}, - {file = "pydantic-1.10.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6dc1cc241440ed7ca9ab59d9929075445da6b7c94ced281b3dd4cfe6c8cff817"}, - {file = "pydantic-1.10.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51bdeb10d2db0f288e71d49c9cefa609bca271720ecd0c58009bd7504a0c464c"}, - {file = "pydantic-1.10.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:78cec42b95dbb500a1f7120bdf95c401f6abb616bbe8785ef09887306792e66e"}, - {file = "pydantic-1.10.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:8775d4ef5e7299a2f4699501077a0defdaac5b6c4321173bcb0f3c496fbadf85"}, - {file = "pydantic-1.10.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:572066051eeac73d23f95ba9a71349c42a3e05999d0ee1572b7860235b850cc6"}, - {file = "pydantic-1.10.4-cp310-cp310-win_amd64.whl", hash = "sha256:7feb6a2d401f4d6863050f58325b8d99c1e56f4512d98b11ac64ad1751dc647d"}, - {file = "pydantic-1.10.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:39f4a73e5342b25c2959529f07f026ef58147249f9b7431e1ba8414a36761f53"}, - {file = "pydantic-1.10.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:983e720704431a6573d626b00662eb78a07148c9115129f9b4351091ec95ecc3"}, - {file = "pydantic-1.10.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75d52162fe6b2b55964fbb0af2ee58e99791a3138588c482572bb6087953113a"}, - {file = "pydantic-1.10.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fdf8d759ef326962b4678d89e275ffc55b7ce59d917d9f72233762061fd04a2d"}, - {file = "pydantic-1.10.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:05a81b006be15655b2a1bae5faa4280cf7c81d0e09fcb49b342ebf826abe5a72"}, - {file = "pydantic-1.10.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d88c4c0e5c5dfd05092a4b271282ef0588e5f4aaf345778056fc5259ba098857"}, - {file = "pydantic-1.10.4-cp311-cp311-win_amd64.whl", hash = "sha256:6a05a9db1ef5be0fe63e988f9617ca2551013f55000289c671f71ec16f4985e3"}, - {file = "pydantic-1.10.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:887ca463c3bc47103c123bc06919c86720e80e1214aab79e9b779cda0ff92a00"}, - {file = "pydantic-1.10.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdf88ab63c3ee282c76d652fc86518aacb737ff35796023fae56a65ced1a5978"}, - {file = "pydantic-1.10.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a48f1953c4a1d9bd0b5167ac50da9a79f6072c63c4cef4cf2a3736994903583e"}, - {file = "pydantic-1.10.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:a9f2de23bec87ff306aef658384b02aa7c32389766af3c5dee9ce33e80222dfa"}, - {file = "pydantic-1.10.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:cd8702c5142afda03dc2b1ee6bc358b62b3735b2cce53fc77b31ca9f728e4bc8"}, - {file = "pydantic-1.10.4-cp37-cp37m-win_amd64.whl", hash = "sha256:6e7124d6855b2780611d9f5e1e145e86667eaa3bd9459192c8dc1a097f5e9903"}, - {file = "pydantic-1.10.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0b53e1d41e97063d51a02821b80538053ee4608b9a181c1005441f1673c55423"}, - {file = "pydantic-1.10.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:55b1625899acd33229c4352ce0ae54038529b412bd51c4915349b49ca575258f"}, - {file = "pydantic-1.10.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:301d626a59edbe5dfb48fcae245896379a450d04baeed50ef40d8199f2733b06"}, - {file = "pydantic-1.10.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b6f9d649892a6f54a39ed56b8dfd5e08b5f3be5f893da430bed76975f3735d15"}, - {file = "pydantic-1.10.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:d7b5a3821225f5c43496c324b0d6875fde910a1c2933d726a743ce328fbb2a8c"}, - {file = "pydantic-1.10.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:f2f7eb6273dd12472d7f218e1fef6f7c7c2f00ac2e1ecde4db8824c457300416"}, - {file = "pydantic-1.10.4-cp38-cp38-win_amd64.whl", hash = "sha256:4b05697738e7d2040696b0a66d9f0a10bec0efa1883ca75ee9e55baf511909d6"}, - {file = "pydantic-1.10.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a9a6747cac06c2beb466064dda999a13176b23535e4c496c9d48e6406f92d42d"}, - {file = "pydantic-1.10.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:eb992a1ef739cc7b543576337bebfc62c0e6567434e522e97291b251a41dad7f"}, - {file = "pydantic-1.10.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:990406d226dea0e8f25f643b370224771878142155b879784ce89f633541a024"}, - {file = "pydantic-1.10.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2e82a6d37a95e0b1b42b82ab340ada3963aea1317fd7f888bb6b9dfbf4fff57c"}, - {file = "pydantic-1.10.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9193d4f4ee8feca58bc56c8306bcb820f5c7905fd919e0750acdeeeef0615b28"}, - {file = "pydantic-1.10.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:2b3ce5f16deb45c472dde1a0ee05619298c864a20cded09c4edd820e1454129f"}, - {file = "pydantic-1.10.4-cp39-cp39-win_amd64.whl", hash = "sha256:9cbdc268a62d9a98c56e2452d6c41c0263d64a2009aac69246486f01b4f594c4"}, - {file = "pydantic-1.10.4-py3-none-any.whl", hash = "sha256:4948f264678c703f3877d1c8877c4e3b2e12e549c57795107f08cf70c6ec7774"}, - {file = "pydantic-1.10.4.tar.gz", hash = "sha256:b9a3859f24eb4e097502a3be1fb4b2abb79b6103dd9e2e0edb70613a4459a648"}, -] -pydantic-factories = [ - {file = "pydantic_factories-1.17.1-py3-none-any.whl", hash = "sha256:05c0b143540f54d9dd9d0d500b7b146ff29e0c1cd4bb5f2ed99c60842ff1d5e6"}, - {file = "pydantic_factories-1.17.1.tar.gz", hash = "sha256:85848136cd768894dc5b6e3ffaf49753c7627c545ef05ff096ff616071cd59ff"}, -] -pydantic-openapi-schema = [ - {file = "pydantic_openapi_schema-1.5.1-py3-none-any.whl", hash = "sha256:fd5b1bff81ff70faa87fec62bd8193ccd671f31bc15b32a4557623d3db0e5eae"}, - {file = "pydantic_openapi_schema-1.5.1.tar.gz", hash = "sha256:d9b56235f4c4817c6e3693c4f8122c3e5e9c75f6b3b454524ad985012a264daf"}, + {file = "pydantic-1.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:cb23bcc093697cdea2708baae4f9ba0e972960a835af22560f6ae4e7e47d33f5"}, + {file = "pydantic-1.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1d5278bd9f0eee04a44c712982343103bba63507480bfd2fc2790fa70cd64cf4"}, + {file = "pydantic-1.9.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ab624700dc145aa809e6f3ec93fb8e7d0f99d9023b713f6a953637429b437d37"}, + {file = "pydantic-1.9.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c8d7da6f1c1049eefb718d43d99ad73100c958a5367d30b9321b092771e96c25"}, + {file = "pydantic-1.9.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:3c3b035103bd4e2e4a28da9da7ef2fa47b00ee4a9cf4f1a735214c1bcd05e0f6"}, + {file = "pydantic-1.9.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:3011b975c973819883842c5ab925a4e4298dffccf7782c55ec3580ed17dc464c"}, + {file = "pydantic-1.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:086254884d10d3ba16da0588604ffdc5aab3f7f09557b998373e885c690dd398"}, + {file = "pydantic-1.9.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:0fe476769acaa7fcddd17cadd172b156b53546ec3614a4d880e5d29ea5fbce65"}, + {file = "pydantic-1.9.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8e9dcf1ac499679aceedac7e7ca6d8641f0193c591a2d090282aaf8e9445a46"}, + {file = "pydantic-1.9.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d1e4c28f30e767fd07f2ddc6f74f41f034d1dd6bc526cd59e63a82fe8bb9ef4c"}, + {file = "pydantic-1.9.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:c86229333cabaaa8c51cf971496f10318c4734cf7b641f08af0a6fbf17ca3054"}, + {file = "pydantic-1.9.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:c0727bda6e38144d464daec31dff936a82917f431d9c39c39c60a26567eae3ed"}, + {file = "pydantic-1.9.0-cp36-cp36m-win_amd64.whl", hash = "sha256:dee5ef83a76ac31ab0c78c10bd7d5437bfdb6358c95b91f1ba7ff7b76f9996a1"}, + {file = "pydantic-1.9.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:d9c9bdb3af48e242838f9f6e6127de9be7063aad17b32215ccc36a09c5cf1070"}, + {file = "pydantic-1.9.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ee7e3209db1e468341ef41fe263eb655f67f5c5a76c924044314e139a1103a2"}, + {file = "pydantic-1.9.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0b6037175234850ffd094ca77bf60fb54b08b5b22bc85865331dd3bda7a02fa1"}, + {file = "pydantic-1.9.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b2571db88c636d862b35090ccf92bf24004393f85c8870a37f42d9f23d13e032"}, + {file = "pydantic-1.9.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8b5ac0f1c83d31b324e57a273da59197c83d1bb18171e512908fe5dc7278a1d6"}, + {file = "pydantic-1.9.0-cp37-cp37m-win_amd64.whl", hash = "sha256:bbbc94d0c94dd80b3340fc4f04fd4d701f4b038ebad72c39693c794fd3bc2d9d"}, + {file = "pydantic-1.9.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e0896200b6a40197405af18828da49f067c2fa1f821491bc8f5bde241ef3f7d7"}, + {file = "pydantic-1.9.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7bdfdadb5994b44bd5579cfa7c9b0e1b0e540c952d56f627eb227851cda9db77"}, + {file = "pydantic-1.9.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:574936363cd4b9eed8acdd6b80d0143162f2eb654d96cb3a8ee91d3e64bf4cf9"}, + {file = "pydantic-1.9.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c556695b699f648c58373b542534308922c46a1cda06ea47bc9ca45ef5b39ae6"}, + {file = "pydantic-1.9.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:f947352c3434e8b937e3aa8f96f47bdfe6d92779e44bb3f41e4c213ba6a32145"}, + {file = "pydantic-1.9.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5e48ef4a8b8c066c4a31409d91d7ca372a774d0212da2787c0d32f8045b1e034"}, + {file = "pydantic-1.9.0-cp38-cp38-win_amd64.whl", hash = "sha256:96f240bce182ca7fe045c76bcebfa0b0534a1bf402ed05914a6f1dadff91877f"}, + {file = "pydantic-1.9.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:815ddebb2792efd4bba5488bc8fde09c29e8ca3227d27cf1c6990fc830fd292b"}, + {file = "pydantic-1.9.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6c5b77947b9e85a54848343928b597b4f74fc364b70926b3c4441ff52620640c"}, + {file = "pydantic-1.9.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c68c3bc88dbda2a6805e9a142ce84782d3930f8fdd9655430d8576315ad97ce"}, + {file = "pydantic-1.9.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5a79330f8571faf71bf93667d3ee054609816f10a259a109a0738dac983b23c3"}, + {file = "pydantic-1.9.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f5a64b64ddf4c99fe201ac2724daada8595ada0d102ab96d019c1555c2d6441d"}, + {file = "pydantic-1.9.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a733965f1a2b4090a5238d40d983dcd78f3ecea221c7af1497b845a9709c1721"}, + {file = "pydantic-1.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:2cc6a4cb8a118ffec2ca5fcb47afbacb4f16d0ab8b7350ddea5e8ef7bcc53a16"}, + {file = "pydantic-1.9.0-py3-none-any.whl", hash = "sha256:085ca1de245782e9b46cefcf99deecc67d418737a1fd3f6a4f511344b613a5b3"}, + {file = "pydantic-1.9.0.tar.gz", hash = "sha256:742645059757a56ecd886faf4ed2441b9c0cd406079c2b4bee51bcc3fbcd510a"}, +] +pyflakes = [ + {file = "pyflakes-2.4.0-py2.py3-none-any.whl", hash = "sha256:3bb3a3f256f4b7968c9c788781e4ff07dce46bdf12339dcda61053375426ee2e"}, + {file = "pyflakes-2.4.0.tar.gz", hash = "sha256:05a85c2872edf37a4ed30b0cce2f6093e1d0581f8c19d7393122da7e25b2b24c"}, ] pygments = [ - {file = "Pygments-2.14.0-py3-none-any.whl", hash = "sha256:fa7bd7bd2771287c0de303af8bfdfc731f51bd2c6a47ab69d117138893b82717"}, - {file = "Pygments-2.14.0.tar.gz", hash = "sha256:b3ed06a9e8ac9a9aae5a6f5dbe78a8a58655d17b43b93c078f094ddc476ae297"}, -] -pyopenssl = [ - {file = "pyOpenSSL-23.0.0-py3-none-any.whl", hash = "sha256:df5fc28af899e74e19fccb5510df423581047e10ab6f1f4ba1763ff5fde844c0"}, - {file = "pyOpenSSL-23.0.0.tar.gz", hash = "sha256:c1cc5f86bcacefc84dada7d31175cae1b1518d5f60d3d0bb595a67822a868a6f"}, -] -pyrsistent = [ - {file = "pyrsistent-0.19.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:20460ac0ea439a3e79caa1dbd560344b64ed75e85d8703943e0b66c2a6150e4a"}, - {file = "pyrsistent-0.19.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4c18264cb84b5e68e7085a43723f9e4c1fd1d935ab240ce02c0324a8e01ccb64"}, - {file = "pyrsistent-0.19.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4b774f9288dda8d425adb6544e5903f1fb6c273ab3128a355c6b972b7df39dcf"}, - {file = "pyrsistent-0.19.3-cp310-cp310-win32.whl", hash = "sha256:5a474fb80f5e0d6c9394d8db0fc19e90fa540b82ee52dba7d246a7791712f74a"}, - {file = "pyrsistent-0.19.3-cp310-cp310-win_amd64.whl", hash = "sha256:49c32f216c17148695ca0e02a5c521e28a4ee6c5089f97e34fe24163113722da"}, - {file = "pyrsistent-0.19.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f0774bf48631f3a20471dd7c5989657b639fd2d285b861237ea9e82c36a415a9"}, - {file = "pyrsistent-0.19.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ab2204234c0ecd8b9368dbd6a53e83c3d4f3cab10ecaf6d0e772f456c442393"}, - {file = "pyrsistent-0.19.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e42296a09e83028b3476f7073fcb69ffebac0e66dbbfd1bd847d61f74db30f19"}, - {file = "pyrsistent-0.19.3-cp311-cp311-win32.whl", hash = "sha256:64220c429e42a7150f4bfd280f6f4bb2850f95956bde93c6fda1b70507af6ef3"}, - {file = "pyrsistent-0.19.3-cp311-cp311-win_amd64.whl", hash = "sha256:016ad1afadf318eb7911baa24b049909f7f3bb2c5b1ed7b6a8f21db21ea3faa8"}, - {file = "pyrsistent-0.19.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c4db1bd596fefd66b296a3d5d943c94f4fac5bcd13e99bffe2ba6a759d959a28"}, - {file = "pyrsistent-0.19.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aeda827381f5e5d65cced3024126529ddc4289d944f75e090572c77ceb19adbf"}, - {file = "pyrsistent-0.19.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:42ac0b2f44607eb92ae88609eda931a4f0dfa03038c44c772e07f43e738bcac9"}, - {file = "pyrsistent-0.19.3-cp37-cp37m-win32.whl", hash = "sha256:e8f2b814a3dc6225964fa03d8582c6e0b6650d68a232df41e3cc1b66a5d2f8d1"}, - {file = "pyrsistent-0.19.3-cp37-cp37m-win_amd64.whl", hash = "sha256:c9bb60a40a0ab9aba40a59f68214eed5a29c6274c83b2cc206a359c4a89fa41b"}, - {file = "pyrsistent-0.19.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:a2471f3f8693101975b1ff85ffd19bb7ca7dd7c38f8a81701f67d6b4f97b87d8"}, - {file = "pyrsistent-0.19.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc5d149f31706762c1f8bda2e8c4f8fead6e80312e3692619a75301d3dbb819a"}, - {file = "pyrsistent-0.19.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3311cb4237a341aa52ab8448c27e3a9931e2ee09561ad150ba94e4cfd3fc888c"}, - {file = "pyrsistent-0.19.3-cp38-cp38-win32.whl", hash = "sha256:f0e7c4b2f77593871e918be000b96c8107da48444d57005b6a6bc61fb4331b2c"}, - {file = "pyrsistent-0.19.3-cp38-cp38-win_amd64.whl", hash = "sha256:c147257a92374fde8498491f53ffa8f4822cd70c0d85037e09028e478cababb7"}, - {file = "pyrsistent-0.19.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b735e538f74ec31378f5a1e3886a26d2ca6351106b4dfde376a26fc32a044edc"}, - {file = "pyrsistent-0.19.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99abb85579e2165bd8522f0c0138864da97847875ecbd45f3e7e2af569bfc6f2"}, - {file = "pyrsistent-0.19.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3a8cb235fa6d3fd7aae6a4f1429bbb1fec1577d978098da1252f0489937786f3"}, - {file = "pyrsistent-0.19.3-cp39-cp39-win32.whl", hash = "sha256:c74bed51f9b41c48366a286395c67f4e894374306b197e62810e0fdaf2364da2"}, - {file = "pyrsistent-0.19.3-cp39-cp39-win_amd64.whl", hash = "sha256:878433581fc23e906d947a6814336eee031a00e6defba224234169ae3d3d6a98"}, - {file = "pyrsistent-0.19.3-py3-none-any.whl", hash = "sha256:ccf0d6bd208f8111179f0c26fdf84ed7c3891982f2edaeae7422575f47e66b64"}, - {file = "pyrsistent-0.19.3.tar.gz", hash = "sha256:1a2994773706bbb4995c31a97bc94f1418314923bd1048c6d964837040376440"}, + {file = "Pygments-2.11.2-py3-none-any.whl", hash = "sha256:44238f1b60a76d78fc8ca0528ee429702aae011c265fe6a8dd8b63049ae41c65"}, + {file = "Pygments-2.11.2.tar.gz", hash = "sha256:4e426f72023d88d03b2fa258de560726ce890ff3b630f88c21cbb8b2503b8c6a"}, +] +pyparsing = [ + {file = "pyparsing-3.0.7-py3-none-any.whl", hash = "sha256:a6c06a88f252e6c322f65faf8f418b16213b51bdfaece0524c1c1bc30c63c484"}, + {file = "pyparsing-3.0.7.tar.gz", hash = "sha256:18ee9022775d270c55187733956460083db60b37d0d0fb357445f3094eed3eea"}, ] pytest = [ - {file = "pytest-7.2.1-py3-none-any.whl", hash = "sha256:c7c6ca206e93355074ae32f7403e8ea12163b1163c976fee7d4d84027c162be5"}, - {file = "pytest-7.2.1.tar.gz", hash = "sha256:d45e0952f3727241918b8fd0f376f5ff6b301cc0777c6f9a556935c92d8a7d42"}, + {file = "pytest-7.0.1-py3-none-any.whl", hash = "sha256:9ce3ff477af913ecf6321fe337b93a2c0dcf2a0a1439c43f5452112c1e4280db"}, + {file = "pytest-7.0.1.tar.gz", hash = "sha256:e30905a0c131d3d94b89624a1cc5afec3e0ba2fbdb151867d8e0ebd49850f171"}, ] pytest-aiohttp = [ {file = "pytest-aiohttp-1.0.4.tar.gz", hash = "sha256:39ff3a0d15484c01d1436cbedad575c6eafbf0f57cdf76fb94994c97b5b8c5a4"}, {file = "pytest_aiohttp-1.0.4-py3-none-any.whl", hash = "sha256:1d2dc3a304c2be1fd496c0c2fb6b31ab60cd9fc33984f761f951f8ea1eb4ca95"}, ] pytest-asyncio = [ - {file = "pytest-asyncio-0.20.3.tar.gz", hash = "sha256:83cbf01169ce3e8eb71c6c278ccb0574d1a7a3bb8eaaf5e50e0ad342afb33b36"}, - {file = "pytest_asyncio-0.20.3-py3-none-any.whl", hash = "sha256:f129998b209d04fcc65c96fc85c11e5316738358909a8399e93be553d7656442"}, + {file = "pytest-asyncio-0.18.1.tar.gz", hash = "sha256:c43fcdfea2335dd82ffe0f2774e40285ddfea78a8e81e56118d47b6a90fbb09e"}, + {file = "pytest_asyncio-0.18.1-py3-none-any.whl", hash = "sha256:c9ec48e8bbf5cc62755e18c4d8bc6907843ec9c5f4ac8f61464093baeba24a7e"}, ] pytest-benchmark = [ - {file = "pytest-benchmark-4.0.0.tar.gz", hash = "sha256:fb0785b83efe599a6a956361c0691ae1dbb5318018561af10f3e915caa0048d1"}, - {file = "pytest_benchmark-4.0.0-py3-none-any.whl", hash = "sha256:fdb7db64e31c8b277dff9850d2a2556d8b60bcb0ea6524e36e28ffd7c87f71d6"}, + {file = "pytest-benchmark-3.4.1.tar.gz", hash = "sha256:40e263f912de5a81d891619032983557d62a3d85843f9a9f30b98baea0cd7b47"}, + {file = "pytest_benchmark-3.4.1-py2.py3-none-any.whl", hash = "sha256:36d2b08c4882f6f997fd3126a3d6dfd70f3249cde178ed8bbc0b73db7c20f809"}, ] pytest-cov = [ - {file = "pytest-cov-4.0.0.tar.gz", hash = "sha256:996b79efde6433cdbd0088872dbc5fb3ed7fe1578b68cdbba634f14bb8dd0470"}, - {file = "pytest_cov-4.0.0-py3-none-any.whl", hash = "sha256:2feb1b751d66a8bd934e5edfa2e961d11309dc37b73b0eabe73b5945fee20f6b"}, + {file = "pytest-cov-3.0.0.tar.gz", hash = "sha256:e7f0f5b1617d2210a2cabc266dfe2f4c75a8d32fb89eafb7ad9d06f6d076d470"}, + {file = "pytest_cov-3.0.0-py3-none-any.whl", hash = "sha256:578d5d15ac4a25e5f961c938b85a05b09fdaae9deef3bb6de9a6e766622ca7a6"}, ] pytest-django = [ {file = "pytest-django-4.5.2.tar.gz", hash = "sha256:d9076f759bb7c36939dbdd5ae6633c18edfc2902d1a69fdbefd2426b970ce6c2"}, @@ -3088,20 +2280,16 @@ pytest-flask = [ {file = "pytest_flask-1.2.0-py3-none-any.whl", hash = "sha256:fe25b39ad0db09c3d1fe728edecf97ced85e774c775db259a6d25f0270a4e7c9"}, ] pytest-mock = [ - {file = "pytest-mock-3.10.0.tar.gz", hash = "sha256:fbbdb085ef7c252a326fd8cdcac0aa3b1333d8811f131bdcc701002e1be7ed4f"}, - {file = "pytest_mock-3.10.0-py3-none-any.whl", hash = "sha256:f4c973eeae0282963eb293eb173ce91b091a79c1334455acfac9ddee8a1c784b"}, + {file = "pytest-mock-3.7.0.tar.gz", hash = "sha256:5112bd92cc9f186ee96e1a92efc84969ea494939c3aead39c50f421c4cc69534"}, + {file = "pytest_mock-3.7.0-py3-none-any.whl", hash = "sha256:6cff27cec936bf81dc5ee87f07132b807bcda51106b5ec4b90a04331cba76231"}, ] pytest-mypy-plugins = [ - {file = "pytest-mypy-plugins-1.10.1.tar.gz", hash = "sha256:1f258ccd784341dee93d4baaedafe715ead2a1f730c59282c45abd025612cf16"}, - {file = "pytest_mypy_plugins-1.10.1-py3-none-any.whl", hash = "sha256:d32e26927bea6646cd1c79f7115cd6e9dcf583fa07d2ab6d4cbeb70bccb42942"}, -] -pytest-snapshot = [ - {file = "pytest-snapshot-0.9.0.tar.gz", hash = "sha256:c7013c3abc3e860f9feff899f8b4debe3708650d8d8242a61bf2625ff64db7f3"}, - {file = "pytest_snapshot-0.9.0-py3-none-any.whl", hash = "sha256:4b9fe1c21c868fe53a545e4e3184d36bc1c88946e3f5c1d9dd676962a9b3d4ab"}, + {file = "pytest-mypy-plugins-1.9.3.tar.gz", hash = "sha256:c2c6590ee68fd634013fdd0b47a5c83df300afd8b38d5aabd3b29e21b7907221"}, + {file = "pytest_mypy_plugins-1.9.3-py3-none-any.whl", hash = "sha256:10e60bd59a2b043ebda70f9a0ad1a8c9324ce6d0b46b93cae3b1172df2d4d81b"}, ] -pytest-xdist = [ - {file = "pytest-xdist-3.1.0.tar.gz", hash = "sha256:40fdb8f3544921c5dfcd486ac080ce22870e71d82ced6d2e78fa97c2addd480c"}, - {file = "pytest_xdist-3.1.0-py3-none-any.whl", hash = "sha256:70a76f191d8a1d2d6be69fc440cdf85f3e4c03c08b520fd5dc5d338d6cf07d89"}, +pytest-xprocess = [ + {file = "pytest-xprocess-0.18.1.tar.gz", hash = "sha256:fd9f30ed1584b5833bc34494748adf0fb9de3ca7bacc4e88ad71989c21cba266"}, + {file = "pytest_xprocess-0.18.1-py3-none-any.whl", hash = "sha256:6f2aba817d842518d9d9dfb7e9adfe2a6e354a4359f4166bef0822ef4be1c9db"}, ] python-dateutil = [ {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, @@ -3110,14 +2298,16 @@ python-dateutil = [ python-editor = [ {file = "python-editor-1.0.4.tar.gz", hash = "sha256:51fda6bcc5ddbbb7063b2af7509e43bd84bfc32a4ff71349ec7847713882327b"}, {file = "python_editor-1.0.4-py2-none-any.whl", hash = "sha256:5f98b069316ea1c2ed3f67e7f5df6c0d8f10b689964a4a811ff64f0106819ec8"}, + {file = "python_editor-1.0.4-py2.7.egg", hash = "sha256:ea87e17f6ec459e780e4221f295411462e0d0810858e055fc514684350a2f522"}, {file = "python_editor-1.0.4-py3-none-any.whl", hash = "sha256:1bf6e860a8ad52a14c3ee1252d5dc25b2030618ed80c022598f00176adc8367d"}, + {file = "python_editor-1.0.4-py3.5.egg", hash = "sha256:c3da2053dbab6b29c94e43c486ff67206eafbe7eb52dbec7390b5e2fb05aac77"}, ] python-multipart = [ {file = "python-multipart-0.0.5.tar.gz", hash = "sha256:f7bb5f611fc600d15fa47b3974c8aa16e93724513b49b5f95c81e6624c83fa43"}, ] pytz = [ - {file = "pytz-2022.7-py2.py3-none-any.whl", hash = "sha256:93007def75ae22f7cd991c84e02d434876818661f8df9ad5df9e950ff4e52cfd"}, - {file = "pytz-2022.7.tar.gz", hash = "sha256:7ccfae7b4b2c067464a6733c6261673fdb8fd1be905460396b97a073e9fa683a"}, + {file = "pytz-2021.3-py2.py3-none-any.whl", hash = "sha256:3672058bc3453457b622aab7a1c3bfd5ab0bdae451512f6cf25f64ed37f5b87c"}, + {file = "pytz-2021.3.tar.gz", hash = "sha256:acad2d8b20a1af07d4e4c9d2e9285c5ed9104354062f275f3fcd88dcef4f1326"}, ] pyyaml = [ {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"}, @@ -3127,13 +2317,6 @@ pyyaml = [ {file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"}, {file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"}, {file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"}, - {file = "PyYAML-6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d4b0ba9512519522b118090257be113b9468d804b19d63c71dbcf4a48fa32358"}, - {file = "PyYAML-6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:81957921f441d50af23654aa6c5e5eaf9b06aba7f0a19c18a538dc7ef291c5a1"}, - {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afa17f5bc4d1b10afd4466fd3a44dc0e245382deca5b3c353d8b757f9e3ecb8d"}, - {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dbad0e9d368bb989f4515da330b88a057617d16b6a8245084f1b05400f24609f"}, - {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:432557aa2c09802be39460360ddffd48156e30721f5e8d917f01d31694216782"}, - {file = "PyYAML-6.0-cp311-cp311-win32.whl", hash = "sha256:bfaef573a63ba8923503d27530362590ff4f576c626d86a9fed95822a8255fd7"}, - {file = "PyYAML-6.0-cp311-cp311-win_amd64.whl", hash = "sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf"}, {file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"}, {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"}, {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"}, @@ -3162,630 +2345,484 @@ pyyaml = [ {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"}, ] readchar = [ - {file = "readchar-4.0.3-py3-none-any.whl", hash = "sha256:3d4351c563aba4ae7d4e0b821a234d48bc748f45b74e3f38b50cba3857b57acb"}, - {file = "readchar-4.0.3.tar.gz", hash = "sha256:1d920d0e9ab76ec5d42192a68d15af2562663b5dfbf4a67cf9eba520e1ca57e6"}, + {file = "readchar-3.0.5-py3-none-any.whl", hash = "sha256:4c31210edfdf3e706d042c58feba2e8b6d5768b7f37002c5579fc473552f8fbb"}, + {file = "readchar-3.0.5.tar.gz", hash = "sha256:d1f5b71e98c37b7f3b695fba9db978ab84f4f8a0ed879653d83e1d90a4c482c0"}, ] regex = [ - {file = "regex-2022.10.31-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a8ff454ef0bb061e37df03557afda9d785c905dab15584860f982e88be73015f"}, - {file = "regex-2022.10.31-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1eba476b1b242620c266edf6325b443a2e22b633217a9835a52d8da2b5c051f9"}, - {file = "regex-2022.10.31-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0e5af9a9effb88535a472e19169e09ce750c3d442fb222254a276d77808620b"}, - {file = "regex-2022.10.31-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d03fe67b2325cb3f09be029fd5da8df9e6974f0cde2c2ac6a79d2634e791dd57"}, - {file = "regex-2022.10.31-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a9d0b68ac1743964755ae2d89772c7e6fb0118acd4d0b7464eaf3921c6b49dd4"}, - {file = "regex-2022.10.31-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a45b6514861916c429e6059a55cf7db74670eaed2052a648e3e4d04f070e001"}, - {file = "regex-2022.10.31-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8b0886885f7323beea6f552c28bff62cbe0983b9fbb94126531693ea6c5ebb90"}, - {file = "regex-2022.10.31-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:5aefb84a301327ad115e9d346c8e2760009131d9d4b4c6b213648d02e2abe144"}, - {file = "regex-2022.10.31-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:702d8fc6f25bbf412ee706bd73019da5e44a8400861dfff7ff31eb5b4a1276dc"}, - {file = "regex-2022.10.31-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a3c1ebd4ed8e76e886507c9eddb1a891673686c813adf889b864a17fafcf6d66"}, - {file = "regex-2022.10.31-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:50921c140561d3db2ab9f5b11c5184846cde686bb5a9dc64cae442926e86f3af"}, - {file = "regex-2022.10.31-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:7db345956ecce0c99b97b042b4ca7326feeec6b75facd8390af73b18e2650ffc"}, - {file = "regex-2022.10.31-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:763b64853b0a8f4f9cfb41a76a4a85a9bcda7fdda5cb057016e7706fde928e66"}, - {file = "regex-2022.10.31-cp310-cp310-win32.whl", hash = "sha256:44136355e2f5e06bf6b23d337a75386371ba742ffa771440b85bed367c1318d1"}, - {file = "regex-2022.10.31-cp310-cp310-win_amd64.whl", hash = "sha256:bfff48c7bd23c6e2aec6454aaf6edc44444b229e94743b34bdcdda2e35126cf5"}, - {file = "regex-2022.10.31-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4b4b1fe58cd102d75ef0552cf17242705ce0759f9695334a56644ad2d83903fe"}, - {file = "regex-2022.10.31-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:542e3e306d1669b25936b64917285cdffcd4f5c6f0247636fec037187bd93542"}, - {file = "regex-2022.10.31-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c27cc1e4b197092e50ddbf0118c788d9977f3f8f35bfbbd3e76c1846a3443df7"}, - {file = "regex-2022.10.31-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8e38472739028e5f2c3a4aded0ab7eadc447f0d84f310c7a8bb697ec417229e"}, - {file = "regex-2022.10.31-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:76c598ca73ec73a2f568e2a72ba46c3b6c8690ad9a07092b18e48ceb936e9f0c"}, - {file = "regex-2022.10.31-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c28d3309ebd6d6b2cf82969b5179bed5fefe6142c70f354ece94324fa11bf6a1"}, - {file = "regex-2022.10.31-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9af69f6746120998cd9c355e9c3c6aec7dff70d47247188feb4f829502be8ab4"}, - {file = "regex-2022.10.31-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a5f9505efd574d1e5b4a76ac9dd92a12acb2b309551e9aa874c13c11caefbe4f"}, - {file = "regex-2022.10.31-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:5ff525698de226c0ca743bfa71fc6b378cda2ddcf0d22d7c37b1cc925c9650a5"}, - {file = "regex-2022.10.31-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:4fe7fda2fe7c8890d454f2cbc91d6c01baf206fbc96d89a80241a02985118c0c"}, - {file = "regex-2022.10.31-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:2cdc55ca07b4e70dda898d2ab7150ecf17c990076d3acd7a5f3b25cb23a69f1c"}, - {file = "regex-2022.10.31-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:44a6c2f6374e0033873e9ed577a54a3602b4f609867794c1a3ebba65e4c93ee7"}, - {file = "regex-2022.10.31-cp311-cp311-win32.whl", hash = "sha256:d8716f82502997b3d0895d1c64c3b834181b1eaca28f3f6336a71777e437c2af"}, - {file = "regex-2022.10.31-cp311-cp311-win_amd64.whl", hash = "sha256:61edbca89aa3f5ef7ecac8c23d975fe7261c12665f1d90a6b1af527bba86ce61"}, - {file = "regex-2022.10.31-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:0a069c8483466806ab94ea9068c34b200b8bfc66b6762f45a831c4baaa9e8cdd"}, - {file = "regex-2022.10.31-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d26166acf62f731f50bdd885b04b38828436d74e8e362bfcb8df221d868b5d9b"}, - {file = "regex-2022.10.31-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ac741bf78b9bb432e2d314439275235f41656e189856b11fb4e774d9f7246d81"}, - {file = "regex-2022.10.31-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75f591b2055523fc02a4bbe598aa867df9e953255f0b7f7715d2a36a9c30065c"}, - {file = "regex-2022.10.31-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b30bddd61d2a3261f025ad0f9ee2586988c6a00c780a2fb0a92cea2aa702c54"}, - {file = "regex-2022.10.31-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef4163770525257876f10e8ece1cf25b71468316f61451ded1a6f44273eedeb5"}, - {file = "regex-2022.10.31-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:7b280948d00bd3973c1998f92e22aa3ecb76682e3a4255f33e1020bd32adf443"}, - {file = "regex-2022.10.31-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:d0213671691e341f6849bf33cd9fad21f7b1cb88b89e024f33370733fec58742"}, - {file = "regex-2022.10.31-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:22e7ebc231d28393dfdc19b185d97e14a0f178bedd78e85aad660e93b646604e"}, - {file = "regex-2022.10.31-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:8ad241da7fac963d7573cc67a064c57c58766b62a9a20c452ca1f21050868dfa"}, - {file = "regex-2022.10.31-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:586b36ebda81e6c1a9c5a5d0bfdc236399ba6595e1397842fd4a45648c30f35e"}, - {file = "regex-2022.10.31-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:0653d012b3bf45f194e5e6a41df9258811ac8fc395579fa82958a8b76286bea4"}, - {file = "regex-2022.10.31-cp36-cp36m-win32.whl", hash = "sha256:144486e029793a733e43b2e37df16a16df4ceb62102636ff3db6033994711066"}, - {file = "regex-2022.10.31-cp36-cp36m-win_amd64.whl", hash = "sha256:c14b63c9d7bab795d17392c7c1f9aaabbffd4cf4387725a0ac69109fb3b550c6"}, - {file = "regex-2022.10.31-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4cac3405d8dda8bc6ed499557625585544dd5cbf32072dcc72b5a176cb1271c8"}, - {file = "regex-2022.10.31-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23cbb932cc53a86ebde0fb72e7e645f9a5eec1a5af7aa9ce333e46286caef783"}, - {file = "regex-2022.10.31-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:74bcab50a13960f2a610cdcd066e25f1fd59e23b69637c92ad470784a51b1347"}, - {file = "regex-2022.10.31-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:78d680ef3e4d405f36f0d6d1ea54e740366f061645930072d39bca16a10d8c93"}, - {file = "regex-2022.10.31-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce6910b56b700bea7be82c54ddf2e0ed792a577dfaa4a76b9af07d550af435c6"}, - {file = "regex-2022.10.31-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:659175b2144d199560d99a8d13b2228b85e6019b6e09e556209dfb8c37b78a11"}, - {file = "regex-2022.10.31-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1ddf14031a3882f684b8642cb74eea3af93a2be68893901b2b387c5fd92a03ec"}, - {file = "regex-2022.10.31-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b683e5fd7f74fb66e89a1ed16076dbab3f8e9f34c18b1979ded614fe10cdc4d9"}, - {file = "regex-2022.10.31-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:2bde29cc44fa81c0a0c8686992c3080b37c488df167a371500b2a43ce9f026d1"}, - {file = "regex-2022.10.31-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:4919899577ba37f505aaebdf6e7dc812d55e8f097331312db7f1aab18767cce8"}, - {file = "regex-2022.10.31-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:9c94f7cc91ab16b36ba5ce476f1904c91d6c92441f01cd61a8e2729442d6fcf5"}, - {file = "regex-2022.10.31-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ae1e96785696b543394a4e3f15f3f225d44f3c55dafe3f206493031419fedf95"}, - {file = "regex-2022.10.31-cp37-cp37m-win32.whl", hash = "sha256:c670f4773f2f6f1957ff8a3962c7dd12e4be54d05839b216cb7fd70b5a1df394"}, - {file = "regex-2022.10.31-cp37-cp37m-win_amd64.whl", hash = "sha256:8e0caeff18b96ea90fc0eb6e3bdb2b10ab5b01a95128dfeccb64a7238decf5f0"}, - {file = "regex-2022.10.31-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:131d4be09bea7ce2577f9623e415cab287a3c8e0624f778c1d955ec7c281bd4d"}, - {file = "regex-2022.10.31-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e613a98ead2005c4ce037c7b061f2409a1a4e45099edb0ef3200ee26ed2a69a8"}, - {file = "regex-2022.10.31-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:052b670fafbe30966bbe5d025e90b2a491f85dfe5b2583a163b5e60a85a321ad"}, - {file = "regex-2022.10.31-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aa62a07ac93b7cb6b7d0389d8ef57ffc321d78f60c037b19dfa78d6b17c928ee"}, - {file = "regex-2022.10.31-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5352bea8a8f84b89d45ccc503f390a6be77917932b1c98c4cdc3565137acc714"}, - {file = "regex-2022.10.31-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:20f61c9944f0be2dc2b75689ba409938c14876c19d02f7585af4460b6a21403e"}, - {file = "regex-2022.10.31-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:29c04741b9ae13d1e94cf93fca257730b97ce6ea64cfe1eba11cf9ac4e85afb6"}, - {file = "regex-2022.10.31-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:543883e3496c8b6d58bd036c99486c3c8387c2fc01f7a342b760c1ea3158a318"}, - {file = "regex-2022.10.31-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b7a8b43ee64ca8f4befa2bea4083f7c52c92864d8518244bfa6e88c751fa8fff"}, - {file = "regex-2022.10.31-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:6a9a19bea8495bb419dc5d38c4519567781cd8d571c72efc6aa959473d10221a"}, - {file = "regex-2022.10.31-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:6ffd55b5aedc6f25fd8d9f905c9376ca44fcf768673ffb9d160dd6f409bfda73"}, - {file = "regex-2022.10.31-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:4bdd56ee719a8f751cf5a593476a441c4e56c9b64dc1f0f30902858c4ef8771d"}, - {file = "regex-2022.10.31-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8ca88da1bd78990b536c4a7765f719803eb4f8f9971cc22d6ca965c10a7f2c4c"}, - {file = "regex-2022.10.31-cp38-cp38-win32.whl", hash = "sha256:5a260758454580f11dd8743fa98319bb046037dfab4f7828008909d0aa5292bc"}, - {file = "regex-2022.10.31-cp38-cp38-win_amd64.whl", hash = "sha256:5e6a5567078b3eaed93558842346c9d678e116ab0135e22eb72db8325e90b453"}, - {file = "regex-2022.10.31-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5217c25229b6a85049416a5c1e6451e9060a1edcf988641e309dbe3ab26d3e49"}, - {file = "regex-2022.10.31-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4bf41b8b0a80708f7e0384519795e80dcb44d7199a35d52c15cc674d10b3081b"}, - {file = "regex-2022.10.31-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cf0da36a212978be2c2e2e2d04bdff46f850108fccc1851332bcae51c8907cc"}, - {file = "regex-2022.10.31-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d403d781b0e06d2922435ce3b8d2376579f0c217ae491e273bab8d092727d244"}, - {file = "regex-2022.10.31-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a37d51fa9a00d265cf73f3de3930fa9c41548177ba4f0faf76e61d512c774690"}, - {file = "regex-2022.10.31-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4f781ffedd17b0b834c8731b75cce2639d5a8afe961c1e58ee7f1f20b3af185"}, - {file = "regex-2022.10.31-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d243b36fbf3d73c25e48014961e83c19c9cc92530516ce3c43050ea6276a2ab7"}, - {file = "regex-2022.10.31-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:370f6e97d02bf2dd20d7468ce4f38e173a124e769762d00beadec3bc2f4b3bc4"}, - {file = "regex-2022.10.31-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:597f899f4ed42a38df7b0e46714880fb4e19a25c2f66e5c908805466721760f5"}, - {file = "regex-2022.10.31-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7dbdce0c534bbf52274b94768b3498abdf675a691fec5f751b6057b3030f34c1"}, - {file = "regex-2022.10.31-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:22960019a842777a9fa5134c2364efaed5fbf9610ddc5c904bd3a400973b0eb8"}, - {file = "regex-2022.10.31-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:7f5a3ffc731494f1a57bd91c47dc483a1e10048131ffb52d901bfe2beb6102e8"}, - {file = "regex-2022.10.31-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7ef6b5942e6bfc5706301a18a62300c60db9af7f6368042227ccb7eeb22d0892"}, - {file = "regex-2022.10.31-cp39-cp39-win32.whl", hash = "sha256:395161bbdbd04a8333b9ff9763a05e9ceb4fe210e3c7690f5e68cedd3d65d8e1"}, - {file = "regex-2022.10.31-cp39-cp39-win_amd64.whl", hash = "sha256:957403a978e10fb3ca42572a23e6f7badff39aa1ce2f4ade68ee452dc6807692"}, - {file = "regex-2022.10.31.tar.gz", hash = "sha256:a3a98921da9a1bf8457aeee6a551948a83601689e5ecdd736894ea9bbec77e83"}, + {file = "regex-2022.1.18-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:34316bf693b1d2d29c087ee7e4bb10cdfa39da5f9c50fa15b07489b4ab93a1b5"}, + {file = "regex-2022.1.18-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7a0b9f6a1a15d494b35f25ed07abda03209fa76c33564c09c9e81d34f4b919d7"}, + {file = "regex-2022.1.18-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f99112aed4fb7cee00c7f77e8b964a9b10f69488cdff626ffd797d02e2e4484f"}, + {file = "regex-2022.1.18-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a2bf98ac92f58777c0fafc772bf0493e67fcf677302e0c0a630ee517a43b949"}, + {file = "regex-2022.1.18-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8618d9213a863c468a865e9d2ec50221015f7abf52221bc927152ef26c484b4c"}, + {file = "regex-2022.1.18-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b52cc45e71657bc4743a5606d9023459de929b2a198d545868e11898ba1c3f59"}, + {file = "regex-2022.1.18-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e12949e5071c20ec49ef00c75121ed2b076972132fc1913ddf5f76cae8d10b4"}, + {file = "regex-2022.1.18-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:b02e3e72665cd02afafb933453b0c9f6c59ff6e3708bd28d0d8580450e7e88af"}, + {file = "regex-2022.1.18-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:abfcb0ef78df0ee9df4ea81f03beea41849340ce33a4c4bd4dbb99e23ec781b6"}, + {file = "regex-2022.1.18-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6213713ac743b190ecbf3f316d6e41d099e774812d470422b3a0f137ea635832"}, + {file = "regex-2022.1.18-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:61ebbcd208d78658b09e19c78920f1ad38936a0aa0f9c459c46c197d11c580a0"}, + {file = "regex-2022.1.18-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:b013f759cd69cb0a62de954d6d2096d648bc210034b79b1881406b07ed0a83f9"}, + {file = "regex-2022.1.18-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:9187500d83fd0cef4669385cbb0961e227a41c0c9bc39219044e35810793edf7"}, + {file = "regex-2022.1.18-cp310-cp310-win32.whl", hash = "sha256:94c623c331a48a5ccc7d25271399aff29729fa202c737ae3b4b28b89d2b0976d"}, + {file = "regex-2022.1.18-cp310-cp310-win_amd64.whl", hash = "sha256:1a171eaac36a08964d023eeff740b18a415f79aeb212169080c170ec42dd5184"}, + {file = "regex-2022.1.18-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:49810f907dfe6de8da5da7d2b238d343e6add62f01a15d03e2195afc180059ed"}, + {file = "regex-2022.1.18-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d2f5c3f7057530afd7b739ed42eb04f1011203bc5e4663e1e1d01bb50f813e3"}, + {file = "regex-2022.1.18-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:85ffd6b1cb0dfb037ede50ff3bef80d9bf7fa60515d192403af6745524524f3b"}, + {file = "regex-2022.1.18-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ba37f11e1d020969e8a779c06b4af866ffb6b854d7229db63c5fdddfceaa917f"}, + {file = "regex-2022.1.18-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:637e27ea1ebe4a561db75a880ac659ff439dec7f55588212e71700bb1ddd5af9"}, + {file = "regex-2022.1.18-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:37978254d9d00cda01acc1997513f786b6b971e57b778fbe7c20e30ae81a97f3"}, + {file = "regex-2022.1.18-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e54a1eb9fd38f2779e973d2f8958fd575b532fe26013405d1afb9ee2374e7ab8"}, + {file = "regex-2022.1.18-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:768632fd8172ae03852e3245f11c8a425d95f65ff444ce46b3e673ae5b057b74"}, + {file = "regex-2022.1.18-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:de2923886b5d3214be951bc2ce3f6b8ac0d6dfd4a0d0e2a4d2e5523d8046fdfb"}, + {file = "regex-2022.1.18-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:1333b3ce73269f986b1fa4d5d395643810074dc2de5b9d262eb258daf37dc98f"}, + {file = "regex-2022.1.18-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:d19a34f8a3429bd536996ad53597b805c10352a8561d8382e05830df389d2b43"}, + {file = "regex-2022.1.18-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:8d2f355a951f60f0843f2368b39970e4667517e54e86b1508e76f92b44811a8a"}, + {file = "regex-2022.1.18-cp36-cp36m-win32.whl", hash = "sha256:2245441445099411b528379dee83e56eadf449db924648e5feb9b747473f42e3"}, + {file = "regex-2022.1.18-cp36-cp36m-win_amd64.whl", hash = "sha256:25716aa70a0d153cd844fe861d4f3315a6ccafce22b39d8aadbf7fcadff2b633"}, + {file = "regex-2022.1.18-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:7e070d3aef50ac3856f2ef5ec7214798453da878bb5e5a16c16a61edf1817cc3"}, + {file = "regex-2022.1.18-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22709d701e7037e64dae2a04855021b62efd64a66c3ceed99dfd684bfef09e38"}, + {file = "regex-2022.1.18-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c9099bf89078675c372339011ccfc9ec310310bf6c292b413c013eb90ffdcafc"}, + {file = "regex-2022.1.18-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04611cc0f627fc4a50bc4a9a2e6178a974c6a6a4aa9c1cca921635d2c47b9c87"}, + {file = "regex-2022.1.18-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:552a39987ac6655dad4bf6f17dd2b55c7b0c6e949d933b8846d2e312ee80005a"}, + {file = "regex-2022.1.18-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e031899cb2bc92c0cf4d45389eff5b078d1936860a1be3aa8c94fa25fb46ed8"}, + {file = "regex-2022.1.18-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:2dacb3dae6b8cc579637a7b72f008bff50a94cde5e36e432352f4ca57b9e54c4"}, + {file = "regex-2022.1.18-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:e5c31d70a478b0ca22a9d2d76d520ae996214019d39ed7dd93af872c7f301e52"}, + {file = "regex-2022.1.18-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:bb804c7d0bfbd7e3f33924ff49757de9106c44e27979e2492819c16972ec0da2"}, + {file = "regex-2022.1.18-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:36b2d700a27e168fa96272b42d28c7ac3ff72030c67b32f37c05616ebd22a202"}, + {file = "regex-2022.1.18-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:16f81025bb3556eccb0681d7946e2b35ff254f9f888cff7d2120e8826330315c"}, + {file = "regex-2022.1.18-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:da80047524eac2acf7c04c18ac7a7da05a9136241f642dd2ed94269ef0d0a45a"}, + {file = "regex-2022.1.18-cp37-cp37m-win32.whl", hash = "sha256:6ca45359d7a21644793de0e29de497ef7f1ae7268e346c4faf87b421fea364e6"}, + {file = "regex-2022.1.18-cp37-cp37m-win_amd64.whl", hash = "sha256:38289f1690a7e27aacd049e420769b996826f3728756859420eeee21cc857118"}, + {file = "regex-2022.1.18-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6014038f52b4b2ac1fa41a58d439a8a00f015b5c0735a0cd4b09afe344c94899"}, + {file = "regex-2022.1.18-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:0b5d6f9aed3153487252d00a18e53f19b7f52a1651bc1d0c4b5844bc286dfa52"}, + {file = "regex-2022.1.18-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9d24b03daf7415f78abc2d25a208f234e2c585e5e6f92f0204d2ab7b9ab48e3"}, + {file = "regex-2022.1.18-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bf594cc7cc9d528338d66674c10a5b25e3cde7dd75c3e96784df8f371d77a298"}, + {file = "regex-2022.1.18-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fd914db437ec25bfa410f8aa0aa2f3ba87cdfc04d9919d608d02330947afaeab"}, + {file = "regex-2022.1.18-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90b6840b6448203228a9d8464a7a0d99aa8fa9f027ef95fe230579abaf8a6ee1"}, + {file = "regex-2022.1.18-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:11772be1eb1748e0e197a40ffb82fb8fd0d6914cd147d841d9703e2bef24d288"}, + {file = "regex-2022.1.18-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a602bdc8607c99eb5b391592d58c92618dcd1537fdd87df1813f03fed49957a6"}, + {file = "regex-2022.1.18-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:7e26eac9e52e8ce86f915fd33380f1b6896a2b51994e40bb094841e5003429b4"}, + {file = "regex-2022.1.18-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:519c0b3a6fbb68afaa0febf0d28f6c4b0a1074aefc484802ecb9709faf181607"}, + {file = "regex-2022.1.18-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:3c7ea86b9ca83e30fa4d4cd0eaf01db3ebcc7b2726a25990966627e39577d729"}, + {file = "regex-2022.1.18-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:51f02ca184518702975b56affde6c573ebad4e411599005ce4468b1014b4786c"}, + {file = "regex-2022.1.18-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:385ccf6d011b97768a640e9d4de25412204fbe8d6b9ae39ff115d4ff03f6fe5d"}, + {file = "regex-2022.1.18-cp38-cp38-win32.whl", hash = "sha256:1f8c0ae0a0de4e19fddaaff036f508db175f6f03db318c80bbc239a1def62d02"}, + {file = "regex-2022.1.18-cp38-cp38-win_amd64.whl", hash = "sha256:760c54ad1b8a9b81951030a7e8e7c3ec0964c1cb9fee585a03ff53d9e531bb8e"}, + {file = "regex-2022.1.18-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:93c20777a72cae8620203ac11c4010365706062aa13aaedd1a21bb07adbb9d5d"}, + {file = "regex-2022.1.18-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6aa427c55a0abec450bca10b64446331b5ca8f79b648531138f357569705bc4a"}, + {file = "regex-2022.1.18-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c38baee6bdb7fe1b110b6b3aaa555e6e872d322206b7245aa39572d3fc991ee4"}, + {file = "regex-2022.1.18-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:752e7ddfb743344d447367baa85bccd3629c2c3940f70506eb5f01abce98ee68"}, + {file = "regex-2022.1.18-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8acef4d8a4353f6678fd1035422a937c2170de58a2b29f7da045d5249e934101"}, + {file = "regex-2022.1.18-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c73d2166e4b210b73d1429c4f1ca97cea9cc090e5302df2a7a0a96ce55373f1c"}, + {file = "regex-2022.1.18-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:24c89346734a4e4d60ecf9b27cac4c1fee3431a413f7aa00be7c4d7bbacc2c4d"}, + {file = "regex-2022.1.18-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:596f5ae2eeddb79b595583c2e0285312b2783b0ec759930c272dbf02f851ff75"}, + {file = "regex-2022.1.18-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ecfe51abf7f045e0b9cdde71ca9e153d11238679ef7b5da6c82093874adf3338"}, + {file = "regex-2022.1.18-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:1d6301f5288e9bdca65fab3de6b7de17362c5016d6bf8ee4ba4cbe833b2eda0f"}, + {file = "regex-2022.1.18-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:93cce7d422a0093cfb3606beae38a8e47a25232eea0f292c878af580a9dc7605"}, + {file = "regex-2022.1.18-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:cf0db26a1f76aa6b3aa314a74b8facd586b7a5457d05b64f8082a62c9c49582a"}, + {file = "regex-2022.1.18-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:defa0652696ff0ba48c8aff5a1fac1eef1ca6ac9c660b047fc8e7623c4eb5093"}, + {file = "regex-2022.1.18-cp39-cp39-win32.whl", hash = "sha256:6db1b52c6f2c04fafc8da17ea506608e6be7086715dab498570c3e55e4f8fbd1"}, + {file = "regex-2022.1.18-cp39-cp39-win_amd64.whl", hash = "sha256:ebaeb93f90c0903233b11ce913a7cb8f6ee069158406e056f884854c737d2442"}, + {file = "regex-2022.1.18.tar.gz", hash = "sha256:97f32dc03a8054a4c4a5ab5d761ed4861e828b2c200febd4e46857069a483916"}, ] requests = [ - {file = "requests-2.28.1-py3-none-any.whl", hash = "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349"}, - {file = "requests-2.28.1.tar.gz", hash = "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983"}, + {file = "requests-2.27.1-py2.py3-none-any.whl", hash = "sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d"}, + {file = "requests-2.27.1.tar.gz", hash = "sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61"}, ] rfc3986 = [ {file = "rfc3986-1.5.0-py2.py3-none-any.whl", hash = "sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97"}, {file = "rfc3986-1.5.0.tar.gz", hash = "sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835"}, ] -rich = [ - {file = "rich-12.6.0-py3-none-any.whl", hash = "sha256:a4eb26484f2c82589bd9a17c73d32a010b1e29d89f1604cd9bf3a2097b81bb5e"}, - {file = "rich-12.6.0.tar.gz", hash = "sha256:ba3a3775974105c221d31141f2c116f4fd65c5ceb0698657a11e9f295ec93fd0"}, -] sanic = [ - {file = "sanic-22.12.0-py3-none-any.whl", hash = "sha256:84edf46cc17d13264ccec0ae6622e43087498f95644dc336ade74a2d5e6c88cb"}, - {file = "sanic-22.12.0.tar.gz", hash = "sha256:e5f81115f838956957046b6c52e7a08c1bd6e8ff530ee1376471eaf1579bfffa"}, + {file = "sanic-21.12.1-py3-none-any.whl", hash = "sha256:53230ba1a1081e6b075e58339a47c07d2bf618119fa40ba88c69fe57c99126f1"}, + {file = "sanic-21.12.1.tar.gz", hash = "sha256:6f15ecfef47d4288aac04d8741833a7701cd94f2d4a28fc5667ce2844363152d"}, ] sanic-routing = [ - {file = "sanic-routing-22.8.0.tar.gz", hash = "sha256:305729b4e0bf01f074044a2a315ff401fa7eeffb009eec1d2c81d35e1038ddfc"}, - {file = "sanic_routing-22.8.0-py3-none-any.whl", hash = "sha256:9a928ed9e19a36bc019223be90a5da0ab88cdd76b101e032510b6a7073c017e9"}, + {file = "sanic-routing-0.7.2.tar.gz", hash = "sha256:139ce88b3f054e7aa336e2ecc8459837092b103b275d3a97609a34092c55374d"}, + {file = "sanic_routing-0.7.2-py3-none-any.whl", hash = "sha256:523034ffd07aca056040e08de438269c9a880722eee1ace3a32e4f74b394d9aa"}, ] sanic-testing = [ - {file = "sanic-testing-22.12.0.tar.gz", hash = "sha256:c9582c9bb9aabd82d3bf9fba2514a0274d0d741d84ce600e3ba2bef7b6c87aed"}, - {file = "sanic_testing-22.12.0-py3-none-any.whl", hash = "sha256:2cc3338207c6aab4cdc6b89264744a3d51ea66685fe1f30f81f9c376f3ee93a3"}, -] -service-identity = [ - {file = "service-identity-21.1.0.tar.gz", hash = "sha256:6e6c6086ca271dc11b033d17c3a8bea9f24ebff920c587da090afc9519419d34"}, - {file = "service_identity-21.1.0-py2.py3-none-any.whl", hash = "sha256:f0b0caac3d40627c3c04d7a51b6e06721857a0e10a8775f2d1d7e72901b3a7db"}, + {file = "sanic-testing-0.8.2.tar.gz", hash = "sha256:dd7123132e159281b14eb6434da811e2082165432aa2c523262e44b2c09c3be0"}, + {file = "sanic_testing-0.8.2-py3-none-any.whl", hash = "sha256:f2c3679cd498351f095d8687a1cc6cc10558fd69e014d060ec21f3a020d5723b"}, ] -setuptools = [ - {file = "setuptools-65.6.3-py3-none-any.whl", hash = "sha256:57f6f22bde4e042978bcd50176fdb381d7c21a9efa4041202288d3737a0c6a54"}, - {file = "setuptools-65.6.3.tar.gz", hash = "sha256:a7620757bf984b58deaf32fc8a4577a9bbc0850cf92c20e1ce41c38c19e5fb75"}, +sentinel = [ + {file = "sentinel-0.3.0-py3-none-any.whl", hash = "sha256:bd8710dd26752039c668604f6be2aaf741b56f7811c5924a4dcdfd74359244f3"}, + {file = "sentinel-0.3.0.tar.gz", hash = "sha256:f28143aa4716dbc8f6193f5682176a3c33cd26aaae05d9ecf66c186a9887cc2d"}, ] six = [ {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, ] sniffio = [ - {file = "sniffio-1.3.0-py3-none-any.whl", hash = "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384"}, - {file = "sniffio-1.3.0.tar.gz", hash = "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101"}, + {file = "sniffio-1.2.0-py3-none-any.whl", hash = "sha256:471b71698eac1c2112a40ce2752bb2f4a4814c22a54a3eed3676bc0f5ca9f663"}, + {file = "sniffio-1.2.0.tar.gz", hash = "sha256:c4666eecec1d3f50960c6bdf61ab7bc350648da6c126e3cf6898d8cd4ddcd3de"}, ] sqlparse = [ - {file = "sqlparse-0.4.3-py3-none-any.whl", hash = "sha256:0323c0ec29cd52bceabc1b4d9d579e311f3e4961b98d174201d5622a23b85e34"}, - {file = "sqlparse-0.4.3.tar.gz", hash = "sha256:69ca804846bb114d2ec380e4360a8a340db83f0ccf3afceeb1404df028f57268"}, + {file = "sqlparse-0.4.2-py3-none-any.whl", hash = "sha256:48719e356bb8b42991bdbb1e8b83223757b93789c00910a616a071910ca4a64d"}, + {file = "sqlparse-0.4.2.tar.gz", hash = "sha256:0c00730c74263a94e5a9919ade150dfc3b19c574389985446148402998287dae"}, ] starlette = [ - {file = "starlette-0.22.0-py3-none-any.whl", hash = "sha256:b5eda991ad5f0ee5d8ce4c4540202a573bb6691ecd0c712262d0bc85cf8f2c50"}, - {file = "starlette-0.22.0.tar.gz", hash = "sha256:b092cbc365bea34dd6840b42861bdabb2f507f8671e642e8272d2442e08ea4ff"}, + {file = "starlette-0.16.0-py3-none-any.whl", hash = "sha256:38eb24bf705a2c317e15868e384c1b8a12ca396e5a3c3a003db7e667c43f939f"}, + {file = "starlette-0.16.0.tar.gz", hash = "sha256:e1904b5d0007aee24bdd3c43994be9b3b729f4f58e740200de1d623f8c3a8870"}, ] -starlite = [ - {file = "starlite-1.50.2-py3-none-any.whl", hash = "sha256:1cca50d66933c56167308a7040b0fcb719e939a8c53263d0dddd09ea174fc67e"}, - {file = "starlite-1.50.2.tar.gz", hash = "sha256:333b8424c3725b4de348cbff1c5a124fe8a780f12ae5bc81c2af84eb8ac1205b"}, +testfixtures = [ + {file = "testfixtures-6.18.3-py2.py3-none-any.whl", hash = "sha256:6ddb7f56a123e1a9339f130a200359092bd0a6455e31838d6c477e8729bb7763"}, + {file = "testfixtures-6.18.3.tar.gz", hash = "sha256:2600100ae96ffd082334b378e355550fef8b4a529a6fa4c34f47130905c7426d"}, ] -tenacity = [ - {file = "tenacity-8.1.0-py3-none-any.whl", hash = "sha256:35525cd47f82830069f0d6b73f7eb83bc5b73ee2fff0437952cedf98b27653ac"}, - {file = "tenacity-8.1.0.tar.gz", hash = "sha256:e48c437fdf9340f5666b92cd7990e96bc5fc955e1298baf4a907e3972067a445"}, +toml = [ + {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, + {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, ] tomli = [ - {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, - {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, -] -twisted = [ - {file = "Twisted-22.4.0-py3-none-any.whl", hash = "sha256:f9f7a91f94932477a9fc3b169d57f54f96c6e74a23d78d9ce54039a7f48928a2"}, - {file = "Twisted-22.4.0.tar.gz", hash = "sha256:a047990f57dfae1e0bd2b7df2526d4f16dcdc843774dc108b78c52f2a5f13680"}, -] -twisted-iocpsupport = [ - {file = "twisted-iocpsupport-1.0.2.tar.gz", hash = "sha256:72068b206ee809c9c596b57b5287259ea41ddb4774d86725b19f35bf56aa32a9"}, - {file = "twisted_iocpsupport-1.0.2-cp310-cp310-win32.whl", hash = "sha256:985c06a33f5c0dae92c71a036d1ea63872ee86a21dd9b01e1f287486f15524b4"}, - {file = "twisted_iocpsupport-1.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:81b3abe3527b367da0220482820cb12a16c661672b7bcfcde328902890d63323"}, - {file = "twisted_iocpsupport-1.0.2-cp36-cp36m-win32.whl", hash = "sha256:9dbb8823b49f06d4de52721b47de4d3b3026064ef4788ce62b1a21c57c3fff6f"}, - {file = "twisted_iocpsupport-1.0.2-cp36-cp36m-win_amd64.whl", hash = "sha256:b9fed67cf0f951573f06d560ac2f10f2a4bbdc6697770113a2fc396ea2cb2565"}, - {file = "twisted_iocpsupport-1.0.2-cp37-cp37m-win32.whl", hash = "sha256:b76b4eed9b27fd63ddb0877efdd2d15835fdcb6baa745cb85b66e5d016ac2878"}, - {file = "twisted_iocpsupport-1.0.2-cp37-cp37m-win_amd64.whl", hash = "sha256:851b3735ca7e8102e661872390e3bce88f8901bece95c25a0c8bb9ecb8a23d32"}, - {file = "twisted_iocpsupport-1.0.2-cp38-cp38-win32.whl", hash = "sha256:bf4133139d77fc706d8f572e6b7d82871d82ec7ef25d685c2351bdacfb701415"}, - {file = "twisted_iocpsupport-1.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:306becd6e22ab6e8e4f36b6bdafd9c92e867c98a5ce517b27fdd27760ee7ae41"}, - {file = "twisted_iocpsupport-1.0.2-cp39-cp39-win32.whl", hash = "sha256:3c61742cb0bc6c1ac117a7e5f422c129832f0c295af49e01d8a6066df8cfc04d"}, - {file = "twisted_iocpsupport-1.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:b435857b9efcbfc12f8c326ef0383f26416272260455bbca2cd8d8eca470c546"}, - {file = "twisted_iocpsupport-1.0.2-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:7d972cfa8439bdcb35a7be78b7ef86d73b34b808c74be56dfa785c8a93b851bf"}, -] -txaio = [ - {file = "txaio-22.2.1-py2.py3-none-any.whl", hash = "sha256:41223af4a9d5726e645a8ee82480f413e5e300dd257db94bc38ae12ea48fb2e5"}, - {file = "txaio-22.2.1.tar.gz", hash = "sha256:2e4582b70f04b2345908254684a984206c0d9b50e3074a24a4c55aba21d24d01"}, + {file = "tomli-1.2.3-py3-none-any.whl", hash = "sha256:e3069e4be3ead9668e21cb9b074cd948f7b3113fd9c8bba083f48247aab8b11c"}, + {file = "tomli-1.2.3.tar.gz", hash = "sha256:05b6166bff487dc068d322585c7ea4ef78deed501cc124060e0f238e89a9231f"}, ] typed-ast = [ - {file = "typed_ast-1.5.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:669dd0c4167f6f2cd9f57041e03c3c2ebf9063d0757dc89f79ba1daa2bfca9d4"}, - {file = "typed_ast-1.5.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:211260621ab1cd7324e0798d6be953d00b74e0428382991adfddb352252f1d62"}, - {file = "typed_ast-1.5.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:267e3f78697a6c00c689c03db4876dd1efdfea2f251a5ad6555e82a26847b4ac"}, - {file = "typed_ast-1.5.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c542eeda69212fa10a7ada75e668876fdec5f856cd3d06829e6aa64ad17c8dfe"}, - {file = "typed_ast-1.5.4-cp310-cp310-win_amd64.whl", hash = "sha256:a9916d2bb8865f973824fb47436fa45e1ebf2efd920f2b9f99342cb7fab93f72"}, - {file = "typed_ast-1.5.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:79b1e0869db7c830ba6a981d58711c88b6677506e648496b1f64ac7d15633aec"}, - {file = "typed_ast-1.5.4-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a94d55d142c9265f4ea46fab70977a1944ecae359ae867397757d836ea5a3f47"}, - {file = "typed_ast-1.5.4-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:183afdf0ec5b1b211724dfef3d2cad2d767cbefac291f24d69b00546c1837fb6"}, - {file = "typed_ast-1.5.4-cp36-cp36m-win_amd64.whl", hash = "sha256:639c5f0b21776605dd6c9dbe592d5228f021404dafd377e2b7ac046b0349b1a1"}, - {file = "typed_ast-1.5.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:cf4afcfac006ece570e32d6fa90ab74a17245b83dfd6655a6f68568098345ff6"}, - {file = "typed_ast-1.5.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed855bbe3eb3715fca349c80174cfcfd699c2f9de574d40527b8429acae23a66"}, - {file = "typed_ast-1.5.4-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6778e1b2f81dfc7bc58e4b259363b83d2e509a65198e85d5700dfae4c6c8ff1c"}, - {file = "typed_ast-1.5.4-cp37-cp37m-win_amd64.whl", hash = "sha256:0261195c2062caf107831e92a76764c81227dae162c4f75192c0d489faf751a2"}, - {file = "typed_ast-1.5.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2efae9db7a8c05ad5547d522e7dbe62c83d838d3906a3716d1478b6c1d61388d"}, - {file = "typed_ast-1.5.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7d5d014b7daa8b0bf2eaef684295acae12b036d79f54178b92a2b6a56f92278f"}, - {file = "typed_ast-1.5.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:370788a63915e82fd6f212865a596a0fefcbb7d408bbbb13dea723d971ed8bdc"}, - {file = "typed_ast-1.5.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4e964b4ff86550a7a7d56345c7864b18f403f5bd7380edf44a3c1fb4ee7ac6c6"}, - {file = "typed_ast-1.5.4-cp38-cp38-win_amd64.whl", hash = "sha256:683407d92dc953c8a7347119596f0b0e6c55eb98ebebd9b23437501b28dcbb8e"}, - {file = "typed_ast-1.5.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4879da6c9b73443f97e731b617184a596ac1235fe91f98d279a7af36c796da35"}, - {file = "typed_ast-1.5.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3e123d878ba170397916557d31c8f589951e353cc95fb7f24f6bb69adc1a8a97"}, - {file = "typed_ast-1.5.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ebd9d7f80ccf7a82ac5f88c521115cc55d84e35bf8b446fcd7836eb6b98929a3"}, - {file = "typed_ast-1.5.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98f80dee3c03455e92796b58b98ff6ca0b2a6f652120c263efdba4d6c5e58f72"}, - {file = "typed_ast-1.5.4-cp39-cp39-win_amd64.whl", hash = "sha256:0fdbcf2fef0ca421a3f5912555804296f0b0960f0418c440f5d6d3abb549f3e1"}, - {file = "typed_ast-1.5.4.tar.gz", hash = "sha256:39e21ceb7388e4bb37f4c679d72707ed46c2fbf2a5609b8b8ebc4b067d977df2"}, + {file = "typed_ast-1.5.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:183b183b7771a508395d2cbffd6db67d6ad52958a5fdc99f450d954003900266"}, + {file = "typed_ast-1.5.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:676d051b1da67a852c0447621fdd11c4e104827417bf216092ec3e286f7da596"}, + {file = "typed_ast-1.5.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bc2542e83ac8399752bc16e0b35e038bdb659ba237f4222616b4e83fb9654985"}, + {file = "typed_ast-1.5.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:74cac86cc586db8dfda0ce65d8bcd2bf17b58668dfcc3652762f3ef0e6677e76"}, + {file = "typed_ast-1.5.2-cp310-cp310-win_amd64.whl", hash = "sha256:18fe320f354d6f9ad3147859b6e16649a0781425268c4dde596093177660e71a"}, + {file = "typed_ast-1.5.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:31d8c6b2df19a777bc8826770b872a45a1f30cfefcfd729491baa5237faae837"}, + {file = "typed_ast-1.5.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:963a0ccc9a4188524e6e6d39b12c9ca24cc2d45a71cfdd04a26d883c922b4b78"}, + {file = "typed_ast-1.5.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:0eb77764ea470f14fcbb89d51bc6bbf5e7623446ac4ed06cbd9ca9495b62e36e"}, + {file = "typed_ast-1.5.2-cp36-cp36m-win_amd64.whl", hash = "sha256:294a6903a4d087db805a7656989f613371915fc45c8cc0ddc5c5a0a8ad9bea4d"}, + {file = "typed_ast-1.5.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:26a432dc219c6b6f38be20a958cbe1abffcc5492821d7e27f08606ef99e0dffd"}, + {file = "typed_ast-1.5.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7407cfcad702f0b6c0e0f3e7ab876cd1d2c13b14ce770e412c0c4b9728a0f88"}, + {file = "typed_ast-1.5.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f30ddd110634c2d7534b2d4e0e22967e88366b0d356b24de87419cc4410c41b7"}, + {file = "typed_ast-1.5.2-cp37-cp37m-win_amd64.whl", hash = "sha256:8c08d6625bb258179b6e512f55ad20f9dfef019bbfbe3095247401e053a3ea30"}, + {file = "typed_ast-1.5.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:90904d889ab8e81a956f2c0935a523cc4e077c7847a836abee832f868d5c26a4"}, + {file = "typed_ast-1.5.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:bbebc31bf11762b63bf61aaae232becb41c5bf6b3461b80a4df7e791fabb3aca"}, + {file = "typed_ast-1.5.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c29dd9a3a9d259c9fa19d19738d021632d673f6ed9b35a739f48e5f807f264fb"}, + {file = "typed_ast-1.5.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:58ae097a325e9bb7a684572d20eb3e1809802c5c9ec7108e85da1eb6c1a3331b"}, + {file = "typed_ast-1.5.2-cp38-cp38-win_amd64.whl", hash = "sha256:da0a98d458010bf4fe535f2d1e367a2e2060e105978873c04c04212fb20543f7"}, + {file = "typed_ast-1.5.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:33b4a19ddc9fc551ebabca9765d54d04600c4a50eda13893dadf67ed81d9a098"}, + {file = "typed_ast-1.5.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1098df9a0592dd4c8c0ccfc2e98931278a6c6c53cb3a3e2cf7e9ee3b06153344"}, + {file = "typed_ast-1.5.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42c47c3b43fe3a39ddf8de1d40dbbfca60ac8530a36c9b198ea5b9efac75c09e"}, + {file = "typed_ast-1.5.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f290617f74a610849bd8f5514e34ae3d09eafd521dceaa6cf68b3f4414266d4e"}, + {file = "typed_ast-1.5.2-cp39-cp39-win_amd64.whl", hash = "sha256:df05aa5b241e2e8045f5f4367a9f6187b09c4cdf8578bb219861c4e27c443db5"}, + {file = "typed_ast-1.5.2.tar.gz", hash = "sha256:525a2d4088e70a9f75b08b3f87a51acc9cde640e19cc523c7e41aa355564ae27"}, ] types-aiofiles = [ - {file = "types-aiofiles-22.1.0.4.tar.gz", hash = "sha256:54465417fdfafc723e6f0fca66de5498866f1cf2595b72c9da35c96d1c3e9fce"}, - {file = "types_aiofiles-22.1.0.4-py3-none-any.whl", hash = "sha256:3c3d389ceec04c78bd33d13304332dcc46856a418d7d4ebf95e398f1efa3a3b7"}, + {file = "types-aiofiles-0.8.3.tar.gz", hash = "sha256:77c455ecfb08a81e39441ce35697870bcba8f2682baad4408b5eb48d9efb02c2"}, + {file = "types_aiofiles-0.8.3-py3-none-any.whl", hash = "sha256:e261d6c0fafc3303c40cab64872609af8c702f6ec6590dc9f04a9bb8aaccc7b2"}, ] types-certifi = [ - {file = "types-certifi-2021.10.8.3.tar.gz", hash = "sha256:72cf7798d165bc0b76e1c10dd1ea3097c7063c42c21d664523b928e88b554a4f"}, - {file = "types_certifi-2021.10.8.3-py3-none-any.whl", hash = "sha256:b2d1e325e69f71f7c78e5943d410e650b4707bb0ef32e4ddf3da37f54176e88a"}, + {file = "types-certifi-2021.10.8.1.tar.gz", hash = "sha256:546cd3ca4466855959fbc8868fd7139a50eb55a2d1fae497e13b60af439597a3"}, + {file = "types_certifi-2021.10.8.1-py3-none-any.whl", hash = "sha256:2290008f32e6ac7c69e779d04fa1bc4c6bb4c7200aa3b3b072ad5475a8968aa5"}, ] types-chardet = [ - {file = "types-chardet-5.0.4.1.tar.gz", hash = "sha256:cb04c8ea105220bf44c1c93f227e09117bbb07aa56d743be2972330fd06751de"}, - {file = "types_chardet-5.0.4.1-py3-none-any.whl", hash = "sha256:651e4b1c593683960fcd0ee5353b37e7d8d3eecebed72277c1ff9b373f86cdaf"}, + {file = "types-chardet-4.0.3.tar.gz", hash = "sha256:519850a12ab0009f3ec5bdca35ce1c0de4eb4a67a2110aa206386e6219b3ecd8"}, + {file = "types_chardet-4.0.3-py3-none-any.whl", hash = "sha256:8990a86d4c7cfa6c6c5889fc49e456e477851e75b5adb396d42ae106d0ae02ea"}, ] types-freezegun = [ - {file = "types-freezegun-1.1.10.tar.gz", hash = "sha256:cb3a2d2eee950eacbaac0673ab50499823365ceb8c655babb1544a41446409ec"}, - {file = "types_freezegun-1.1.10-py3-none-any.whl", hash = "sha256:fadebe72213e0674036153366205038e1f95c8ca96deb4ef9b71ddc15413543e"}, + {file = "types-freezegun-1.1.6.tar.gz", hash = "sha256:5c70a4b7444b8c7dd2800e0063d6fe721ab11209399264fa0f77af253dd8b14f"}, + {file = "types_freezegun-1.1.6-py3-none-any.whl", hash = "sha256:eaa4ccac7f4ff92762b6e5d34c3c4e41a7763b6d09a8595e0224ff1f24c9d4e1"}, ] types-python-dateutil = [ - {file = "types-python-dateutil-2.8.19.6.tar.gz", hash = "sha256:4a6f4cc19ce4ba1a08670871e297bf3802f55d4f129e6aa2443f540b6cf803d2"}, - {file = "types_python_dateutil-2.8.19.6-py3-none-any.whl", hash = "sha256:cfb7d31021c6bce6f3362c69af6e3abb48fe3e08854f02487e844ff910deec2a"}, + {file = "types-python-dateutil-2.8.9.tar.gz", hash = "sha256:90f95a6b6d4faba359287f17a2cae511ccc9d4abc89b01969bdac1185815c05d"}, + {file = "types_python_dateutil-2.8.9-py3-none-any.whl", hash = "sha256:d60db7f5d40ce85ce54e7fb14e4157daf33e24f5a4bfb5f44ee7a5b790dfabd0"}, ] types-requests = [ - {file = "types-requests-2.28.11.8.tar.gz", hash = "sha256:e67424525f84adfbeab7268a159d3c633862dafae15c5b19547ce1b55954f0a3"}, - {file = "types_requests-2.28.11.8-py3-none-any.whl", hash = "sha256:61960554baca0008ae7e2db2bd3b322ca9a144d3e80ce270f5fb640817e40994"}, + {file = "types-requests-2.27.10.tar.gz", hash = "sha256:5dcb088fcaa778efeee6b7fc46967037e983fbfb9fec02594578bd33fd75e555"}, + {file = "types_requests-2.27.10-py3-none-any.whl", hash = "sha256:6cb4fb0bbcbc585c57eeee6ffe5a47638dc89706b8d290ec89a77213fc5bad1a"}, +] +types-setuptools = [ + {file = "types-setuptools-57.4.9.tar.gz", hash = "sha256:536ef74744f8e1e4be4fc719887f886e74e4cf3c792b4a06984320be4df450b5"}, + {file = "types_setuptools-57.4.9-py3-none-any.whl", hash = "sha256:948dc6863373750e2cd0b223a84f1fb608414cde5e55cf38ea657b93aeb411d2"}, ] types-toml = [ - {file = "types-toml-0.10.8.1.tar.gz", hash = "sha256:171bdb3163d79a520560f24ba916a9fc9bff81659c5448a9fea89240923722be"}, - {file = "types_toml-0.10.8.1-py3-none-any.whl", hash = "sha256:b7b5c4977f96ab7b5ac06d8a6590d17c0bf252a96efc03b109c2711fb3e0eafd"}, + {file = "types-toml-0.10.4.tar.gz", hash = "sha256:9340e7c1587715581bb13905b3af30b79fe68afaccfca377665d5e63b694129a"}, + {file = "types_toml-0.10.4-py3-none-any.whl", hash = "sha256:4a9ffd47bbcec49c6fde6351a889b2c1bd3c0ef309fa0eed60dc28e58c8b9ea6"}, ] types-typed-ast = [ - {file = "types-typed-ast-1.5.8.3.tar.gz", hash = "sha256:3a62bc25168f8b44ce74e1114f9fbc2ee87d6e96e3880cbef39aad9522555b4e"}, - {file = "types_typed_ast-1.5.8.3-py3-none-any.whl", hash = "sha256:d945082da658987e6f656e1b82373fe7f16acc1e5fe10bb1ebf2258e1b96a4bd"}, + {file = "types-typed-ast-1.5.2.tar.gz", hash = "sha256:6d8fd2ea90836cb43111ac8aae7cd864e098593c93ecceba90fe46d6b7b0c73e"}, + {file = "types_typed_ast-1.5.2-py3-none-any.whl", hash = "sha256:8f459097ad11d2efc0c7e31f3adce45dc94e1c8950d55e1fe502aeff4b528d9a"}, ] types-ujson = [ - {file = "types-ujson-5.7.0.0.tar.gz", hash = "sha256:d7c001f668a2e1ac2decbafd2c42cc0bf58798a5cc3b5a3ee397b7ee37775919"}, - {file = "types_ujson-5.7.0.0-py3-none-any.whl", hash = "sha256:ad48848c3da10e8023e9e75b1833282c28568ea680938c271e34d4d1426be816"}, + {file = "types-ujson-4.2.1.tar.gz", hash = "sha256:9e7576316914151f4e7086fae4c5aea3a17ae2d66c2edea6525f4c82354c364d"}, + {file = "types_ujson-4.2.1-py3-none-any.whl", hash = "sha256:dd6ef2d1a29561e8fbbe4fa95e3509a1104c8f1d7d77004ae50ff99bfa89320c"}, ] types-urllib3 = [ - {file = "types-urllib3-1.26.25.4.tar.gz", hash = "sha256:eec5556428eec862b1ac578fb69aab3877995a99ffec9e5a12cf7fbd0cc9daee"}, - {file = "types_urllib3-1.26.25.4-py3-none-any.whl", hash = "sha256:ed6b9e8a8be488796f72306889a06a3fc3cb1aa99af02ab8afb50144d7317e49"}, + {file = "types-urllib3-1.26.9.tar.gz", hash = "sha256:abd2d4857837482b1834b4817f0587678dcc531dbc9abe4cde4da28cef3f522c"}, + {file = "types_urllib3-1.26.9-py3-none-any.whl", hash = "sha256:4a54f6274ab1c80968115634a55fb9341a699492b95e32104a7c513db9fe02e9"}, ] typing-extensions = [ - {file = "typing_extensions-4.4.0-py3-none-any.whl", hash = "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e"}, - {file = "typing_extensions-4.4.0.tar.gz", hash = "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa"}, + {file = "typing_extensions-3.10.0.2-py2-none-any.whl", hash = "sha256:d8226d10bc02a29bcc81df19a26e56a9647f8b0a6d4a83924139f4a8b01f17b7"}, + {file = "typing_extensions-3.10.0.2-py3-none-any.whl", hash = "sha256:f1d25edafde516b146ecd0613dabcc61409817af4766fbbcfb8d1ad4ec441a34"}, + {file = "typing_extensions-3.10.0.2.tar.gz", hash = "sha256:49f75d16ff11f1cd258e1b988ccff82a3ca5570217d7ad8c5f48205dd99a677e"}, ] -typing-inspect = [ - {file = "typing_inspect-0.8.0-py3-none-any.whl", hash = "sha256:5fbf9c1e65d4fa01e701fe12a5bca6c6e08a4ffd5bc60bfac028253a447c5188"}, - {file = "typing_inspect-0.8.0.tar.gz", hash = "sha256:8b1ff0c400943b6145df8119c41c244ca8207f1f10c9c057aeed1560e4806e3d"}, +tzdata = [ + {file = "tzdata-2021.5-py2.py3-none-any.whl", hash = "sha256:3eee491e22ebfe1e5cfcc97a4137cd70f092ce59144d81f8924a844de05ba8f5"}, + {file = "tzdata-2021.5.tar.gz", hash = "sha256:68dbe41afd01b867894bbdfd54fa03f468cfa4f0086bfb4adcd8de8f24f3ee21"}, ] ujson = [ - {file = "ujson-5.7.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5eba5e69e4361ac3a311cf44fa71bc619361b6e0626768a494771aacd1c2f09b"}, - {file = "ujson-5.7.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:aae4d9e1b4c7b61780f0a006c897a4a1904f862fdab1abb3ea8f45bd11aa58f3"}, - {file = "ujson-5.7.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d2e43ccdba1cb5c6d3448eadf6fc0dae7be6c77e357a3abc968d1b44e265866d"}, - {file = "ujson-5.7.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54384ce4920a6d35fa9ea8e580bc6d359e3eb961fa7e43f46c78e3ed162d56ff"}, - {file = "ujson-5.7.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:24ad1aa7fc4e4caa41d3d343512ce68e41411fb92adf7f434a4d4b3749dc8f58"}, - {file = "ujson-5.7.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:afff311e9f065a8f03c3753db7011bae7beb73a66189c7ea5fcb0456b7041ea4"}, - {file = "ujson-5.7.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6e80f0d03e7e8646fc3d79ed2d875cebd4c83846e129737fdc4c2532dbd43d9e"}, - {file = "ujson-5.7.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:137831d8a0db302fb6828ee21c67ad63ac537bddc4376e1aab1c8573756ee21c"}, - {file = "ujson-5.7.0-cp310-cp310-win32.whl", hash = "sha256:7df3fd35ebc14dafeea031038a99232b32f53fa4c3ecddb8bed132a43eefb8ad"}, - {file = "ujson-5.7.0-cp310-cp310-win_amd64.whl", hash = "sha256:af4639f684f425177d09ae409c07602c4096a6287027469157bfb6f83e01448b"}, - {file = "ujson-5.7.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9b0f2680ce8a70f77f5d70aaf3f013d53e6af6d7058727a35d8ceb4a71cdd4e9"}, - {file = "ujson-5.7.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:67a19fd8e7d8cc58a169bea99fed5666023adf707a536d8f7b0a3c51dd498abf"}, - {file = "ujson-5.7.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6abb8e6d8f1ae72f0ed18287245f5b6d40094e2656d1eab6d99d666361514074"}, - {file = "ujson-5.7.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8cd622c069368d5074bd93817b31bdb02f8d818e57c29e206f10a1f9c6337dd"}, - {file = "ujson-5.7.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:14f9082669f90e18e64792b3fd0bf19f2b15e7fe467534a35ea4b53f3bf4b755"}, - {file = "ujson-5.7.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d7ff6ebb43bc81b057724e89550b13c9a30eda0f29c2f506f8b009895438f5a6"}, - {file = "ujson-5.7.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:f7f241488879d91a136b299e0c4ce091996c684a53775e63bb442d1a8e9ae22a"}, - {file = "ujson-5.7.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5593263a7fcfb934107444bcfba9dde8145b282de0ee9f61e285e59a916dda0f"}, - {file = "ujson-5.7.0-cp311-cp311-win32.whl", hash = "sha256:26c2b32b489c393106e9cb68d0a02e1a7b9d05a07429d875c46b94ee8405bdb7"}, - {file = "ujson-5.7.0-cp311-cp311-win_amd64.whl", hash = "sha256:ed24406454bb5a31df18f0a423ae14beb27b28cdfa34f6268e7ebddf23da807e"}, - {file = "ujson-5.7.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:18679484e3bf9926342b1c43a3bd640f93a9eeeba19ef3d21993af7b0c44785d"}, - {file = "ujson-5.7.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ee295761e1c6c30400641f0a20d381633d7622633cdf83a194f3c876a0e4b7e"}, - {file = "ujson-5.7.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b738282e12a05f400b291966630a98d622da0938caa4bc93cf65adb5f4281c60"}, - {file = "ujson-5.7.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:00343501dbaa5172e78ef0e37f9ebd08040110e11c12420ff7c1f9f0332d939e"}, - {file = "ujson-5.7.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c0d1f7c3908357ee100aa64c4d1cf91edf99c40ac0069422a4fd5fd23b263263"}, - {file = "ujson-5.7.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:a5d2f44331cf04689eafac7a6596c71d6657967c07ac700b0ae1c921178645da"}, - {file = "ujson-5.7.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:16b2254a77b310f118717715259a196662baa6b1f63b1a642d12ab1ff998c3d7"}, - {file = "ujson-5.7.0-cp37-cp37m-win32.whl", hash = "sha256:6faf46fa100b2b89e4db47206cf8a1ffb41542cdd34dde615b2fc2288954f194"}, - {file = "ujson-5.7.0-cp37-cp37m-win_amd64.whl", hash = "sha256:ff0004c3f5a9a6574689a553d1b7819d1a496b4f005a7451f339dc2d9f4cf98c"}, - {file = "ujson-5.7.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:75204a1dd7ec6158c8db85a2f14a68d2143503f4bafb9a00b63fe09d35762a5e"}, - {file = "ujson-5.7.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7312731c7826e6c99cdd3ac503cd9acd300598e7a80bcf41f604fee5f49f566c"}, - {file = "ujson-5.7.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b9dc5a90e2149643df7f23634fe202fed5ebc787a2a1be95cf23632b4d90651"}, - {file = "ujson-5.7.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b6a6961fc48821d84b1198a09516e396d56551e910d489692126e90bf4887d29"}, - {file = "ujson-5.7.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b01a9af52a0d5c46b2c68e3f258fdef2eacaa0ce6ae3e9eb97983f5b1166edb6"}, - {file = "ujson-5.7.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b7316d3edeba8a403686cdcad4af737b8415493101e7462a70ff73dd0609eafc"}, - {file = "ujson-5.7.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:4ee997799a23227e2319a3f8817ce0b058923dbd31904761b788dc8f53bd3e30"}, - {file = "ujson-5.7.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:dda9aa4c33435147262cd2ea87c6b7a1ca83ba9b3933ff7df34e69fee9fced0c"}, - {file = "ujson-5.7.0-cp38-cp38-win32.whl", hash = "sha256:bea8d30e362180aafecabbdcbe0e1f0b32c9fa9e39c38e4af037b9d3ca36f50c"}, - {file = "ujson-5.7.0-cp38-cp38-win_amd64.whl", hash = "sha256:c96e3b872bf883090ddf32cc41957edf819c5336ab0007d0cf3854e61841726d"}, - {file = "ujson-5.7.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6411aea4c94a8e93c2baac096fbf697af35ba2b2ed410b8b360b3c0957a952d3"}, - {file = "ujson-5.7.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3d3b3499c55911f70d4e074c626acdb79a56f54262c3c83325ffb210fb03e44d"}, - {file = "ujson-5.7.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:341f891d45dd3814d31764626c55d7ab3fd21af61fbc99d070e9c10c1190680b"}, - {file = "ujson-5.7.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2f242eec917bafdc3f73a1021617db85f9958df80f267db69c76d766058f7b19"}, - {file = "ujson-5.7.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c3af9f9f22a67a8c9466a32115d9073c72a33ae627b11de6f592df0ee09b98b6"}, - {file = "ujson-5.7.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4a3d794afbf134df3056a813e5c8a935208cddeae975bd4bc0ef7e89c52f0ce0"}, - {file = "ujson-5.7.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:800bf998e78dae655008dd10b22ca8dc93bdcfcc82f620d754a411592da4bbf2"}, - {file = "ujson-5.7.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b5ac3d5c5825e30b438ea92845380e812a476d6c2a1872b76026f2e9d8060fc2"}, - {file = "ujson-5.7.0-cp39-cp39-win32.whl", hash = "sha256:cd90027e6d93e8982f7d0d23acf88c896d18deff1903dd96140613389b25c0dd"}, - {file = "ujson-5.7.0-cp39-cp39-win_amd64.whl", hash = "sha256:523ee146cdb2122bbd827f4dcc2a8e66607b3f665186bce9e4f78c9710b6d8ab"}, - {file = "ujson-5.7.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:e87cec407ec004cf1b04c0ed7219a68c12860123dfb8902ef880d3d87a71c172"}, - {file = "ujson-5.7.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bab10165db6a7994e67001733f7f2caf3400b3e11538409d8756bc9b1c64f7e8"}, - {file = "ujson-5.7.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b522be14a28e6ac1cf818599aeff1004a28b42df4ed4d7bc819887b9dac915fc"}, - {file = "ujson-5.7.0-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7592f40175c723c032cdbe9fe5165b3b5903604f774ab0849363386e99e1f253"}, - {file = "ujson-5.7.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:ed22f9665327a981f288a4f758a432824dc0314e4195a0eaeb0da56a477da94d"}, - {file = "ujson-5.7.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:adf445a49d9a97a5a4c9bb1d652a1528de09dd1c48b29f79f3d66cea9f826bf6"}, - {file = "ujson-5.7.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:64772a53f3c4b6122ed930ae145184ebaed38534c60f3d859d8c3f00911eb122"}, - {file = "ujson-5.7.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:35209cb2c13fcb9d76d249286105b4897b75a5e7f0efb0c0f4b90f222ce48910"}, - {file = "ujson-5.7.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:90712dfc775b2c7a07d4d8e059dd58636bd6ff1776d79857776152e693bddea6"}, - {file = "ujson-5.7.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:0e4e8981c6e7e9e637e637ad8ffe948a09e5434bc5f52ecbb82b4b4cfc092bfb"}, - {file = "ujson-5.7.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:581c945b811a3d67c27566539bfcb9705ea09cb27c4be0002f7a553c8886b817"}, - {file = "ujson-5.7.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d36a807a24c7d44f71686685ae6fbc8793d784bca1adf4c89f5f780b835b6243"}, - {file = "ujson-5.7.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b4257307e3662aa65e2644a277ca68783c5d51190ed9c49efebdd3cbfd5fa44"}, - {file = "ujson-5.7.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea7423d8a2f9e160c5e011119741682414c5b8dce4ae56590a966316a07a4618"}, - {file = "ujson-5.7.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:4c592eb91a5968058a561d358d0fef59099ed152cfb3e1cd14eee51a7a93879e"}, - {file = "ujson-5.7.0.tar.gz", hash = "sha256:e788e5d5dcae8f6118ac9b45d0b891a0d55f7ac480eddcb7f07263f2bcf37b23"}, + {file = "ujson-5.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:644552d1e89983c08d0c24358fbcb5829ae5b5deee9d876e16d20085cfa7dc81"}, + {file = "ujson-5.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0cae4a9c141856f7ad1a79c17ff1aaebf7fd8faa2f2c2614c37d6f82ed261d96"}, + {file = "ujson-5.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ba63b789d83ca92237dbc72041a268d91559f981c01763a107105878bae442e"}, + {file = "ujson-5.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe4e8f71e2fd42dce245bace7e2aa97dabef13926750a351eadca89a1e0f1abd"}, + {file = "ujson-5.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f73946c047a38640b1f5a2a459237b7bdc417ab028a76c796e4eea984b359b9"}, + {file = "ujson-5.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:afe91153c2046fa8210b92def513124e0ea5b87ad8fa4c14fef8197204b980f1"}, + {file = "ujson-5.1.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:b1ef400fc73ab0cb61b74a662ad4207917223aba6f933a9fea9b0fbe75de2361"}, + {file = "ujson-5.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:5c8a884d60dd2eed2fc95a9474d57ead82adf254f54caffb3d9e8ed185c49aba"}, + {file = "ujson-5.1.0-cp310-cp310-win32.whl", hash = "sha256:173b90a2c2836ee42f708df88ecfe3efbc4d868df73c9fcea8cb8f6f3ab93892"}, + {file = "ujson-5.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:6c45ad95e82155372d9908774db46e0ef7880af28a734d0b14eaa4f505e64982"}, + {file = "ujson-5.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4155a7c29bf330329519027c815e15e381c1fff22f50d26f135584d482bbd95d"}, + {file = "ujson-5.1.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fa616d0d3c594785c6e9b7f42686bb1c86f9e64aa0f30a72c86d8eb315f54194"}, + {file = "ujson-5.1.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a48efcb5d3695b295c26835ed81048da8cd40e76c4fde2940c807aa452b560c9"}, + {file = "ujson-5.1.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:838d35eb9006d36f9241e95958d9f4819bcf1ea2ec155daf92d5751c31bcc62b"}, + {file = "ujson-5.1.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:05aa6c7297a22081f65497b6f586de6b7060ea47c3ecda80896f47200e9dbf04"}, + {file = "ujson-5.1.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:ce441ab7ad1db592e2db95b6c2a1eb882123532897340afac1342c28819e9833"}, + {file = "ujson-5.1.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:9937e819196b894ffd00801b24f1042dabda142f355313c3f20410993219bc4f"}, + {file = "ujson-5.1.0-cp37-cp37m-win32.whl", hash = "sha256:06bed66ae62d517f67a61cf53c056800b35ef364270723168a1db62702e2d30c"}, + {file = "ujson-5.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:74e41a0222e6e8136e38f103d6cc228e4e20f1c35cc80224a42804fd67fb35c8"}, + {file = "ujson-5.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7bbb87f040e618bebe8c6257b3e4e8ae2f708dcbff3270c84718b3360a152799"}, + {file = "ujson-5.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:68e38122115a8097fbe1cfe52979a797eaff91c10c1bf4b27774e5f30e7f723a"}, + {file = "ujson-5.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b09843123425337d2efee5c8ff6519e4dfc7b044db66c8bd560517fc1070a157"}, + {file = "ujson-5.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dca10174a3bd482d969a2d12d0aec2fdd63fb974e255ec0147e36a516a2d68a"}, + {file = "ujson-5.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:202ae52f4a53f03c42ead6d046b1a146517e93bd757f517bdeef0a26228e0260"}, + {file = "ujson-5.1.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:7a4bed7bd7b288cf73ba47bda27fdd1d78ef6906831489e7f296aef9e786eccb"}, + {file = "ujson-5.1.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:d423956f8dfd98a075c9338b886414b6e3c2817dbf67935797466c998af39936"}, + {file = "ujson-5.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:083c1078e4de3a39019e590c43865b17e07a763fee25b012e650bb4f42c89703"}, + {file = "ujson-5.1.0-cp38-cp38-win32.whl", hash = "sha256:31671ad99f0395eb881d698f2871dc64ff00fbd4380c5d9bfd8bff3d4c8f8d88"}, + {file = "ujson-5.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:994eaf4369e6bc24258f59fe8c6345037abcf24557571814e27879851c4353aa"}, + {file = "ujson-5.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:00d6ea9702c2eaeaf1a826934eaba1b4c609c873379bf54e36ba7b7e128edf94"}, + {file = "ujson-5.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a53c4fe8e1c067e6c98b4526e982ed9486f08578ad8eb5f0e94f8cadf0c1d911"}, + {file = "ujson-5.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:368f855779fded560724a6448838304621f498113a116d66bc5ed5ad5ad3ca92"}, + {file = "ujson-5.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4dd97e45a0f450ba2c43cda18147e54b8e41e886c22e3506c62f7d61e9e53b0d"}, + {file = "ujson-5.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:caeadbf95ce277f1f8f4f71913bc20c01f49fc9228f238920f9ff6f7645d2a5f"}, + {file = "ujson-5.1.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:681fed63c948f757466eeb3aea98873e2ab8b2b18e9020c96a97479a513e2018"}, + {file = "ujson-5.1.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:6fc4376266ae67f6d8f9e69386ab950eb84ba345c6fdbeb1884fa5b773c8c76b"}, + {file = "ujson-5.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:585271d6ad545a2ccfc237582f70c160e627735c89d0ca2bde24afa321bc0750"}, + {file = "ujson-5.1.0-cp39-cp39-win32.whl", hash = "sha256:b631af423e6d5d35f9f37fbcc4fbdb6085abc1c441cf864c64b7fbb5b150faf7"}, + {file = "ujson-5.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:08265db5ccff8b521ff68aee13a417d68cca784d7e711d961b92fda6ccffcc4f"}, + {file = "ujson-5.1.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:e2b1c372583eb4363b42e21222d3a18116a41973781d502d61e1b0daf4b8352f"}, + {file = "ujson-5.1.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:51142c9d40439f299594e399bef8892a16586ded54c88d3af926865ca221a177"}, + {file = "ujson-5.1.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ba8be1717b1867a85b2413a8585bad0e4507a22d6af2c244e1c74151f6d5cc0"}, + {file = "ujson-5.1.0-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d0b26d9d6eb9a0979d37f28c715e717a409c9e03163e5cd8fa73aab806351ab5"}, + {file = "ujson-5.1.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:b2c7e4afde0d36926b091fa9613b18b65e911fcaa60024e8721f2dcfedc25329"}, + {file = "ujson-5.1.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:110633a8dda6c8ca78090292231e15381f8b2423e998399d4bc5f135149c722b"}, + {file = "ujson-5.1.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdac161127ef8e0889180a4c07475457c55fe0bbd644436d8f4c7ef07565d653"}, + {file = "ujson-5.1.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:452990c2b18445a7379a45873527d2ec47789b9289c13a17a3c1cc76b9641126"}, + {file = "ujson-5.1.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5304ad25d100d50b5bc8513ef110335df678f66c7ccf3d4728c0c3aa69e08e0c"}, + {file = "ujson-5.1.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:ce620a6563b21aa3fbb1658bc1bfddb484a6dad542de1efb5121eb7bb4f2b93a"}, + {file = "ujson-5.1.0.tar.gz", hash = "sha256:a88944d2f99db71a3ca0c63d81f37e55b660edde0b07216fb65a3e46403ef004"}, ] urllib3 = [ - {file = "urllib3-1.26.13-py2.py3-none-any.whl", hash = "sha256:47cc05d99aaa09c9e72ed5809b60e7ba354e64b59c9c173ac3018642d8bb41fc"}, - {file = "urllib3-1.26.13.tar.gz", hash = "sha256:c083dd0dce68dbfbe1129d5271cb90f9447dea7d52097c6e0126120c521ddea8"}, + {file = "urllib3-1.26.8-py2.py3-none-any.whl", hash = "sha256:000ca7f471a233c2251c6c7023ee85305721bfdf18621ebff4fd17a8653427ed"}, + {file = "urllib3-1.26.8.tar.gz", hash = "sha256:0e7c33d9a63e7ddfcb86780aac87befc2fbddf46c58dbb487e0855f7ceec283c"}, ] uvicorn = [ - {file = "uvicorn-0.20.0-py3-none-any.whl", hash = "sha256:c3ed1598a5668208723f2bb49336f4509424ad198d6ab2615b7783db58d919fd"}, - {file = "uvicorn-0.20.0.tar.gz", hash = "sha256:a4e12017b940247f836bc90b72e725d7dfd0c8ed1c51eb365f5ba30d9f5127d8"}, + {file = "uvicorn-0.17.5-py3-none-any.whl", hash = "sha256:8adddf629b79857b48b999ae1b14d6c92c95d4d7840bd86461f09bee75f1653e"}, + {file = "uvicorn-0.17.5.tar.gz", hash = "sha256:c04a9c069111489c324f427501b3840d306c6b91a77b00affc136a840a3f45f1"}, ] uvloop = [ - {file = "uvloop-0.17.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ce9f61938d7155f79d3cb2ffa663147d4a76d16e08f65e2c66b77bd41b356718"}, - {file = "uvloop-0.17.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:68532f4349fd3900b839f588972b3392ee56042e440dd5873dfbbcd2cc67617c"}, - {file = "uvloop-0.17.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0949caf774b9fcefc7c5756bacbbbd3fc4c05a6b7eebc7c7ad6f825b23998d6d"}, - {file = "uvloop-0.17.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ff3d00b70ce95adce264462c930fbaecb29718ba6563db354608f37e49e09024"}, - {file = "uvloop-0.17.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:a5abddb3558d3f0a78949c750644a67be31e47936042d4f6c888dd6f3c95f4aa"}, - {file = "uvloop-0.17.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8efcadc5a0003d3a6e887ccc1fb44dec25594f117a94e3127954c05cf144d811"}, - {file = "uvloop-0.17.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3378eb62c63bf336ae2070599e49089005771cc651c8769aaad72d1bd9385a7c"}, - {file = "uvloop-0.17.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6aafa5a78b9e62493539456f8b646f85abc7093dd997f4976bb105537cf2635e"}, - {file = "uvloop-0.17.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c686a47d57ca910a2572fddfe9912819880b8765e2f01dc0dd12a9bf8573e539"}, - {file = "uvloop-0.17.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:864e1197139d651a76c81757db5eb199db8866e13acb0dfe96e6fc5d1cf45fc4"}, - {file = "uvloop-0.17.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:2a6149e1defac0faf505406259561bc14b034cdf1d4711a3ddcdfbaa8d825a05"}, - {file = "uvloop-0.17.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:6708f30db9117f115eadc4f125c2a10c1a50d711461699a0cbfaa45b9a78e376"}, - {file = "uvloop-0.17.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:23609ca361a7fc587031429fa25ad2ed7242941adec948f9d10c045bfecab06b"}, - {file = "uvloop-0.17.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2deae0b0fb00a6af41fe60a675cec079615b01d68beb4cc7b722424406b126a8"}, - {file = "uvloop-0.17.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45cea33b208971e87a31c17622e4b440cac231766ec11e5d22c76fab3bf9df62"}, - {file = "uvloop-0.17.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:9b09e0f0ac29eee0451d71798878eae5a4e6a91aa275e114037b27f7db72702d"}, - {file = "uvloop-0.17.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:dbbaf9da2ee98ee2531e0c780455f2841e4675ff580ecf93fe5c48fe733b5667"}, - {file = "uvloop-0.17.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:a4aee22ece20958888eedbad20e4dbb03c37533e010fb824161b4f05e641f738"}, - {file = "uvloop-0.17.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:307958f9fc5c8bb01fad752d1345168c0abc5d62c1b72a4a8c6c06f042b45b20"}, - {file = "uvloop-0.17.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ebeeec6a6641d0adb2ea71dcfb76017602ee2bfd8213e3fcc18d8f699c5104f"}, - {file = "uvloop-0.17.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1436c8673c1563422213ac6907789ecb2b070f5939b9cbff9ef7113f2b531595"}, - {file = "uvloop-0.17.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:8887d675a64cfc59f4ecd34382e5b4f0ef4ae1da37ed665adba0c2badf0d6578"}, - {file = "uvloop-0.17.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:3db8de10ed684995a7f34a001f15b374c230f7655ae840964d51496e2f8a8474"}, - {file = "uvloop-0.17.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7d37dccc7ae63e61f7b96ee2e19c40f153ba6ce730d8ba4d3b4e9738c1dccc1b"}, - {file = "uvloop-0.17.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:cbbe908fda687e39afd6ea2a2f14c2c3e43f2ca88e3a11964b297822358d0e6c"}, - {file = "uvloop-0.17.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3d97672dc709fa4447ab83276f344a165075fd9f366a97b712bdd3fee05efae8"}, - {file = "uvloop-0.17.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f1e507c9ee39c61bfddd79714e4f85900656db1aec4d40c6de55648e85c2799c"}, - {file = "uvloop-0.17.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:c092a2c1e736086d59ac8e41f9c98f26bbf9b9222a76f21af9dfe949b99b2eb9"}, - {file = "uvloop-0.17.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:30babd84706115626ea78ea5dbc7dd8d0d01a2e9f9b306d24ca4ed5796c66ded"}, - {file = "uvloop-0.17.0.tar.gz", hash = "sha256:0ddf6baf9cf11a1a22c71487f39f15b2cf78eb5bde7e5b45fbb99e8a9d91b9e1"}, + {file = "uvloop-0.16.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:6224f1401025b748ffecb7a6e2652b17768f30b1a6a3f7b44660e5b5b690b12d"}, + {file = "uvloop-0.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:30ba9dcbd0965f5c812b7c2112a1ddf60cf904c1c160f398e7eed3a6b82dcd9c"}, + {file = "uvloop-0.16.0-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:bd53f7f5db562f37cd64a3af5012df8cac2c464c97e732ed556800129505bd64"}, + {file = "uvloop-0.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:772206116b9b57cd625c8a88f2413df2fcfd0b496eb188b82a43bed7af2c2ec9"}, + {file = "uvloop-0.16.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b572256409f194521a9895aef274cea88731d14732343da3ecdb175228881638"}, + {file = "uvloop-0.16.0-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:04ff57aa137230d8cc968f03481176041ae789308b4d5079118331ab01112450"}, + {file = "uvloop-0.16.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3a19828c4f15687675ea912cc28bbcb48e9bb907c801873bd1519b96b04fb805"}, + {file = "uvloop-0.16.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e814ac2c6f9daf4c36eb8e85266859f42174a4ff0d71b99405ed559257750382"}, + {file = "uvloop-0.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bd8f42ea1ea8f4e84d265769089964ddda95eb2bb38b5cbe26712b0616c3edee"}, + {file = "uvloop-0.16.0-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:647e481940379eebd314c00440314c81ea547aa636056f554d491e40503c8464"}, + {file = "uvloop-0.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e0d26fa5875d43ddbb0d9d79a447d2ace4180d9e3239788208527c4784f7cab"}, + {file = "uvloop-0.16.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:6ccd57ae8db17d677e9e06192e9c9ec4bd2066b77790f9aa7dede2cc4008ee8f"}, + {file = "uvloop-0.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:089b4834fd299d82d83a25e3335372f12117a7d38525217c2258e9b9f4578897"}, + {file = "uvloop-0.16.0-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98d117332cc9e5ea8dfdc2b28b0a23f60370d02e1395f88f40d1effd2cb86c4f"}, + {file = "uvloop-0.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e5f2e2ff51aefe6c19ee98af12b4ae61f5be456cd24396953244a30880ad861"}, + {file = "uvloop-0.16.0.tar.gz", hash = "sha256:f74bc20c7b67d1c27c72601c78cf95be99d5c2cdd4514502b4f3eb0933ff1228"}, +] +virtualenv = [ + {file = "virtualenv-20.13.1-py2.py3-none-any.whl", hash = "sha256:45e1d053cad4cd453181ae877c4ffc053546ae99e7dd049b9ff1d9be7491abf7"}, + {file = "virtualenv-20.13.1.tar.gz", hash = "sha256:e0621bcbf4160e4e1030f05065c8834b4e93f4fcc223255db2a823440aca9c14"}, ] wcwidth = [ {file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"}, {file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"}, ] websockets = [ - {file = "websockets-10.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d58804e996d7d2307173d56c297cf7bc132c52df27a3efaac5e8d43e36c21c48"}, - {file = "websockets-10.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bc0b82d728fe21a0d03e65f81980abbbcb13b5387f733a1a870672c5be26edab"}, - {file = "websockets-10.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ba089c499e1f4155d2a3c2a05d2878a3428cf321c848f2b5a45ce55f0d7d310c"}, - {file = "websockets-10.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:33d69ca7612f0ddff3316b0c7b33ca180d464ecac2d115805c044bf0a3b0d032"}, - {file = "websockets-10.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62e627f6b6d4aed919a2052efc408da7a545c606268d5ab5bfab4432734b82b4"}, - {file = "websockets-10.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:38ea7b82bfcae927eeffc55d2ffa31665dc7fec7b8dc654506b8e5a518eb4d50"}, - {file = "websockets-10.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e0cb5cc6ece6ffa75baccfd5c02cffe776f3f5c8bf486811f9d3ea3453676ce8"}, - {file = "websockets-10.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:ae5e95cfb53ab1da62185e23b3130e11d64431179debac6dc3c6acf08760e9b1"}, - {file = "websockets-10.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7c584f366f46ba667cfa66020344886cf47088e79c9b9d39c84ce9ea98aaa331"}, - {file = "websockets-10.4-cp310-cp310-win32.whl", hash = "sha256:b029fb2032ae4724d8ae8d4f6b363f2cc39e4c7b12454df8df7f0f563ed3e61a"}, - {file = "websockets-10.4-cp310-cp310-win_amd64.whl", hash = "sha256:8dc96f64ae43dde92530775e9cb169979f414dcf5cff670455d81a6823b42089"}, - {file = "websockets-10.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:47a2964021f2110116cc1125b3e6d87ab5ad16dea161949e7244ec583b905bb4"}, - {file = "websockets-10.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e789376b52c295c4946403bd0efecf27ab98f05319df4583d3c48e43c7342c2f"}, - {file = "websockets-10.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7d3f0b61c45c3fa9a349cf484962c559a8a1d80dae6977276df8fd1fa5e3cb8c"}, - {file = "websockets-10.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f55b5905705725af31ccef50e55391621532cd64fbf0bc6f4bac935f0fccec46"}, - {file = "websockets-10.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:00c870522cdb69cd625b93f002961ffb0c095394f06ba8c48f17eef7c1541f96"}, - {file = "websockets-10.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f38706e0b15d3c20ef6259fd4bc1700cd133b06c3c1bb108ffe3f8947be15fa"}, - {file = "websockets-10.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f2c38d588887a609191d30e902df2a32711f708abfd85d318ca9b367258cfd0c"}, - {file = "websockets-10.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:fe10ddc59b304cb19a1bdf5bd0a7719cbbc9fbdd57ac80ed436b709fcf889106"}, - {file = "websockets-10.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:90fcf8929836d4a0e964d799a58823547df5a5e9afa83081761630553be731f9"}, - {file = "websockets-10.4-cp311-cp311-win32.whl", hash = "sha256:b9968694c5f467bf67ef97ae7ad4d56d14be2751000c1207d31bf3bb8860bae8"}, - {file = "websockets-10.4-cp311-cp311-win_amd64.whl", hash = "sha256:a7a240d7a74bf8d5cb3bfe6be7f21697a28ec4b1a437607bae08ac7acf5b4882"}, - {file = "websockets-10.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:74de2b894b47f1d21cbd0b37a5e2b2392ad95d17ae983e64727e18eb281fe7cb"}, - {file = "websockets-10.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e3a686ecb4aa0d64ae60c9c9f1a7d5d46cab9bfb5d91a2d303d00e2cd4c4c5cc"}, - {file = "websockets-10.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b0d15c968ea7a65211e084f523151dbf8ae44634de03c801b8bd070b74e85033"}, - {file = "websockets-10.4-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00213676a2e46b6ebf6045bc11d0f529d9120baa6f58d122b4021ad92adabd41"}, - {file = "websockets-10.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:e23173580d740bf8822fd0379e4bf30aa1d5a92a4f252d34e893070c081050df"}, - {file = "websockets-10.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:dd500e0a5e11969cdd3320935ca2ff1e936f2358f9c2e61f100a1660933320ea"}, - {file = "websockets-10.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:4239b6027e3d66a89446908ff3027d2737afc1a375f8fd3eea630a4842ec9a0c"}, - {file = "websockets-10.4-cp37-cp37m-win32.whl", hash = "sha256:8a5cc00546e0a701da4639aa0bbcb0ae2bb678c87f46da01ac2d789e1f2d2038"}, - {file = "websockets-10.4-cp37-cp37m-win_amd64.whl", hash = "sha256:a9f9a735deaf9a0cadc2d8c50d1a5bcdbae8b6e539c6e08237bc4082d7c13f28"}, - {file = "websockets-10.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5c1289596042fad2cdceb05e1ebf7aadf9995c928e0da2b7a4e99494953b1b94"}, - {file = "websockets-10.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0cff816f51fb33c26d6e2b16b5c7d48eaa31dae5488ace6aae468b361f422b63"}, - {file = "websockets-10.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:dd9becd5fe29773d140d68d607d66a38f60e31b86df75332703757ee645b6faf"}, - {file = "websockets-10.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45ec8e75b7dbc9539cbfafa570742fe4f676eb8b0d3694b67dabe2f2ceed8aa6"}, - {file = "websockets-10.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f72e5cd0f18f262f5da20efa9e241699e0cf3a766317a17392550c9ad7b37d8"}, - {file = "websockets-10.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:185929b4808b36a79c65b7865783b87b6841e852ef5407a2fb0c03381092fa3b"}, - {file = "websockets-10.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:7d27a7e34c313b3a7f91adcd05134315002aaf8540d7b4f90336beafaea6217c"}, - {file = "websockets-10.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:884be66c76a444c59f801ac13f40c76f176f1bfa815ef5b8ed44321e74f1600b"}, - {file = "websockets-10.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:931c039af54fc195fe6ad536fde4b0de04da9d5916e78e55405436348cfb0e56"}, - {file = "websockets-10.4-cp38-cp38-win32.whl", hash = "sha256:db3c336f9eda2532ec0fd8ea49fef7a8df8f6c804cdf4f39e5c5c0d4a4ad9a7a"}, - {file = "websockets-10.4-cp38-cp38-win_amd64.whl", hash = "sha256:48c08473563323f9c9debac781ecf66f94ad5a3680a38fe84dee5388cf5acaf6"}, - {file = "websockets-10.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:40e826de3085721dabc7cf9bfd41682dadc02286d8cf149b3ad05bff89311e4f"}, - {file = "websockets-10.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:56029457f219ade1f2fc12a6504ea61e14ee227a815531f9738e41203a429112"}, - {file = "websockets-10.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f5fc088b7a32f244c519a048c170f14cf2251b849ef0e20cbbb0fdf0fdaf556f"}, - {file = "websockets-10.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2fc8709c00704194213d45e455adc106ff9e87658297f72d544220e32029cd3d"}, - {file = "websockets-10.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0154f7691e4fe6c2b2bc275b5701e8b158dae92a1ab229e2b940efe11905dff4"}, - {file = "websockets-10.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c6d2264f485f0b53adf22697ac11e261ce84805c232ed5dbe6b1bcb84b00ff0"}, - {file = "websockets-10.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9bc42e8402dc5e9905fb8b9649f57efcb2056693b7e88faa8fb029256ba9c68c"}, - {file = "websockets-10.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:edc344de4dac1d89300a053ac973299e82d3db56330f3494905643bb68801269"}, - {file = "websockets-10.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:84bc2a7d075f32f6ed98652db3a680a17a4edb21ca7f80fe42e38753a58ee02b"}, - {file = "websockets-10.4-cp39-cp39-win32.whl", hash = "sha256:c94ae4faf2d09f7c81847c63843f84fe47bf6253c9d60b20f25edfd30fb12588"}, - {file = "websockets-10.4-cp39-cp39-win_amd64.whl", hash = "sha256:bbccd847aa0c3a69b5f691a84d2341a4f8a629c6922558f2a70611305f902d74"}, - {file = "websockets-10.4-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:82ff5e1cae4e855147fd57a2863376ed7454134c2bf49ec604dfe71e446e2193"}, - {file = "websockets-10.4-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d210abe51b5da0ffdbf7b43eed0cfdff8a55a1ab17abbec4301c9ff077dd0342"}, - {file = "websockets-10.4-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:942de28af58f352a6f588bc72490ae0f4ccd6dfc2bd3de5945b882a078e4e179"}, - {file = "websockets-10.4-pp37-pypy37_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9b27d6c1c6cd53dc93614967e9ce00ae7f864a2d9f99fe5ed86706e1ecbf485"}, - {file = "websockets-10.4-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:3d3cac3e32b2c8414f4f87c1b2ab686fa6284a980ba283617404377cd448f631"}, - {file = "websockets-10.4-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:da39dd03d130162deb63da51f6e66ed73032ae62e74aaccc4236e30edccddbb0"}, - {file = "websockets-10.4-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:389f8dbb5c489e305fb113ca1b6bdcdaa130923f77485db5b189de343a179393"}, - {file = "websockets-10.4-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09a1814bb15eff7069e51fed0826df0bc0702652b5cb8f87697d469d79c23576"}, - {file = "websockets-10.4-pp38-pypy38_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ff64a1d38d156d429404aaa84b27305e957fd10c30e5880d1765c9480bea490f"}, - {file = "websockets-10.4-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:b343f521b047493dc4022dd338fc6db9d9282658862756b4f6fd0e996c1380e1"}, - {file = "websockets-10.4-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:932af322458da7e4e35df32f050389e13d3d96b09d274b22a7aa1808f292fee4"}, - {file = "websockets-10.4-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6a4162139374a49eb18ef5b2f4da1dd95c994588f5033d64e0bbfda4b6b6fcf"}, - {file = "websockets-10.4-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c57e4c1349fbe0e446c9fa7b19ed2f8a4417233b6984277cce392819123142d3"}, - {file = "websockets-10.4-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b627c266f295de9dea86bd1112ed3d5fafb69a348af30a2422e16590a8ecba13"}, - {file = "websockets-10.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:05a7233089f8bd355e8cbe127c2e8ca0b4ea55467861906b80d2ebc7db4d6b72"}, - {file = "websockets-10.4.tar.gz", hash = "sha256:eef610b23933c54d5d921c92578ae5f89813438fded840c2e9809d378dc765d3"}, + {file = "websockets-10.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:38db6e2163b021642d0a43200ee2dec8f4980bdbda96db54fde72b283b54cbfc"}, + {file = "websockets-10.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e1b60fd297adb9fc78375778a5220da7f07bf54d2a33ac781319650413fc6a60"}, + {file = "websockets-10.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3477146d1f87ead8df0f27e8960249f5248dceb7c2741e8bbec9aa5338d0c053"}, + {file = "websockets-10.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb01ea7b5f52e7125bdc3c5807aeaa2d08a0553979cf2d96a8b7803ea33e15e7"}, + {file = "websockets-10.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:9fd62c6dc83d5d35fb6a84ff82ec69df8f4657fff05f9cd6c7d9bec0dd57f0f6"}, + {file = "websockets-10.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3bbf080f3892ba1dc8838786ec02899516a9d227abe14a80ef6fd17d4fb57127"}, + {file = "websockets-10.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:5560558b0dace8312c46aa8915da977db02738ac8ecffbc61acfbfe103e10155"}, + {file = "websockets-10.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:667c41351a6d8a34b53857ceb8343a45c85d438ee4fd835c279591db8aeb85be"}, + {file = "websockets-10.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:468f0031fdbf4d643f89403a66383247eb82803430b14fa27ce2d44d2662ca37"}, + {file = "websockets-10.1-cp310-cp310-win32.whl", hash = "sha256:d0d81b46a5c87d443e40ce2272436da8e6092aa91f5fbeb60d1be9f11eff5b4c"}, + {file = "websockets-10.1-cp310-cp310-win_amd64.whl", hash = "sha256:b68b6caecb9a0c6db537aa79750d1b592a841e4f1a380c6196091e65b2ad35f9"}, + {file = "websockets-10.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a249139abc62ef333e9e85064c27fefb113b16ffc5686cefc315bdaef3eefbc8"}, + {file = "websockets-10.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8877861e3dee38c8d302eee0d5dbefa6663de3b46dc6a888f70cd7e82562d1f7"}, + {file = "websockets-10.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:e3872ae57acd4306ecf937d36177854e218e999af410a05c17168cd99676c512"}, + {file = "websockets-10.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:b66e6d514f12c28d7a2d80bb2a48ef223342e99c449782d9831b0d29a9e88a17"}, + {file = "websockets-10.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:9f304a22ece735a3da8a51309bc2c010e23961a8f675fae46fdf62541ed62123"}, + {file = "websockets-10.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:189ed478395967d6a98bb293abf04e8815349e17456a0a15511f1088b6cb26e4"}, + {file = "websockets-10.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:08a42856158307e231b199671c4fce52df5786dd3d703f36b5d8ac76b206c485"}, + {file = "websockets-10.1-cp37-cp37m-win32.whl", hash = "sha256:3ef6f73854cded34e78390dbdf40dfdcf0b89b55c0e282468ef92646fce8d13a"}, + {file = "websockets-10.1-cp37-cp37m-win_amd64.whl", hash = "sha256:89e985d40d407545d5f5e2e58e1fdf19a22bd2d8cd54d20a882e29f97e930a0a"}, + {file = "websockets-10.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:002071169d2e44ce8eb9e5ebac9fbce142ba4b5146eef1cfb16b177a27662657"}, + {file = "websockets-10.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cfae282c2aa7f0c4be45df65c248481f3509f8c40ca8b15ed96c35668ae0ff69"}, + {file = "websockets-10.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:97b4b68a2ddaf5c4707ae79c110bfd874c5be3c6ac49261160fb243fa45d8bbb"}, + {file = "websockets-10.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7c9407719f42cb77049975410490c58a705da6af541adb64716573e550e5c9db"}, + {file = "websockets-10.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:1d858fb31e5ac992a2cdf17e874c95f8a5b1e917e1fb6b45ad85da30734b223f"}, + {file = "websockets-10.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:7bdd3d26315db0a9cf8a0af30ca95e0aa342eda9c1377b722e71ccd86bc5d1dd"}, + {file = "websockets-10.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:e259be0863770cb91b1a6ccf6907f1ac2f07eff0b7f01c249ed751865a70cb0d"}, + {file = "websockets-10.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:6b014875fae19577a392372075e937ebfebf53fd57f613df07b35ab210f31534"}, + {file = "websockets-10.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:98de71f86bdb29430fd7ba9997f47a6b10866800e3ea577598a786a785701bb0"}, + {file = "websockets-10.1-cp38-cp38-win32.whl", hash = "sha256:3a02ab91d84d9056a9ee833c254895421a6333d7ae7fff94b5c68e4fa8095519"}, + {file = "websockets-10.1-cp38-cp38-win_amd64.whl", hash = "sha256:7d6673b2753f9c5377868a53445d0c321ef41ff3c8e3b6d57868e72054bfce5f"}, + {file = "websockets-10.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:ddab2dc69ee5ae27c74dbfe9d7bb6fee260826c136dca257faa1a41d1db61a89"}, + {file = "websockets-10.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:14e9cf68a08d1a5d42109549201aefba473b1d925d233ae19035c876dd845da9"}, + {file = "websockets-10.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e4819c6fb4f336fd5388372cb556b1f3a165f3f68e66913d1a2fc1de55dc6f58"}, + {file = "websockets-10.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:05e7f098c76b0a4743716590bb8f9706de19f1ef5148d61d0cf76495ec3edb9c"}, + {file = "websockets-10.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:5bb6256de5a4fb1d42b3747b4e2268706c92965d75d0425be97186615bf2f24f"}, + {file = "websockets-10.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:888a5fa2a677e0c2b944f9826c756475980f1b276b6302e606f5c4ff5635be9e"}, + {file = "websockets-10.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:6fdec1a0b3e5630c58e3d8704d2011c678929fce90b40908c97dfc47de8dca72"}, + {file = "websockets-10.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:531d8eb013a9bc6b3ad101588182aa9b6dd994b190c56df07f0d84a02b85d530"}, + {file = "websockets-10.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0d93b7cadc761347d98da12ec1930b5c71b2096f1ceed213973e3cda23fead9c"}, + {file = "websockets-10.1-cp39-cp39-win32.whl", hash = "sha256:d9b245db5a7e64c95816e27d72830e51411c4609c05673d1ae81eb5d23b0be54"}, + {file = "websockets-10.1-cp39-cp39-win_amd64.whl", hash = "sha256:882c0b8bdff3bf1bd7f024ce17c6b8006042ec4cceba95cf15df57e57efa471c"}, + {file = "websockets-10.1-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:10edd9d7d3581cfb9ff544ac09fc98cab7ee8f26778a5a8b2d5fd4b0684c5ba5"}, + {file = "websockets-10.1-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:baa83174390c0ff4fc1304fbe24393843ac7a08fdd59295759c4b439e06b1536"}, + {file = "websockets-10.1-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:483edee5abed738a0b6a908025be47f33634c2ad8e737edd03ffa895bd600909"}, + {file = "websockets-10.1-pp37-pypy37_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:816ae7dac2c6522cfa620947ead0ca95ac654916eebf515c94d7c28de5601a6e"}, + {file = "websockets-10.1-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:1dafe98698ece09b8ccba81b910643ff37198e43521d977be76caf37709cf62b"}, + {file = "websockets-10.1.tar.gz", hash = "sha256:181d2b25de5a437b36aefedaf006ecb6fa3aa1328ec0236cdde15f32f9d3ff6d"}, ] werkzeug = [ - {file = "Werkzeug-2.2.2-py3-none-any.whl", hash = "sha256:f979ab81f58d7318e064e99c4506445d60135ac5cd2e177a2de0089bfd4c9bd5"}, - {file = "Werkzeug-2.2.2.tar.gz", hash = "sha256:7ea2d48322cc7c0f8b3a215ed73eabd7b5d75d0b50e31ab006286ccff9e00b8f"}, -] -wheel = [ - {file = "wheel-0.38.4-py3-none-any.whl", hash = "sha256:b60533f3f5d530e971d6737ca6d58681ee434818fab630c83a734bb10c083ce8"}, - {file = "wheel-0.38.4.tar.gz", hash = "sha256:965f5259b566725405b05e7cf774052044b1ed30119b5d586b2703aafe8719ac"}, + {file = "Werkzeug-1.0.1-py2.py3-none-any.whl", hash = "sha256:2de2a5db0baeae7b2d2664949077c2ac63fbd16d98da0ff71837f7d1dea3fd43"}, + {file = "Werkzeug-1.0.1.tar.gz", hash = "sha256:6c80b1e5ad3665290ea39320b91e1be1e0d5f60652b964a3070216de83d2e47c"}, ] wrapt = [ - {file = "wrapt-1.14.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:1b376b3f4896e7930f1f772ac4b064ac12598d1c38d04907e696cc4d794b43d3"}, - {file = "wrapt-1.14.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:903500616422a40a98a5a3c4ff4ed9d0066f3b4c951fa286018ecdf0750194ef"}, - {file = "wrapt-1.14.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:5a9a0d155deafd9448baff28c08e150d9b24ff010e899311ddd63c45c2445e28"}, - {file = "wrapt-1.14.1-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:ddaea91abf8b0d13443f6dac52e89051a5063c7d014710dcb4d4abb2ff811a59"}, - {file = "wrapt-1.14.1-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:36f582d0c6bc99d5f39cd3ac2a9062e57f3cf606ade29a0a0d6b323462f4dd87"}, - {file = "wrapt-1.14.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:7ef58fb89674095bfc57c4069e95d7a31cfdc0939e2a579882ac7d55aadfd2a1"}, - {file = "wrapt-1.14.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:e2f83e18fe2f4c9e7db597e988f72712c0c3676d337d8b101f6758107c42425b"}, - {file = "wrapt-1.14.1-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:ee2b1b1769f6707a8a445162ea16dddf74285c3964f605877a20e38545c3c462"}, - {file = "wrapt-1.14.1-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:833b58d5d0b7e5b9832869f039203389ac7cbf01765639c7309fd50ef619e0b1"}, - {file = "wrapt-1.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:80bb5c256f1415f747011dc3604b59bc1f91c6e7150bd7db03b19170ee06b320"}, - {file = "wrapt-1.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:07f7a7d0f388028b2df1d916e94bbb40624c59b48ecc6cbc232546706fac74c2"}, - {file = "wrapt-1.14.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:02b41b633c6261feff8ddd8d11c711df6842aba629fdd3da10249a53211a72c4"}, - {file = "wrapt-1.14.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2fe803deacd09a233e4762a1adcea5db5d31e6be577a43352936179d14d90069"}, - {file = "wrapt-1.14.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:257fd78c513e0fb5cdbe058c27a0624c9884e735bbd131935fd49e9fe719d310"}, - {file = "wrapt-1.14.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4fcc4649dc762cddacd193e6b55bc02edca674067f5f98166d7713b193932b7f"}, - {file = "wrapt-1.14.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:11871514607b15cfeb87c547a49bca19fde402f32e2b1c24a632506c0a756656"}, - {file = "wrapt-1.14.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8ad85f7f4e20964db4daadcab70b47ab05c7c1cf2a7c1e51087bfaa83831854c"}, - {file = "wrapt-1.14.1-cp310-cp310-win32.whl", hash = "sha256:a9a52172be0b5aae932bef82a79ec0a0ce87288c7d132946d645eba03f0ad8a8"}, - {file = "wrapt-1.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:6d323e1554b3d22cfc03cd3243b5bb815a51f5249fdcbb86fda4bf62bab9e164"}, - {file = "wrapt-1.14.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:43ca3bbbe97af00f49efb06e352eae40434ca9d915906f77def219b88e85d907"}, - {file = "wrapt-1.14.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:6b1a564e6cb69922c7fe3a678b9f9a3c54e72b469875aa8018f18b4d1dd1adf3"}, - {file = "wrapt-1.14.1-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:00b6d4ea20a906c0ca56d84f93065b398ab74b927a7a3dbd470f6fc503f95dc3"}, - {file = "wrapt-1.14.1-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:a85d2b46be66a71bedde836d9e41859879cc54a2a04fad1191eb50c2066f6e9d"}, - {file = "wrapt-1.14.1-cp35-cp35m-win32.whl", hash = "sha256:dbcda74c67263139358f4d188ae5faae95c30929281bc6866d00573783c422b7"}, - {file = "wrapt-1.14.1-cp35-cp35m-win_amd64.whl", hash = "sha256:b21bb4c09ffabfa0e85e3a6b623e19b80e7acd709b9f91452b8297ace2a8ab00"}, - {file = "wrapt-1.14.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:9e0fd32e0148dd5dea6af5fee42beb949098564cc23211a88d799e434255a1f4"}, - {file = "wrapt-1.14.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9736af4641846491aedb3c3f56b9bc5568d92b0692303b5a305301a95dfd38b1"}, - {file = "wrapt-1.14.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5b02d65b9ccf0ef6c34cba6cf5bf2aab1bb2f49c6090bafeecc9cd81ad4ea1c1"}, - {file = "wrapt-1.14.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21ac0156c4b089b330b7666db40feee30a5d52634cc4560e1905d6529a3897ff"}, - {file = "wrapt-1.14.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:9f3e6f9e05148ff90002b884fbc2a86bd303ae847e472f44ecc06c2cd2fcdb2d"}, - {file = "wrapt-1.14.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:6e743de5e9c3d1b7185870f480587b75b1cb604832e380d64f9504a0535912d1"}, - {file = "wrapt-1.14.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:d79d7d5dc8a32b7093e81e97dad755127ff77bcc899e845f41bf71747af0c569"}, - {file = "wrapt-1.14.1-cp36-cp36m-win32.whl", hash = "sha256:81b19725065dcb43df02b37e03278c011a09e49757287dca60c5aecdd5a0b8ed"}, - {file = "wrapt-1.14.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b014c23646a467558be7da3d6b9fa409b2c567d2110599b7cf9a0c5992b3b471"}, - {file = "wrapt-1.14.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:88bd7b6bd70a5b6803c1abf6bca012f7ed963e58c68d76ee20b9d751c74a3248"}, - {file = "wrapt-1.14.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5901a312f4d14c59918c221323068fad0540e34324925c8475263841dbdfe68"}, - {file = "wrapt-1.14.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d77c85fedff92cf788face9bfa3ebaa364448ebb1d765302e9af11bf449ca36d"}, - {file = "wrapt-1.14.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d649d616e5c6a678b26d15ece345354f7c2286acd6db868e65fcc5ff7c24a77"}, - {file = "wrapt-1.14.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7d2872609603cb35ca513d7404a94d6d608fc13211563571117046c9d2bcc3d7"}, - {file = "wrapt-1.14.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:ee6acae74a2b91865910eef5e7de37dc6895ad96fa23603d1d27ea69df545015"}, - {file = "wrapt-1.14.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2b39d38039a1fdad98c87279b48bc5dce2c0ca0d73483b12cb72aa9609278e8a"}, - {file = "wrapt-1.14.1-cp37-cp37m-win32.whl", hash = "sha256:60db23fa423575eeb65ea430cee741acb7c26a1365d103f7b0f6ec412b893853"}, - {file = "wrapt-1.14.1-cp37-cp37m-win_amd64.whl", hash = "sha256:709fe01086a55cf79d20f741f39325018f4df051ef39fe921b1ebe780a66184c"}, - {file = "wrapt-1.14.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8c0ce1e99116d5ab21355d8ebe53d9460366704ea38ae4d9f6933188f327b456"}, - {file = "wrapt-1.14.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e3fb1677c720409d5f671e39bac6c9e0e422584e5f518bfd50aa4cbbea02433f"}, - {file = "wrapt-1.14.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:642c2e7a804fcf18c222e1060df25fc210b9c58db7c91416fb055897fc27e8cc"}, - {file = "wrapt-1.14.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b7c050ae976e286906dd3f26009e117eb000fb2cf3533398c5ad9ccc86867b1"}, - {file = "wrapt-1.14.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef3f72c9666bba2bab70d2a8b79f2c6d2c1a42a7f7e2b0ec83bb2f9e383950af"}, - {file = "wrapt-1.14.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:01c205616a89d09827986bc4e859bcabd64f5a0662a7fe95e0d359424e0e071b"}, - {file = "wrapt-1.14.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:5a0f54ce2c092aaf439813735584b9537cad479575a09892b8352fea5e988dc0"}, - {file = "wrapt-1.14.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2cf71233a0ed05ccdabe209c606fe0bac7379fdcf687f39b944420d2a09fdb57"}, - {file = "wrapt-1.14.1-cp38-cp38-win32.whl", hash = "sha256:aa31fdcc33fef9eb2552cbcbfee7773d5a6792c137b359e82879c101e98584c5"}, - {file = "wrapt-1.14.1-cp38-cp38-win_amd64.whl", hash = "sha256:d1967f46ea8f2db647c786e78d8cc7e4313dbd1b0aca360592d8027b8508e24d"}, - {file = "wrapt-1.14.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3232822c7d98d23895ccc443bbdf57c7412c5a65996c30442ebe6ed3df335383"}, - {file = "wrapt-1.14.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:988635d122aaf2bdcef9e795435662bcd65b02f4f4c1ae37fbee7401c440b3a7"}, - {file = "wrapt-1.14.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cca3c2cdadb362116235fdbd411735de4328c61425b0aa9f872fd76d02c4e86"}, - {file = "wrapt-1.14.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d52a25136894c63de15a35bc0bdc5adb4b0e173b9c0d07a2be9d3ca64a332735"}, - {file = "wrapt-1.14.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40e7bc81c9e2b2734ea4bc1aceb8a8f0ceaac7c5299bc5d69e37c44d9081d43b"}, - {file = "wrapt-1.14.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b9b7a708dd92306328117d8c4b62e2194d00c365f18eff11a9b53c6f923b01e3"}, - {file = "wrapt-1.14.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:6a9a25751acb379b466ff6be78a315e2b439d4c94c1e99cb7266d40a537995d3"}, - {file = "wrapt-1.14.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:34aa51c45f28ba7f12accd624225e2b1e5a3a45206aa191f6f9aac931d9d56fe"}, - {file = "wrapt-1.14.1-cp39-cp39-win32.whl", hash = "sha256:dee0ce50c6a2dd9056c20db781e9c1cfd33e77d2d569f5d1d9321c641bb903d5"}, - {file = "wrapt-1.14.1-cp39-cp39-win_amd64.whl", hash = "sha256:dee60e1de1898bde3b238f18340eec6148986da0455d8ba7848d50470a7a32fb"}, - {file = "wrapt-1.14.1.tar.gz", hash = "sha256:380a85cf89e0e69b7cfbe2ea9f765f004ff419f34194018a6827ac0e3edfed4d"}, -] -xmltodict = [ - {file = "xmltodict-0.13.0-py2.py3-none-any.whl", hash = "sha256:aa89e8fd76320154a40d19a0df04a4695fb9dc5ba977cbb68ab3e4eb225e7852"}, - {file = "xmltodict-0.13.0.tar.gz", hash = "sha256:341595a488e3e01a85a9d8911d8912fd922ede5fecc4dce437eb4b6c8d037e56"}, + {file = "wrapt-1.13.3-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:e05e60ff3b2b0342153be4d1b597bbcfd8330890056b9619f4ad6b8d5c96a81a"}, + {file = "wrapt-1.13.3-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:85148f4225287b6a0665eef08a178c15097366d46b210574a658c1ff5b377489"}, + {file = "wrapt-1.13.3-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:2dded5496e8f1592ec27079b28b6ad2a1ef0b9296d270f77b8e4a3a796cf6909"}, + {file = "wrapt-1.13.3-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:e94b7d9deaa4cc7bac9198a58a7240aaf87fe56c6277ee25fa5b3aa1edebd229"}, + {file = "wrapt-1.13.3-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:498e6217523111d07cd67e87a791f5e9ee769f9241fcf8a379696e25806965af"}, + {file = "wrapt-1.13.3-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:ec7e20258ecc5174029a0f391e1b948bf2906cd64c198a9b8b281b811cbc04de"}, + {file = "wrapt-1.13.3-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:87883690cae293541e08ba2da22cacaae0a092e0ed56bbba8d018cc486fbafbb"}, + {file = "wrapt-1.13.3-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:f99c0489258086308aad4ae57da9e8ecf9e1f3f30fa35d5e170b4d4896554d80"}, + {file = "wrapt-1.13.3-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:6a03d9917aee887690aa3f1747ce634e610f6db6f6b332b35c2dd89412912bca"}, + {file = "wrapt-1.13.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:936503cb0a6ed28dbfa87e8fcd0a56458822144e9d11a49ccee6d9a8adb2ac44"}, + {file = "wrapt-1.13.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:f9c51d9af9abb899bd34ace878fbec8bf357b3194a10c4e8e0a25512826ef056"}, + {file = "wrapt-1.13.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:220a869982ea9023e163ba915077816ca439489de6d2c09089b219f4e11b6785"}, + {file = "wrapt-1.13.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:0877fe981fd76b183711d767500e6b3111378ed2043c145e21816ee589d91096"}, + {file = "wrapt-1.13.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:43e69ffe47e3609a6aec0fe723001c60c65305784d964f5007d5b4fb1bc6bf33"}, + {file = "wrapt-1.13.3-cp310-cp310-win32.whl", hash = "sha256:78dea98c81915bbf510eb6a3c9c24915e4660302937b9ae05a0947164248020f"}, + {file = "wrapt-1.13.3-cp310-cp310-win_amd64.whl", hash = "sha256:ea3e746e29d4000cd98d572f3ee2a6050a4f784bb536f4ac1f035987fc1ed83e"}, + {file = "wrapt-1.13.3-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:8c73c1a2ec7c98d7eaded149f6d225a692caa1bd7b2401a14125446e9e90410d"}, + {file = "wrapt-1.13.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:086218a72ec7d986a3eddb7707c8c4526d677c7b35e355875a0fe2918b059179"}, + {file = "wrapt-1.13.3-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:e92d0d4fa68ea0c02d39f1e2f9cb5bc4b4a71e8c442207433d8db47ee79d7aa3"}, + {file = "wrapt-1.13.3-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:d4a5f6146cfa5c7ba0134249665acd322a70d1ea61732723c7d3e8cc0fa80755"}, + {file = "wrapt-1.13.3-cp35-cp35m-win32.whl", hash = "sha256:8aab36778fa9bba1a8f06a4919556f9f8c7b33102bd71b3ab307bb3fecb21851"}, + {file = "wrapt-1.13.3-cp35-cp35m-win_amd64.whl", hash = "sha256:944b180f61f5e36c0634d3202ba8509b986b5fbaf57db3e94df11abee244ba13"}, + {file = "wrapt-1.13.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:2ebdde19cd3c8cdf8df3fc165bc7827334bc4e353465048b36f7deeae8ee0918"}, + {file = "wrapt-1.13.3-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:610f5f83dd1e0ad40254c306f4764fcdc846641f120c3cf424ff57a19d5f7ade"}, + {file = "wrapt-1.13.3-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:5601f44a0f38fed36cc07db004f0eedeaadbdcec90e4e90509480e7e6060a5bc"}, + {file = "wrapt-1.13.3-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:e6906d6f48437dfd80464f7d7af1740eadc572b9f7a4301e7dd3d65db285cacf"}, + {file = "wrapt-1.13.3-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:766b32c762e07e26f50d8a3468e3b4228b3736c805018e4b0ec8cc01ecd88125"}, + {file = "wrapt-1.13.3-cp36-cp36m-win32.whl", hash = "sha256:5f223101f21cfd41deec8ce3889dc59f88a59b409db028c469c9b20cfeefbe36"}, + {file = "wrapt-1.13.3-cp36-cp36m-win_amd64.whl", hash = "sha256:f122ccd12fdc69628786d0c947bdd9cb2733be8f800d88b5a37c57f1f1d73c10"}, + {file = "wrapt-1.13.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:46f7f3af321a573fc0c3586612db4decb7eb37172af1bc6173d81f5b66c2e068"}, + {file = "wrapt-1.13.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:778fd096ee96890c10ce96187c76b3e99b2da44e08c9e24d5652f356873f6709"}, + {file = "wrapt-1.13.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:0cb23d36ed03bf46b894cfec777eec754146d68429c30431c99ef28482b5c1df"}, + {file = "wrapt-1.13.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:96b81ae75591a795d8c90edc0bfaab44d3d41ffc1aae4d994c5aa21d9b8e19a2"}, + {file = "wrapt-1.13.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:7dd215e4e8514004c8d810a73e342c536547038fb130205ec4bba9f5de35d45b"}, + {file = "wrapt-1.13.3-cp37-cp37m-win32.whl", hash = "sha256:47f0a183743e7f71f29e4e21574ad3fa95676136f45b91afcf83f6a050914829"}, + {file = "wrapt-1.13.3-cp37-cp37m-win_amd64.whl", hash = "sha256:fd76c47f20984b43d93de9a82011bb6e5f8325df6c9ed4d8310029a55fa361ea"}, + {file = "wrapt-1.13.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b73d4b78807bd299b38e4598b8e7bd34ed55d480160d2e7fdaabd9931afa65f9"}, + {file = "wrapt-1.13.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:ec9465dd69d5657b5d2fa6133b3e1e989ae27d29471a672416fd729b429eb554"}, + {file = "wrapt-1.13.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:dd91006848eb55af2159375134d724032a2d1d13bcc6f81cd8d3ed9f2b8e846c"}, + {file = "wrapt-1.13.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:ae9de71eb60940e58207f8e71fe113c639da42adb02fb2bcbcaccc1ccecd092b"}, + {file = "wrapt-1.13.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:51799ca950cfee9396a87f4a1240622ac38973b6df5ef7a41e7f0b98797099ce"}, + {file = "wrapt-1.13.3-cp38-cp38-win32.whl", hash = "sha256:4b9c458732450ec42578b5642ac53e312092acf8c0bfce140ada5ca1ac556f79"}, + {file = "wrapt-1.13.3-cp38-cp38-win_amd64.whl", hash = "sha256:7dde79d007cd6dfa65afe404766057c2409316135cb892be4b1c768e3f3a11cb"}, + {file = "wrapt-1.13.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:981da26722bebb9247a0601e2922cedf8bb7a600e89c852d063313102de6f2cb"}, + {file = "wrapt-1.13.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:705e2af1f7be4707e49ced9153f8d72131090e52be9278b5dbb1498c749a1e32"}, + {file = "wrapt-1.13.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:25b1b1d5df495d82be1c9d2fad408f7ce5ca8a38085e2da41bb63c914baadff7"}, + {file = "wrapt-1.13.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:77416e6b17926d953b5c666a3cb718d5945df63ecf922af0ee576206d7033b5e"}, + {file = "wrapt-1.13.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:865c0b50003616f05858b22174c40ffc27a38e67359fa1495605f96125f76640"}, + {file = "wrapt-1.13.3-cp39-cp39-win32.whl", hash = "sha256:0a017a667d1f7411816e4bf214646d0ad5b1da2c1ea13dec6c162736ff25a374"}, + {file = "wrapt-1.13.3-cp39-cp39-win_amd64.whl", hash = "sha256:81bd7c90d28a4b2e1df135bfbd7c23aee3050078ca6441bead44c42483f9ebfb"}, + {file = "wrapt-1.13.3.tar.gz", hash = "sha256:1fea9cd438686e6682271d36f3481a9f3636195578bab9ca3382e2f5f01fc185"}, ] yarl = [ - {file = "yarl-1.8.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:bb81f753c815f6b8e2ddd2eef3c855cf7da193b82396ac013c661aaa6cc6b0a5"}, - {file = "yarl-1.8.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:47d49ac96156f0928f002e2424299b2c91d9db73e08c4cd6742923a086f1c863"}, - {file = "yarl-1.8.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3fc056e35fa6fba63248d93ff6e672c096f95f7836938241ebc8260e062832fe"}, - {file = "yarl-1.8.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:58a3c13d1c3005dbbac5c9f0d3210b60220a65a999b1833aa46bd6677c69b08e"}, - {file = "yarl-1.8.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:10b08293cda921157f1e7c2790999d903b3fd28cd5c208cf8826b3b508026996"}, - {file = "yarl-1.8.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:de986979bbd87272fe557e0a8fcb66fd40ae2ddfe28a8b1ce4eae22681728fef"}, - {file = "yarl-1.8.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c4fcfa71e2c6a3cb568cf81aadc12768b9995323186a10827beccf5fa23d4f8"}, - {file = "yarl-1.8.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae4d7ff1049f36accde9e1ef7301912a751e5bae0a9d142459646114c70ecba6"}, - {file = "yarl-1.8.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:bf071f797aec5b96abfc735ab97da9fd8f8768b43ce2abd85356a3127909d146"}, - {file = "yarl-1.8.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:74dece2bfc60f0f70907c34b857ee98f2c6dd0f75185db133770cd67300d505f"}, - {file = "yarl-1.8.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:df60a94d332158b444301c7f569659c926168e4d4aad2cfbf4bce0e8fb8be826"}, - {file = "yarl-1.8.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:63243b21c6e28ec2375f932a10ce7eda65139b5b854c0f6b82ed945ba526bff3"}, - {file = "yarl-1.8.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:cfa2bbca929aa742b5084fd4663dd4b87c191c844326fcb21c3afd2d11497f80"}, - {file = "yarl-1.8.2-cp310-cp310-win32.whl", hash = "sha256:b05df9ea7496df11b710081bd90ecc3a3db6adb4fee36f6a411e7bc91a18aa42"}, - {file = "yarl-1.8.2-cp310-cp310-win_amd64.whl", hash = "sha256:24ad1d10c9db1953291f56b5fe76203977f1ed05f82d09ec97acb623a7976574"}, - {file = "yarl-1.8.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2a1fca9588f360036242f379bfea2b8b44cae2721859b1c56d033adfd5893634"}, - {file = "yarl-1.8.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f37db05c6051eff17bc832914fe46869f8849de5b92dc4a3466cd63095d23dfd"}, - {file = "yarl-1.8.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:77e913b846a6b9c5f767b14dc1e759e5aff05502fe73079f6f4176359d832581"}, - {file = "yarl-1.8.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0978f29222e649c351b173da2b9b4665ad1feb8d1daa9d971eb90df08702668a"}, - {file = "yarl-1.8.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:388a45dc77198b2460eac0aca1efd6a7c09e976ee768b0d5109173e521a19daf"}, - {file = "yarl-1.8.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2305517e332a862ef75be8fad3606ea10108662bc6fe08509d5ca99503ac2aee"}, - {file = "yarl-1.8.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42430ff511571940d51e75cf42f1e4dbdded477e71c1b7a17f4da76c1da8ea76"}, - {file = "yarl-1.8.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3150078118f62371375e1e69b13b48288e44f6691c1069340081c3fd12c94d5b"}, - {file = "yarl-1.8.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c15163b6125db87c8f53c98baa5e785782078fbd2dbeaa04c6141935eb6dab7a"}, - {file = "yarl-1.8.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:4d04acba75c72e6eb90745447d69f84e6c9056390f7a9724605ca9c56b4afcc6"}, - {file = "yarl-1.8.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:e7fd20d6576c10306dea2d6a5765f46f0ac5d6f53436217913e952d19237efc4"}, - {file = "yarl-1.8.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:75c16b2a900b3536dfc7014905a128a2bea8fb01f9ee26d2d7d8db0a08e7cb2c"}, - {file = "yarl-1.8.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:6d88056a04860a98341a0cf53e950e3ac9f4e51d1b6f61a53b0609df342cc8b2"}, - {file = "yarl-1.8.2-cp311-cp311-win32.whl", hash = "sha256:fb742dcdd5eec9f26b61224c23baea46c9055cf16f62475e11b9b15dfd5c117b"}, - {file = "yarl-1.8.2-cp311-cp311-win_amd64.whl", hash = "sha256:8c46d3d89902c393a1d1e243ac847e0442d0196bbd81aecc94fcebbc2fd5857c"}, - {file = "yarl-1.8.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:ceff9722e0df2e0a9e8a79c610842004fa54e5b309fe6d218e47cd52f791d7ef"}, - {file = "yarl-1.8.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f6b4aca43b602ba0f1459de647af954769919c4714706be36af670a5f44c9c1"}, - {file = "yarl-1.8.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1684a9bd9077e922300ecd48003ddae7a7474e0412bea38d4631443a91d61077"}, - {file = "yarl-1.8.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ebb78745273e51b9832ef90c0898501006670d6e059f2cdb0e999494eb1450c2"}, - {file = "yarl-1.8.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3adeef150d528ded2a8e734ebf9ae2e658f4c49bf413f5f157a470e17a4a2e89"}, - {file = "yarl-1.8.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57a7c87927a468e5a1dc60c17caf9597161d66457a34273ab1760219953f7f4c"}, - {file = "yarl-1.8.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:efff27bd8cbe1f9bd127e7894942ccc20c857aa8b5a0327874f30201e5ce83d0"}, - {file = "yarl-1.8.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:a783cd344113cb88c5ff7ca32f1f16532a6f2142185147822187913eb989f739"}, - {file = "yarl-1.8.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:705227dccbe96ab02c7cb2c43e1228e2826e7ead880bb19ec94ef279e9555b5b"}, - {file = "yarl-1.8.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:34c09b43bd538bf6c4b891ecce94b6fa4f1f10663a8d4ca589a079a5018f6ed7"}, - {file = "yarl-1.8.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:a48f4f7fea9a51098b02209d90297ac324241bf37ff6be6d2b0149ab2bd51b37"}, - {file = "yarl-1.8.2-cp37-cp37m-win32.whl", hash = "sha256:0414fd91ce0b763d4eadb4456795b307a71524dbacd015c657bb2a39db2eab89"}, - {file = "yarl-1.8.2-cp37-cp37m-win_amd64.whl", hash = "sha256:d881d152ae0007809c2c02e22aa534e702f12071e6b285e90945aa3c376463c5"}, - {file = "yarl-1.8.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5df5e3d04101c1e5c3b1d69710b0574171cc02fddc4b23d1b2813e75f35a30b1"}, - {file = "yarl-1.8.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7a66c506ec67eb3159eea5096acd05f5e788ceec7b96087d30c7d2865a243918"}, - {file = "yarl-1.8.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2b4fa2606adf392051d990c3b3877d768771adc3faf2e117b9de7eb977741229"}, - {file = "yarl-1.8.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e21fb44e1eff06dd6ef971d4bdc611807d6bd3691223d9c01a18cec3677939e"}, - {file = "yarl-1.8.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:93202666046d9edadfe9f2e7bf5e0782ea0d497b6d63da322e541665d65a044e"}, - {file = "yarl-1.8.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fc77086ce244453e074e445104f0ecb27530d6fd3a46698e33f6c38951d5a0f1"}, - {file = "yarl-1.8.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dd68a92cab699a233641f5929a40f02a4ede8c009068ca8aa1fe87b8c20ae3"}, - {file = "yarl-1.8.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1b372aad2b5f81db66ee7ec085cbad72c4da660d994e8e590c997e9b01e44901"}, - {file = "yarl-1.8.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:e6f3515aafe0209dd17fb9bdd3b4e892963370b3de781f53e1746a521fb39fc0"}, - {file = "yarl-1.8.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:dfef7350ee369197106805e193d420b75467b6cceac646ea5ed3049fcc950a05"}, - {file = "yarl-1.8.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:728be34f70a190566d20aa13dc1f01dc44b6aa74580e10a3fb159691bc76909d"}, - {file = "yarl-1.8.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:ff205b58dc2929191f68162633d5e10e8044398d7a45265f90a0f1d51f85f72c"}, - {file = "yarl-1.8.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:baf211dcad448a87a0d9047dc8282d7de59473ade7d7fdf22150b1d23859f946"}, - {file = "yarl-1.8.2-cp38-cp38-win32.whl", hash = "sha256:272b4f1599f1b621bf2aabe4e5b54f39a933971f4e7c9aa311d6d7dc06965165"}, - {file = "yarl-1.8.2-cp38-cp38-win_amd64.whl", hash = "sha256:326dd1d3caf910cd26a26ccbfb84c03b608ba32499b5d6eeb09252c920bcbe4f"}, - {file = "yarl-1.8.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:f8ca8ad414c85bbc50f49c0a106f951613dfa5f948ab69c10ce9b128d368baf8"}, - {file = "yarl-1.8.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:418857f837347e8aaef682679f41e36c24250097f9e2f315d39bae3a99a34cbf"}, - {file = "yarl-1.8.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ae0eec05ab49e91a78700761777f284c2df119376e391db42c38ab46fd662b77"}, - {file = "yarl-1.8.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:009a028127e0a1755c38b03244c0bea9d5565630db9c4cf9572496e947137a87"}, - {file = "yarl-1.8.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3edac5d74bb3209c418805bda77f973117836e1de7c000e9755e572c1f7850d0"}, - {file = "yarl-1.8.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:da65c3f263729e47351261351b8679c6429151ef9649bba08ef2528ff2c423b2"}, - {file = "yarl-1.8.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ef8fb25e52663a1c85d608f6dd72e19bd390e2ecaf29c17fb08f730226e3a08"}, - {file = "yarl-1.8.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bcd7bb1e5c45274af9a1dd7494d3c52b2be5e6bd8d7e49c612705fd45420b12d"}, - {file = "yarl-1.8.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:44ceac0450e648de86da8e42674f9b7077d763ea80c8ceb9d1c3e41f0f0a9951"}, - {file = "yarl-1.8.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:97209cc91189b48e7cfe777237c04af8e7cc51eb369004e061809bcdf4e55220"}, - {file = "yarl-1.8.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:48dd18adcf98ea9cd721a25313aef49d70d413a999d7d89df44f469edfb38a06"}, - {file = "yarl-1.8.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:e59399dda559688461762800d7fb34d9e8a6a7444fd76ec33220a926c8be1516"}, - {file = "yarl-1.8.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d617c241c8c3ad5c4e78a08429fa49e4b04bedfc507b34b4d8dceb83b4af3588"}, - {file = "yarl-1.8.2-cp39-cp39-win32.whl", hash = "sha256:cb6d48d80a41f68de41212f3dfd1a9d9898d7841c8f7ce6696cf2fd9cb57ef83"}, - {file = "yarl-1.8.2-cp39-cp39-win_amd64.whl", hash = "sha256:6604711362f2dbf7160df21c416f81fac0de6dbcf0b5445a2ef25478ecc4c778"}, - {file = "yarl-1.8.2.tar.gz", hash = "sha256:49d43402c6e3013ad0978602bf6bf5328535c48d192304b91b97a3c6790b1562"}, + {file = "yarl-1.7.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f2a8508f7350512434e41065684076f640ecce176d262a7d54f0da41d99c5a95"}, + {file = "yarl-1.7.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:da6df107b9ccfe52d3a48165e48d72db0eca3e3029b5b8cb4fe6ee3cb870ba8b"}, + {file = "yarl-1.7.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a1d0894f238763717bdcfea74558c94e3bc34aeacd3351d769460c1a586a8b05"}, + {file = "yarl-1.7.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dfe4b95b7e00c6635a72e2d00b478e8a28bfb122dc76349a06e20792eb53a523"}, + {file = "yarl-1.7.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c145ab54702334c42237a6c6c4cc08703b6aa9b94e2f227ceb3d477d20c36c63"}, + {file = "yarl-1.7.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1ca56f002eaf7998b5fcf73b2421790da9d2586331805f38acd9997743114e98"}, + {file = "yarl-1.7.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:1d3d5ad8ea96bd6d643d80c7b8d5977b4e2fb1bab6c9da7322616fd26203d125"}, + {file = "yarl-1.7.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:167ab7f64e409e9bdd99333fe8c67b5574a1f0495dcfd905bc7454e766729b9e"}, + {file = "yarl-1.7.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:95a1873b6c0dd1c437fb3bb4a4aaa699a48c218ac7ca1e74b0bee0ab16c7d60d"}, + {file = "yarl-1.7.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6152224d0a1eb254f97df3997d79dadd8bb2c1a02ef283dbb34b97d4f8492d23"}, + {file = "yarl-1.7.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:5bb7d54b8f61ba6eee541fba4b83d22b8a046b4ef4d8eb7f15a7e35db2e1e245"}, + {file = "yarl-1.7.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:9c1f083e7e71b2dd01f7cd7434a5f88c15213194df38bc29b388ccdf1492b739"}, + {file = "yarl-1.7.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f44477ae29025d8ea87ec308539f95963ffdc31a82f42ca9deecf2d505242e72"}, + {file = "yarl-1.7.2-cp310-cp310-win32.whl", hash = "sha256:cff3ba513db55cc6a35076f32c4cdc27032bd075c9faef31fec749e64b45d26c"}, + {file = "yarl-1.7.2-cp310-cp310-win_amd64.whl", hash = "sha256:c9c6d927e098c2d360695f2e9d38870b2e92e0919be07dbe339aefa32a090265"}, + {file = "yarl-1.7.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:9b4c77d92d56a4c5027572752aa35082e40c561eec776048330d2907aead891d"}, + {file = "yarl-1.7.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c01a89a44bb672c38f42b49cdb0ad667b116d731b3f4c896f72302ff77d71656"}, + {file = "yarl-1.7.2-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c19324a1c5399b602f3b6e7db9478e5b1adf5cf58901996fc973fe4fccd73eed"}, + {file = "yarl-1.7.2-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3abddf0b8e41445426d29f955b24aeecc83fa1072be1be4e0d194134a7d9baee"}, + {file = "yarl-1.7.2-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6a1a9fe17621af43e9b9fcea8bd088ba682c8192d744b386ee3c47b56eaabb2c"}, + {file = "yarl-1.7.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8b0915ee85150963a9504c10de4e4729ae700af11df0dc5550e6587ed7891e92"}, + {file = "yarl-1.7.2-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:29e0656d5497733dcddc21797da5a2ab990c0cb9719f1f969e58a4abac66234d"}, + {file = "yarl-1.7.2-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:bf19725fec28452474d9887a128e98dd67eee7b7d52e932e6949c532d820dc3b"}, + {file = "yarl-1.7.2-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:d6f3d62e16c10e88d2168ba2d065aa374e3c538998ed04996cd373ff2036d64c"}, + {file = "yarl-1.7.2-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:ac10bbac36cd89eac19f4e51c032ba6b412b3892b685076f4acd2de18ca990aa"}, + {file = "yarl-1.7.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:aa32aaa97d8b2ed4e54dc65d241a0da1c627454950f7d7b1f95b13985afd6c5d"}, + {file = "yarl-1.7.2-cp36-cp36m-win32.whl", hash = "sha256:87f6e082bce21464857ba58b569370e7b547d239ca22248be68ea5d6b51464a1"}, + {file = "yarl-1.7.2-cp36-cp36m-win_amd64.whl", hash = "sha256:ac35ccde589ab6a1870a484ed136d49a26bcd06b6a1c6397b1967ca13ceb3913"}, + {file = "yarl-1.7.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a467a431a0817a292121c13cbe637348b546e6ef47ca14a790aa2fa8cc93df63"}, + {file = "yarl-1.7.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ab0c3274d0a846840bf6c27d2c60ba771a12e4d7586bf550eefc2df0b56b3b4"}, + {file = "yarl-1.7.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d260d4dc495c05d6600264a197d9d6f7fc9347f21d2594926202fd08cf89a8ba"}, + {file = "yarl-1.7.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fc4dd8b01a8112809e6b636b00f487846956402834a7fd59d46d4f4267181c41"}, + {file = "yarl-1.7.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c1164a2eac148d85bbdd23e07dfcc930f2e633220f3eb3c3e2a25f6148c2819e"}, + {file = "yarl-1.7.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:67e94028817defe5e705079b10a8438b8cb56e7115fa01640e9c0bb3edf67332"}, + {file = "yarl-1.7.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:89ccbf58e6a0ab89d487c92a490cb5660d06c3a47ca08872859672f9c511fc52"}, + {file = "yarl-1.7.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:8cce6f9fa3df25f55521fbb5c7e4a736683148bcc0c75b21863789e5185f9185"}, + {file = "yarl-1.7.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:211fcd65c58bf250fb994b53bc45a442ddc9f441f6fec53e65de8cba48ded986"}, + {file = "yarl-1.7.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c10ea1e80a697cf7d80d1ed414b5cb8f1eec07d618f54637067ae3c0334133c4"}, + {file = "yarl-1.7.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:52690eb521d690ab041c3919666bea13ab9fbff80d615ec16fa81a297131276b"}, + {file = "yarl-1.7.2-cp37-cp37m-win32.whl", hash = "sha256:695ba021a9e04418507fa930d5f0704edbce47076bdcfeeaba1c83683e5649d1"}, + {file = "yarl-1.7.2-cp37-cp37m-win_amd64.whl", hash = "sha256:c17965ff3706beedafd458c452bf15bac693ecd146a60a06a214614dc097a271"}, + {file = "yarl-1.7.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:fce78593346c014d0d986b7ebc80d782b7f5e19843ca798ed62f8e3ba8728576"}, + {file = "yarl-1.7.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c2a1ac41a6aa980db03d098a5531f13985edcb451bcd9d00670b03129922cd0d"}, + {file = "yarl-1.7.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:39d5493c5ecd75c8093fa7700a2fb5c94fe28c839c8e40144b7ab7ccba6938c8"}, + {file = "yarl-1.7.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1eb6480ef366d75b54c68164094a6a560c247370a68c02dddb11f20c4c6d3c9d"}, + {file = "yarl-1.7.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ba63585a89c9885f18331a55d25fe81dc2d82b71311ff8bd378fc8004202ff6"}, + {file = "yarl-1.7.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e39378894ee6ae9f555ae2de332d513a5763276a9265f8e7cbaeb1b1ee74623a"}, + {file = "yarl-1.7.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c0910c6b6c31359d2f6184828888c983d54d09d581a4a23547a35f1d0b9484b1"}, + {file = "yarl-1.7.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6feca8b6bfb9eef6ee057628e71e1734caf520a907b6ec0d62839e8293e945c0"}, + {file = "yarl-1.7.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:8300401dc88cad23f5b4e4c1226f44a5aa696436a4026e456fe0e5d2f7f486e6"}, + {file = "yarl-1.7.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:788713c2896f426a4e166b11f4ec538b5736294ebf7d5f654ae445fd44270832"}, + {file = "yarl-1.7.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:fd547ec596d90c8676e369dd8a581a21227fe9b4ad37d0dc7feb4ccf544c2d59"}, + {file = "yarl-1.7.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:737e401cd0c493f7e3dd4db72aca11cfe069531c9761b8ea474926936b3c57c8"}, + {file = "yarl-1.7.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:baf81561f2972fb895e7844882898bda1eef4b07b5b385bcd308d2098f1a767b"}, + {file = "yarl-1.7.2-cp38-cp38-win32.whl", hash = "sha256:ede3b46cdb719c794427dcce9d8beb4abe8b9aa1e97526cc20de9bd6583ad1ef"}, + {file = "yarl-1.7.2-cp38-cp38-win_amd64.whl", hash = "sha256:cc8b7a7254c0fc3187d43d6cb54b5032d2365efd1df0cd1749c0c4df5f0ad45f"}, + {file = "yarl-1.7.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:580c1f15500e137a8c37053e4cbf6058944d4c114701fa59944607505c2fe3a0"}, + {file = "yarl-1.7.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3ec1d9a0d7780416e657f1e405ba35ec1ba453a4f1511eb8b9fbab81cb8b3ce1"}, + {file = "yarl-1.7.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3bf8cfe8856708ede6a73907bf0501f2dc4e104085e070a41f5d88e7faf237f3"}, + {file = "yarl-1.7.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1be4bbb3d27a4e9aa5f3df2ab61e3701ce8fcbd3e9846dbce7c033a7e8136746"}, + {file = "yarl-1.7.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:534b047277a9a19d858cde163aba93f3e1677d5acd92f7d10ace419d478540de"}, + {file = "yarl-1.7.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6ddcd80d79c96eb19c354d9dca95291589c5954099836b7c8d29278a7ec0bda"}, + {file = "yarl-1.7.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:9bfcd43c65fbb339dc7086b5315750efa42a34eefad0256ba114cd8ad3896f4b"}, + {file = "yarl-1.7.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f64394bd7ceef1237cc604b5a89bf748c95982a84bcd3c4bbeb40f685c810794"}, + {file = "yarl-1.7.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:044daf3012e43d4b3538562da94a88fb12a6490652dbc29fb19adfa02cf72eac"}, + {file = "yarl-1.7.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:368bcf400247318382cc150aaa632582d0780b28ee6053cd80268c7e72796dec"}, + {file = "yarl-1.7.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:bab827163113177aee910adb1f48ff7af31ee0289f434f7e22d10baf624a6dfe"}, + {file = "yarl-1.7.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:0cba38120db72123db7c58322fa69e3c0efa933040ffb586c3a87c063ec7cae8"}, + {file = "yarl-1.7.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:59218fef177296451b23214c91ea3aba7858b4ae3306dde120224cfe0f7a6ee8"}, + {file = "yarl-1.7.2-cp39-cp39-win32.whl", hash = "sha256:1edc172dcca3f11b38a9d5c7505c83c1913c0addc99cd28e993efeaafdfaa18d"}, + {file = "yarl-1.7.2-cp39-cp39-win_amd64.whl", hash = "sha256:797c2c412b04403d2da075fb93c123df35239cd7b4cc4e0cd9e5839b73f52c58"}, + {file = "yarl-1.7.2.tar.gz", hash = "sha256:45399b46d60c253327a460e99856752009fcee5f5d3c80b2f7c0cae1c38d56dd"}, ] zipp = [ - {file = "zipp-3.11.0-py3-none-any.whl", hash = "sha256:83a28fcb75844b5c0cdaf5aa4003c2d728c77e05f5aeabe8e95e56727005fbaa"}, - {file = "zipp-3.11.0.tar.gz", hash = "sha256:a7a22e05929290a67401440b39690ae6563279bced5f314609d9d03798f56766"}, -] -zope-interface = [ - {file = "zope.interface-5.5.2-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:a2ad597c8c9e038a5912ac3cf166f82926feff2f6e0dabdab956768de0a258f5"}, - {file = "zope.interface-5.5.2-cp27-cp27m-win_amd64.whl", hash = "sha256:65c3c06afee96c654e590e046c4a24559e65b0a87dbff256cd4bd6f77e1a33f9"}, - {file = "zope.interface-5.5.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d514c269d1f9f5cd05ddfed15298d6c418129f3f064765295659798349c43e6f"}, - {file = "zope.interface-5.5.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5334e2ef60d3d9439c08baedaf8b84dc9bb9522d0dacbc10572ef5609ef8db6d"}, - {file = "zope.interface-5.5.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc26c8d44472e035d59d6f1177eb712888447f5799743da9c398b0339ed90b1b"}, - {file = "zope.interface-5.5.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:17ebf6e0b1d07ed009738016abf0d0a0f80388e009d0ac6e0ead26fc162b3b9c"}, - {file = "zope.interface-5.5.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f98d4bd7bbb15ca701d19b93263cc5edfd480c3475d163f137385f49e5b3a3a7"}, - {file = "zope.interface-5.5.2-cp310-cp310-win_amd64.whl", hash = "sha256:696f3d5493eae7359887da55c2afa05acc3db5fc625c49529e84bd9992313296"}, - {file = "zope.interface-5.5.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7579960be23d1fddecb53898035a0d112ac858c3554018ce615cefc03024e46d"}, - {file = "zope.interface-5.5.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:765d703096ca47aa5d93044bf701b00bbce4d903a95b41fff7c3796e747b1f1d"}, - {file = "zope.interface-5.5.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e945de62917acbf853ab968d8916290548df18dd62c739d862f359ecd25842a6"}, - {file = "zope.interface-5.5.2-cp311-cp311-win_amd64.whl", hash = "sha256:655796a906fa3ca67273011c9805c1e1baa047781fca80feeb710328cdbed87f"}, - {file = "zope.interface-5.5.2-cp35-cp35m-win_amd64.whl", hash = "sha256:0fb497c6b088818e3395e302e426850f8236d8d9f4ef5b2836feae812a8f699c"}, - {file = "zope.interface-5.5.2-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:008b0b65c05993bb08912f644d140530e775cf1c62a072bf9340c2249e613c32"}, - {file = "zope.interface-5.5.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:404d1e284eda9e233c90128697c71acffd55e183d70628aa0bbb0e7a3084ed8b"}, - {file = "zope.interface-5.5.2-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:3218ab1a7748327e08ef83cca63eea7cf20ea7e2ebcb2522072896e5e2fceedf"}, - {file = "zope.interface-5.5.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d169ccd0756c15bbb2f1acc012f5aab279dffc334d733ca0d9362c5beaebe88e"}, - {file = "zope.interface-5.5.2-cp36-cp36m-win_amd64.whl", hash = "sha256:e1574980b48c8c74f83578d1e77e701f8439a5d93f36a5a0af31337467c08fcf"}, - {file = "zope.interface-5.5.2-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:0217a9615531c83aeedb12e126611b1b1a3175013bbafe57c702ce40000eb9a0"}, - {file = "zope.interface-5.5.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:311196634bb9333aa06f00fc94f59d3a9fddd2305c2c425d86e406ddc6f2260d"}, - {file = "zope.interface-5.5.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6373d7eb813a143cb7795d3e42bd8ed857c82a90571567e681e1b3841a390d16"}, - {file = "zope.interface-5.5.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:959697ef2757406bff71467a09d940ca364e724c534efbf3786e86eee8591452"}, - {file = "zope.interface-5.5.2-cp37-cp37m-win_amd64.whl", hash = "sha256:dbaeb9cf0ea0b3bc4b36fae54a016933d64c6d52a94810a63c00f440ecb37dd7"}, - {file = "zope.interface-5.5.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:604cdba8f1983d0ab78edc29aa71c8df0ada06fb147cea436dc37093a0100a4e"}, - {file = "zope.interface-5.5.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e74a578172525c20d7223eac5f8ad187f10940dac06e40113d62f14f3adb1e8f"}, - {file = "zope.interface-5.5.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f0980d44b8aded808bec5059018d64692f0127f10510eca71f2f0ace8fb11188"}, - {file = "zope.interface-5.5.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6e972493cdfe4ad0411fd9abfab7d4d800a7317a93928217f1a5de2bb0f0d87a"}, - {file = "zope.interface-5.5.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:9d783213fab61832dbb10d385a319cb0e45451088abd45f95b5bb88ed0acca1a"}, - {file = "zope.interface-5.5.2-cp38-cp38-win_amd64.whl", hash = "sha256:a16025df73d24795a0bde05504911d306307c24a64187752685ff6ea23897cb0"}, - {file = "zope.interface-5.5.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:40f4065745e2c2fa0dff0e7ccd7c166a8ac9748974f960cd39f63d2c19f9231f"}, - {file = "zope.interface-5.5.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8a2ffadefd0e7206adc86e492ccc60395f7edb5680adedf17a7ee4205c530df4"}, - {file = "zope.interface-5.5.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d692374b578360d36568dd05efb8a5a67ab6d1878c29c582e37ddba80e66c396"}, - {file = "zope.interface-5.5.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:4087e253bd3bbbc3e615ecd0b6dd03c4e6a1e46d152d3be6d2ad08fbad742dcc"}, - {file = "zope.interface-5.5.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:fb68d212efd057596dee9e6582daded9f8ef776538afdf5feceb3059df2d2e7b"}, - {file = "zope.interface-5.5.2-cp39-cp39-win_amd64.whl", hash = "sha256:7e66f60b0067a10dd289b29dceabd3d0e6d68be1504fc9d0bc209cf07f56d189"}, - {file = "zope.interface-5.5.2.tar.gz", hash = "sha256:bfee1f3ff62143819499e348f5b8a7f3aa0259f9aca5e0ddae7391d059dce671"}, + {file = "zipp-3.7.0-py3-none-any.whl", hash = "sha256:b47250dd24f92b7dd6a0a8fc5244da14608f3ca90a5efcd37a3b1642fac9a375"}, + {file = "zipp-3.7.0.tar.gz", hash = "sha256:9f50f446828eb9d45b267433fd3e9da8d801f614129124863f9c51ebceafb87d"}, ] diff --git a/pyproject.toml b/pyproject.toml index 39087392fd..062a324e48 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,12 +1,12 @@ [tool.poetry] name = "strawberry-graphql" packages = [ { include = "strawberry" } ] -version = "0.155.2" +version = "0.97.0" description = "A library for creating GraphQL APIs" authors = ["Patrick Arminio "] license = "MIT" readme = "README.md" -keywords = ["graphql", "api", "rest", "starlette", "async"] +keywords = ["grapqhl", "api", "rest", "starlette", "async"] homepage = "https://strawberry.rocks/" repository = "https://github.com/strawberry-graphql/strawberry" @@ -21,143 +21,127 @@ classifiers = [ ] include = ["strawberry/py.typed"] -[tool.poetry.urls] -"Changelog" = "https://strawberry.rocks/changelog" -"Discord" = "https://discord.com/invite/3uQ2PaY" -"Twitter" = "https://twitter.com/strawberry_gql" -"Mastodon" = "https://farbun.social/@strawberry" -"Sponsor on GitHub" = "https://github.com/sponsors/strawberry-graphql" -"Sponsor on Open Collective" = "https://opencollective.com/strawberry-graphql" - - - [build-system] requires = ["poetry>=0.12"] build-backend = "poetry.masonry.api" [tool.poetry.dependencies] python = "^3.7" - -graphql-core = "~3.2.0" -typing_extensions = ">=3.7.4,<5.0.0" -python-dateutil = "^2.7.0" - -starlette = {version = ">=0.13.6", optional = true} +starlette = {version = ">=0.13.6,<0.17.0", optional = true} click = {version = ">=7.0,<9.0", optional = true} -pygments = {version = "^2.3", optional = true} -uvicorn = {version = ">=0.11.6,<0.21.0", optional = true} -Django = {version = ">=3.2", optional = true} +pygments = "^2.3" +uvicorn = {version = ">=0.11.6,<0.18.0", optional = true} +Django = [ + {version = ">=2.2,<4", markers = "python_version < '3.8' and extra == 'django'", optional = true}, + {version = ">=2.2,<5", markers = "python_version >= '3.8' and extra == 'django'", optional = true} +] +graphql-core = "~3.1.0" asgiref = {version = "^3.2", optional = true} -flask = {version = ">=1.1", optional = true} +flask = {version = "^1.1", optional = true} +typing_extensions = ">=3.7.4,<5.0.0" opentelemetry-api = {version = "<2", optional = true} opentelemetry-sdk = {version = "<2", optional = true} chalice = {version = "^1.22", optional = true} +python-dateutil = "^2.7.0" pydantic = {version = "<2", optional = true} -python-multipart = {version = "^0.0.5", optional = true} -sanic = {version = ">=20.12.2", optional = true} +python-multipart = "^0.0.5" +sanic = {version = ">=20.12.2,<22.0.0", optional = true} aiohttp = {version = "^3.7.4.post0", optional = true} fastapi = {version = ">=0.65.2", optional = true} -starlite = {version = ">=1.48.0", optional = true, python = "^3.8"} -channels = {version = ">=3.0.5", optional = true} -"backports.cached-property" = {version = "^1.0.2", python = "<3.8"} -libcst = {version = ">=0.4.7", optional = true} -rich = {version = ">=12.0.0", optional = true} - +sqlmodel = {version = "^0.0.6", optional = true} +ormar = {version = "^0.10.24", optional = true} +sentinel = "^0.3.0" +"backports.cached-property" = "^1.0.1" -[tool.poetry.group.dev.dependencies] -pytest-xdist = {extras = ["psutil"], version = "^3.1.0"} -pytest-cov = "^4.0.0" -pytest = "^7.2" +[tool.poetry.dev-dependencies] +pytest = "^7.0" pytest-emoji = "^0.2.0" -black = "^22" -pytest-asyncio = "^0.20.3" -mypy = "^0.991" -pytest-mypy-plugins = "^1.10" -pytest-mock = "^3.10" +flake8 = "^4.0" +black = {version = "^21.12b0", allow-prereleases = true} +isort = "^5.10.1" +pytest-asyncio = "^0.18.1" +pytest-cov = "^3.0" +mypy = "^0.931" +flake8-bugbear = "^22.1.11" +flake8-eradicate = "^1.2.0" +pytest-mypy-plugins = "^1.9" +pytest-mock = "^3.7" pytest-django = {version = "^4.5"} asgiref = "^3.2" pytest-flask = {version = "^1.2.0"} -flask = ">=1.1" +flask = {version = "^1.1"} chalice = {version = "^1.22"} -requests = "^2.28.1" -pytest-benchmark = "^4.0.0" -freezegun = "^1.2.1" +requests = "^2.27.1" +pre-commit = "^2.16.0" +pytest-benchmark = "^3.4.1" +freezegun = "^1.1.0" opentelemetry-api = "<2" opentelemetry-sdk = "<2" -Django = ">=3.2" +flake8-isort = "^4.1.1" +flake8-black = "^0.2.1" +Django = [ + {version = ">=2.2,<4", python = "<3.8", optional = false}, + {version = ">=2.2,<5", python = ">=3.8", optional = false} +] pydantic = {version = "<2", optional = false} email-validator = {version = "^1.1.3", optional = false} -uvicorn = ">=0.11.6" -starlette = ">=0.13.6" -sanic = ">=20.12.2" +starlette = ">=0.13.6,<0.17.0" +uvicorn = ">=0.11.6,<0.18.0" +sanic = ">=20.12.2,<22.0.0" aiohttp = "^3.7.4.post0" pytest-aiohttp = "^1.0.3" -types-typed-ast = "^1.5.8" -types-toml = "^0.10.8" -types-ujson = "^5.6.0" -types-requests = "^2.28.11" -types-python-dateutil = "^2.8.19" -types-freezegun = "^1.1.9" -types-chardet = "^5.0.4" +types-typed-ast = "^1.5.2" +types-toml = "^0.10.4" +types-ujson = "^4.2.1" +types-setuptools = "^57.4.9" +types-requests = "^2.27.10" +types-python-dateutil = "^2.8.9" +types-freezegun = "^1.1.6" +types-chardet = "^4.0.3" types-certifi = "^2021.10.8" -types-aiofiles = "^22.1.0" -sanic-testing = "^22.9.0" +types-aiofiles = "^0.8.3" +sanic-testing = "^0.8" fastapi = {version = ">=0.65.0", optional = false} -starlite = {version = ">=1.48.0", optional = false, python = "^3.8", extras = ["testing"]} -MarkupSafe = "2.1.1" -pytest-snapshot = "^0.9.0" -channels = "^3.0.5" -rich = {version = "^12.5.1", optional = false} -libcst = {version = "^0.4.7", optional = false} -ddtrace = "^1.6.4" -python-multipart = "^0.0.5" -pygments = "^2.3" +pytest-xprocess = "^0.18.1" +sqlmodel = {version = "^0.0.6", optional = false} +ormar = {version = "^0.10.24", optional = false} [tool.poetry.extras] aiohttp = ["aiohttp", "pytest-aiohttp"] -asgi = ["starlette", "python-multipart"] -debug = ["rich", "libcst"] -debug-server = ["starlette", "uvicorn", "python-multipart", "click", "pygments", "rich", "libcst"] +asgi = ["starlette"] +debug-server = ["starlette", "uvicorn"] django = ["Django", "pytest-django", "asgiref"] -channels = ["channels", "asgiref"] flask = ["flask", "pytest-flask"] opentelemetry = ["opentelemetry-api", "opentelemetry-sdk"] pydantic = ["pydantic"] sanic = ["sanic"] -fastapi = ["fastapi", "python-multipart"] +fastapi = ["fastapi"] chalice = ["chalice"] -cli = ["click", "pygments", "rich", "libcst"] +ormar = ["ormar"] +sqlmodel = ["sqlmodel"] [tool.poetry.scripts] strawberry = "strawberry.cli:run" -[tool.black] -line-length = 88 -extend-exclude = ''' -tests/codegen/snapshots/ -''' +[tool.isort] +src_paths = ["strawberry", "tests", "scripts"] +profile = "black" +indent = 4 +combine_star = true +combine_as_imports = true +lines_after_imports = 2 +known_django = ["django"] +known_graphql = ["graphql"] +known_pytest = ["pytest"] +known_first_party = ["strawberry"] +sections = ["FUTURE", "STDLIB", "PYTEST", "THIRDPARTY", "DJANGO", "GRAPHQL", "FIRSTPARTY", "LOCALFOLDER"] [tool.pytest.ini_options] -addopts = "-s --emoji --mypy-ini-file=mypy.ini --benchmark-disable" +addopts = "-s --emoji --mypy-ini-file=mypy_tests.ini --benchmark-disable" DJANGO_SETTINGS_MODULE = "tests.django.django_settings" testpaths = ["tests/"] -markers = [ - "django", - "asgi", - "starlette", - "channels", - "sanic", - "aiohttp", - "fastapi", - "chalice", - "flask", - "starlite," -] +markers = ["django"] asyncio_mode = "auto" -filterwarnings = [ - "ignore::DeprecationWarning:strawberry.*.resolver", - "ignore:LazyType is deprecated:DeprecationWarning", -] [tool.autopub] git-username = "Botberry" @@ -172,156 +156,3 @@ reportMissingImports = true reportMissingTypeStubs = false pythonVersion = "3.7" stubPath = "" - -[tool.ruff] -line-length = 88 -select = ["ALL"] -target-version = "py37" -ignore = [ - "TID252", - # we use asserts in tests and to hint mypy - "S101", - "S102", - "S104", - "S324", - # maybe we can enable this in future - # we'd want to have consistent docstrings in future - "D", - "ANN101", # missing annotation for self? - # definitely enable these, maybe not in tests - "ANN001", - "ANN002", - "ANN003", - "ANN102", - "ANN201", - "ANN202", - "ANN204", - "ANN205", - "ANN206", - "ANN401", - "PGH003", - "PGH004", - "RET504", - "RET505", - "RET506", - "RET507", - "BLE001", - "B008", - "N811", - "N804", - "N818", - # Variable `T` in function should be lowercase - # this seems a potential bug or opportunity for improvement in ruff - "N806", - - # first argument should named self (found in tests) - "N805", - - "N815", - - # shadowing builtins - "A001", - "A002", - "A003", - - "ARG001", - "ARG002", - "ARG003", - "ARG004", - "ARG005", - "FBT001", - "FBT002", - "FBT003", - - "PT001", - "PT023", - - # enable these, we have some in tests - "B006", - "PT004", - "PT007", - "PT011", - "PT012", - "PT015", - "PT017", - "C414", - "N802", - "SIM117", - "SIM102", - - "F841", - "B027", - "B905", - "ISC001", - - # same? - "S105", - "S106", - - "DTZ003", - "DTZ005", - # in tests - "DTZ001", - - "EM101", - "EM102", - "EM103", - - "B904", - "B019", - - "N801", - "N807", - - # pandas - "PD", - - # code complexity - "C", - - # trailing commas - "COM812", - - "PLR", - "INP", - "TRY", -] -fix = true -exclude = [ - ".bzr", - ".direnv", - ".eggs", - ".git", - ".hg", - ".mypy_cache", - ".nox", - ".pants.d", - ".ruff_cache", - ".svn", - ".tox", - ".venv", - "__pypackages__", - "_build", - "buck-out", - "build", - "dist", - "node_modules", - "venv", - "tests/codegen/snapshots" -] -src = ["strawberry", "tests"] - -[tool.ruff.per-file-ignores] -"tests/federation/printer/*" = ["E501"] -"tests/test_printer/test_basic.py" = ["E501"] -"tests/pyright/test_federation.py" = ["E501"] -"tests/test_printer/test_schema_directives.py" = ["E501"] - -[tool.ruff.isort] -known-first-party = ["strawberry"] -known-third-party = ["django", "graphql"] -extra-standard-library = ["typing_extensions"] - -[tool.ruff.pyupgrade] -# Preserve types, even if a file imports `from __future__ import annotations`. -keep-runtime-typing = true diff --git a/setup.py b/setup.py old mode 100755 new mode 100644 index 8157720105..09694c4caa --- a/setup.py +++ b/setup.py @@ -5,5 +5,6 @@ import setuptools + if __name__ == "__main__": setuptools.setup(name="strawberry-graphql") diff --git a/strawberry/__init__.py b/strawberry/__init__.py index 4ab62fa742..22e5138b8d 100644 --- a/strawberry/__init__.py +++ b/strawberry/__init__.py @@ -2,35 +2,31 @@ from .arguments import argument from .auto import auto from .custom_scalar import scalar -from .directive import directive, directive_field -from .enum import enum, enum_value +from .directive import directive +from .enum import enum from .field import field -from .lazy_type import LazyType, lazy +from .lazy_type import LazyType from .mutation import mutation, subscription -from .object_type import asdict, input, interface, type +from .object_type import input, interface, type from .permission import BasePermission from .private import Private from .scalars import ID from .schema import Schema from .schema_directive import schema_directive from .union import union -from .unset import UNSET + __all__ = [ "BasePermission", "experimental", "ID", - "UNSET", - "lazy", "LazyType", "Private", "Schema", "argument", "directive", - "directive_field", "schema_directive", "enum", - "enum_value", "federation", "field", "input", @@ -41,5 +37,4 @@ "type", "union", "auto", - "asdict", ] diff --git a/strawberry/__main__.py b/strawberry/__main__.py index 2e933c92f5..92e8476732 100644 --- a/strawberry/__main__.py +++ b/strawberry/__main__.py @@ -1,4 +1,5 @@ from .cli import run + if __name__ == "__main__": run() diff --git a/strawberry/aiohttp/handlers/__init__.py b/strawberry/aiohttp/handlers/__init__.py index de5b7f5eca..84b3301471 100644 --- a/strawberry/aiohttp/handlers/__init__.py +++ b/strawberry/aiohttp/handlers/__init__.py @@ -4,4 +4,5 @@ from strawberry.aiohttp.handlers.graphql_ws_handler import GraphQLWSHandler from strawberry.aiohttp.handlers.http_handler import HTTPHandler + __all__ = ["GraphQLTransportWSHandler", "GraphQLWSHandler", "HTTPHandler"] diff --git a/strawberry/aiohttp/handlers/graphql_transport_ws_handler.py b/strawberry/aiohttp/handlers/graphql_transport_ws_handler.py index 5246fbf029..1c2c95e73b 100644 --- a/strawberry/aiohttp/handlers/graphql_transport_ws_handler.py +++ b/strawberry/aiohttp/handlers/graphql_transport_ws_handler.py @@ -50,6 +50,5 @@ async def handle_request(self) -> web.StreamResponse: finally: for operation_id in list(self.subscriptions.keys()): await self.cleanup_operation(operation_id) - await self.reap_completed_tasks() return self._ws diff --git a/strawberry/aiohttp/handlers/http_handler.py b/strawberry/aiohttp/handlers/http_handler.py index 29ca306050..6c9b1e45f3 100644 --- a/strawberry/aiohttp/handlers/http_handler.py +++ b/strawberry/aiohttp/handlers/http_handler.py @@ -1,16 +1,13 @@ import json from io import BytesIO -from typing import Any, Dict, Union -from typing_extensions import Literal +from pathlib import Path +from typing import Any, Dict from aiohttp import web from strawberry.exceptions import MissingQueryError from strawberry.file_uploads.utils import replace_placeholders_with_files -from strawberry.http import GraphQLRequestData, parse_query_params, parse_request_data +from strawberry.http import GraphQLRequestData, parse_request_data from strawberry.schema import BaseSchema -from strawberry.schema.exceptions import InvalidOperationTypeError -from strawberry.types.graphql import OperationType -from strawberry.utils.graphiql import get_graphiql_html class HTTPHandler: @@ -18,19 +15,15 @@ def __init__( self, schema: BaseSchema, graphiql: bool, - allow_queries_via_get: bool, get_context, get_root_value, - encode_json, process_result, request: web.Request, ): self.schema = schema self.graphiql = graphiql - self.allow_queries_via_get = allow_queries_via_get self.get_context = get_context self.get_root_value = get_root_value - self.encode_json = encode_json self.process_result = process_result self.request = request @@ -42,83 +35,46 @@ async def handle(self) -> web.StreamResponse: raise web.HTTPMethodNotAllowed(self.request.method, ["GET", "POST"]) async def get(self, request: web.Request) -> web.StreamResponse: - if request.query: - try: - query_params = { - key: request.query.getone(key) for key in set(request.query.keys()) - } - query_data = parse_query_params(query_params) - request_data = parse_request_data(query_data) - except json.JSONDecodeError: - raise web.HTTPBadRequest(reason="Unable to parse request body as JSON") - - return await self.execute_request( - request=request, request_data=request_data, method="GET" - ) - - elif self.should_render_graphiql(request): + if self.should_render_graphiql(request): return self.render_graphiql() raise web.HTTPNotFound() async def post(self, request: web.Request) -> web.StreamResponse: request_data = await self.get_request_data(request) - - return await self.execute_request( - request=request, request_data=request_data, method="POST" - ) - - async def execute_request( - self, - request: web.Request, - request_data: GraphQLRequestData, - method: Union[Literal["GET"], Literal["POST"]], - ) -> web.StreamResponse: response = web.Response() - context = await self.get_context(request, response) root_value = await self.get_root_value(request) - allowed_operation_types = OperationType.from_http(method) - - if not self.allow_queries_via_get and method == "GET": - allowed_operation_types = allowed_operation_types - {OperationType.QUERY} - - try: - result = await self.schema.execute( - query=request_data.query, - root_value=root_value, - variable_values=request_data.variables, - context_value=context, - operation_name=request_data.operation_name, - allowed_operation_types=allowed_operation_types, - ) - except InvalidOperationTypeError as e: - raise web.HTTPBadRequest( - reason=e.as_http_error_reason(method=method) - ) from e - except MissingQueryError: - raise web.HTTPBadRequest(reason="No GraphQL query found in the request") + result = await self.schema.execute( + query=request_data.query, + root_value=root_value, + variable_values=request_data.variables, + context_value=context, + operation_name=request_data.operation_name, + ) response_data = await self.process_result(request, result) - - response.text = self.encode_json(response_data) + response.text = json.dumps(response_data) response.content_type = "application/json" - return response async def get_request_data(self, request: web.Request) -> GraphQLRequestData: data = await self.parse_body(request) - return parse_request_data(data) + + try: + request_data = parse_request_data(data) + except MissingQueryError: + raise web.HTTPBadRequest(reason="No GraphQL query found in the request") + + return request_data async def parse_body(self, request: web.Request) -> dict: if request.content_type.startswith("multipart/form-data"): return await self.parse_multipart_body(request) try: return await request.json() - except json.JSONDecodeError as e: - raise web.HTTPBadRequest( - reason="Unable to parse request body as JSON" - ) from e + except json.JSONDecodeError: + raise web.HTTPBadRequest(reason="Unable to parse request body as JSON") async def parse_multipart_body(self, request: web.Request) -> dict: reader = await request.multipart() @@ -143,15 +99,15 @@ async def parse_multipart_body(self, request: web.Request) -> dict: raise web.HTTPBadRequest(reason="File(s) missing in form data") def render_graphiql(self) -> web.StreamResponse: - html_string = get_graphiql_html() - + html_string = self.graphiql_html_file_path.read_text() + html_string = html_string.replace("{{ SUBSCRIPTION_ENABLED }}", "true") return web.Response(text=html_string, content_type="text/html") def should_render_graphiql(self, request: web.Request) -> bool: if not self.graphiql: return False + return "text/html" in request.headers.get("Accept", "") - return any( - supported_header in request.headers.get("Accept", "") - for supported_header in ("text/html", "*/*") - ) + @property + def graphiql_html_file_path(self) -> Path: + return Path(__file__).parent.parent.parent / "static" / "graphiql.html" diff --git a/strawberry/aiohttp/test/__init__.py b/strawberry/aiohttp/test/__init__.py deleted file mode 100644 index 47b4c12cc0..0000000000 --- a/strawberry/aiohttp/test/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .client import GraphQLTestClient - -__all__ = ["GraphQLTestClient"] diff --git a/strawberry/aiohttp/test/client.py b/strawberry/aiohttp/test/client.py deleted file mode 100644 index 0edd9a1423..0000000000 --- a/strawberry/aiohttp/test/client.py +++ /dev/null @@ -1,42 +0,0 @@ -from typing import Dict, Mapping, Optional - -from strawberry.test.client import BaseGraphQLTestClient, Response - - -class GraphQLTestClient(BaseGraphQLTestClient): - async def query( - self, - query: str, - variables: Optional[Dict[str, Mapping]] = None, - headers: Optional[Dict[str, object]] = None, - asserts_errors: Optional[bool] = True, - files: Optional[Dict[str, object]] = None, - ) -> Response: - body = self._build_body(query, variables, files) - - resp = await self.request(body, headers, files) - data = await resp.json() - - response = Response( - errors=data.get("errors"), - data=data.get("data"), - extensions=data.get("extensions"), - ) - if asserts_errors: - assert resp.status == 200 - assert response.errors is None - - return response - - async def request( - self, - body: Dict[str, object], - headers: Optional[Dict[str, object]] = None, - files: Optional[Dict[str, object]] = None, - ): - response = await self._client.post( - self.url, - json=body if not files else None, - data=body if files else None, - ) - return response diff --git a/strawberry/aiohttp/views.py b/strawberry/aiohttp/views.py index 3c418839d3..44bc1e7444 100644 --- a/strawberry/aiohttp/views.py +++ b/strawberry/aiohttp/views.py @@ -1,7 +1,4 @@ -import asyncio -import json from datetime import timedelta -from typing import Iterable from aiohttp import web from strawberry.aiohttp.handlers import ( @@ -16,10 +13,6 @@ class GraphQLView: - # Mark the view as coroutine so that AIOHTTP does not confuse it with a deprecated - # bare handler function. - _is_coroutine = asyncio.coroutines._is_coroutine # type: ignore[attr-defined] - graphql_transport_ws_handler_class = GraphQLTransportWSHandler graphql_ws_handler_class = GraphQLWSHandler http_handler_class = HTTPHandler @@ -28,19 +21,14 @@ def __init__( self, schema: BaseSchema, graphiql: bool = True, - allow_queries_via_get: bool = True, keep_alive: bool = True, keep_alive_interval: float = 1, debug: bool = False, - subscription_protocols: Iterable[str] = ( - GRAPHQL_TRANSPORT_WS_PROTOCOL, - GRAPHQL_WS_PROTOCOL, - ), + subscription_protocols=(GRAPHQL_TRANSPORT_WS_PROTOCOL, GRAPHQL_WS_PROTOCOL), connection_init_wait_timeout: timedelta = timedelta(minutes=1), ): self.schema = schema self.graphiql = graphiql - self.allow_queries_via_get = allow_queries_via_get self.keep_alive = keep_alive self.keep_alive_interval = keep_alive_interval self.debug = debug @@ -79,10 +67,8 @@ async def __call__(self, request: web.Request) -> web.StreamResponse: return await self.http_handler_class( schema=self.schema, graphiql=self.graphiql, - allow_queries_via_get=self.allow_queries_via_get, get_context=self.get_context, get_root_value=self.get_root_value, - encode_json=self.encode_json, process_result=self.process_result, request=request, ).handle() @@ -99,6 +85,3 @@ async def process_result( self, request: web.Request, result: ExecutionResult ) -> GraphQLHTTPResponse: return process_result(result) - - def encode_json(self, response_data: GraphQLHTTPResponse) -> str: - return json.dumps(response_data) diff --git a/strawberry/annotation.py b/strawberry/annotation.py index 2abe3020c2..9fd0018db7 100644 --- a/strawberry/annotation.py +++ b/strawberry/annotation.py @@ -1,31 +1,28 @@ import sys import typing -from collections import abc +from collections.abc import AsyncGenerator as AsyncGenerator_abc from enum import Enum -from typing import ( # type: ignore[attr-defined] +from typing import ( # type: ignore TYPE_CHECKING, Any, + AsyncGenerator as AsyncGenerator_typing, Dict, - List, Optional, TypeVar, Union, _eval_type, ) -from typing_extensions import Annotated, Self, get_args, get_origin -from strawberry.exceptions.not_a_strawberry_enum import NotAStrawberryEnumError -from strawberry.private import is_private try: - from typing import ForwardRef + from typing import ForwardRef # type: ignore except ImportError: # pragma: no cover # ForwardRef is private in python 3.6 and 3.7 from typing import _ForwardRef as ForwardRef # type: ignore from strawberry.custom_scalar import ScalarDefinition from strawberry.enum import EnumDefinition -from strawberry.lazy_type import LazyType, StrawberryLazyReference +from strawberry.lazy_type import LazyType from strawberry.type import ( StrawberryList, StrawberryOptional, @@ -33,25 +30,14 @@ StrawberryTypeVar, ) from strawberry.types.types import TypeDefinition -from strawberry.unset import UNSET -from strawberry.utils.typing import is_generic, is_list, is_type_var, is_union +from strawberry.unset import _Unset +from strawberry.utils.typing import is_generic, is_type_var + if TYPE_CHECKING: - from strawberry.field import StrawberryField from strawberry.union import StrawberryUnion -ASYNC_TYPES = ( - abc.AsyncGenerator, - abc.AsyncIterable, - abc.AsyncIterator, - typing.AsyncContextManager, - typing.AsyncGenerator, - typing.AsyncIterable, - typing.AsyncIterator, -) - - class StrawberryAnnotation: def __init__( self, annotation: Union[object, str], *, namespace: Optional[Dict] = None @@ -65,73 +51,16 @@ def __eq__(self, other: object) -> bool: return self.resolve() == other.resolve() - @staticmethod - def from_annotation( - annotation: object, namespace: Optional[Dict] = None - ) -> Optional["StrawberryAnnotation"]: - if annotation is None: - return None - - if not isinstance(annotation, StrawberryAnnotation): - return StrawberryAnnotation(annotation, namespace=namespace) - return annotation - - @staticmethod - def parse_annotated(annotation: object) -> object: - from strawberry.auto import StrawberryAuto - - if is_private(annotation): - return annotation - - annotation_origin = get_origin(annotation) - - if annotation_origin is Annotated: - annotated_args = get_args(annotation) - annotation_type = annotated_args[0] - - for arg in annotated_args[1:]: - if isinstance(arg, StrawberryLazyReference): - assert isinstance(annotation_type, ForwardRef) - - return arg.resolve_forward_ref(annotation_type) - - if isinstance(arg, StrawberryAuto): - return arg - - return StrawberryAnnotation.parse_annotated(annotation_type) - - elif is_union(annotation): - return Union[ - tuple( - StrawberryAnnotation.parse_annotated(arg) - for arg in get_args(annotation) - ) # pyright: ignore - ] # pyright: ignore - - elif is_list(annotation): - return List[StrawberryAnnotation.parse_annotated(get_args(annotation)[0])] # type: ignore # noqa: E501 - - elif annotation_origin and is_generic(annotation_origin): - args = get_args(annotation) - - return annotation_origin[ - tuple(StrawberryAnnotation.parse_annotated(arg) for arg in args) - ] - - return annotation - def resolve(self) -> Union[StrawberryType, type]: - annotation = self.parse_annotated(self.annotation) - + annotation: object if isinstance(self.annotation, str): annotation = ForwardRef(self.annotation) + else: + annotation = self.annotation evaled_type = _eval_type(annotation, self.namespace, None) - - if is_private(evaled_type): - return evaled_type - if self._is_async_type(evaled_type): - evaled_type = self._strip_async_type(evaled_type) + if self._is_async_generator(evaled_type): + evaled_type = self._strip_async_generator(evaled_type) if self._is_lazy_type(evaled_type): return evaled_type @@ -154,17 +83,13 @@ def resolve(self) -> Union[StrawberryType, type]: return self.create_optional(evaled_type) elif self._is_union(evaled_type): return self.create_union(evaled_type) - elif is_type_var(evaled_type) or evaled_type is Self: + elif is_type_var(evaled_type): return self.create_type_var(evaled_type) # TODO: Raise exception now, or later? # ... raise NotImplementedError(f"Unknown type {evaled_type}") return evaled_type - def set_namespace_from_field(self, field: "StrawberryField"): - module = sys.modules[field.origin.__module__] - self.namespace = module.__dict__ - def create_concrete_type(self, evaled_type: type) -> type: if _is_object_type(evaled_type): type_definition: TypeDefinition @@ -174,10 +99,7 @@ def create_concrete_type(self, evaled_type: type) -> type: raise ValueError(f"Not supported {evaled_type}") def create_enum(self, evaled_type: Any) -> EnumDefinition: - try: - return evaled_type._enum_definition - except AttributeError: - raise NotAStrawberryEnumError(evaled_type) + return evaled_type._enum_definition def create_list(self, evaled_type: Any) -> StrawberryList: of_type = StrawberryAnnotation( @@ -191,7 +113,7 @@ def create_optional(self, evaled_type: Any) -> StrawberryOptional: types = evaled_type.__args__ non_optional_types = tuple( filter( - lambda x: x is not type(None) and x is not type(UNSET), + lambda x: x is not type(None) and x is not _Unset, # noqa: E721 types, ) ) @@ -228,9 +150,14 @@ def create_union(self, evaled_type) -> "StrawberryUnion": return union @classmethod - def _is_async_type(cls, annotation: type) -> bool: + def _is_async_generator(cls, annotation: type) -> bool: origin = getattr(annotation, "__origin__", None) - return origin in ASYNC_TYPES + if origin is AsyncGenerator_abc: + return True + if origin is AsyncGenerator_typing: + # deprecated in Python 3.9 and above + return True + return False @classmethod def _is_enum(cls, annotation: Any) -> bool: @@ -262,7 +189,7 @@ def _is_optional(cls, annotation: Any) -> bool: types = annotation.__args__ # A Union to be optional needs to have at least one None type - return any(x is type(None) for x in types) + return any(x is type(None) for x in types) # noqa: E721 @classmethod def _is_list(cls, annotation: Any) -> bool: @@ -270,7 +197,7 @@ def _is_list(cls, annotation: Any) -> bool: annotation_origin = getattr(annotation, "__origin__", None) - return (annotation_origin in (list, tuple)) or annotation_origin is abc.Sequence + return annotation_origin == list @classmethod def _is_strawberry_type(cls, evaled_type: Any) -> bool: @@ -307,20 +234,19 @@ def _is_union(cls, annotation: Any) -> bool: # don't have a `__origin__` property on them, but they are instances of # `UnionType`, which is only available in Python 3.10+ if sys.version_info >= (3, 10): - from types import UnionType + from types import UnionType # type: ignore if isinstance(annotation, UnionType): return True - # unions declared as Union[A, B] fall through to this check - # even on python 3.10+ + # unions declared as Union[A, B] fall through to this check, even on python 3.10+ annotation_origin = getattr(annotation, "__origin__", None) return annotation_origin is typing.Union @classmethod - def _strip_async_type(cls, annotation) -> type: + def _strip_async_generator(cls, annotation) -> type: return annotation.__args__[0] @classmethod @@ -341,4 +267,5 @@ def _is_input_type(type_: Any) -> bool: def _is_object_type(type_: Any) -> bool: + # isinstance(type_, StrawberryObjectType) # noqa: E800 return hasattr(type_, "_type_definition") diff --git a/strawberry/arguments.py b/strawberry/arguments.py index 9136972ec5..94d07b3dcd 100644 --- a/strawberry/arguments.py +++ b/strawberry/arguments.py @@ -1,7 +1,6 @@ from __future__ import annotations import inspect -import warnings from typing import ( TYPE_CHECKING, Any, @@ -13,49 +12,45 @@ Union, cast, ) + from typing_extensions import Annotated, get_args, get_origin from strawberry.annotation import StrawberryAnnotation from strawberry.custom_scalar import ScalarDefinition, ScalarWrapper from strawberry.enum import EnumDefinition -from strawberry.lazy_type import LazyType, StrawberryLazyReference +from strawberry.lazy_type import LazyType from strawberry.type import StrawberryList, StrawberryOptional, StrawberryType from .exceptions import MultipleStrawberryArgumentsError, UnsupportedTypeError from .scalars import is_scalar from .types.types import TypeDefinition -from .unset import UNSET as _deprecated_UNSET -from .unset import _deprecated_is_unset # noqa +from .unset import _Unset + if TYPE_CHECKING: from strawberry.schema.config import StrawberryConfig -DEPRECATED_NAMES: Dict[str, str] = { - "UNSET": ( - "importing `UNSET` from `strawberry.arguments` is deprecated, " - "import instead from `strawberry` or from `strawberry.unset`" - ), - "is_unset": "`is_unset` is deprecated use `value is UNSET` instead", -} +UNSET: Any = _Unset() + + +def is_unset(value: Any) -> bool: + return type(value) is _Unset class StrawberryArgumentAnnotation: description: Optional[str] name: Optional[str] deprecation_reason: Optional[str] - directives: Iterable[object] def __init__( self, description: Optional[str] = None, name: Optional[str] = None, deprecation_reason: Optional[str] = None, - directives: Iterable[object] = (), ): self.description = description self.name = name self.deprecation_reason = deprecation_reason - self.directives = directives class StrawberryArgument: @@ -66,23 +61,19 @@ def __init__( type_annotation: StrawberryAnnotation, is_subscription: bool = False, description: Optional[str] = None, - default: object = _deprecated_UNSET, + default: object = UNSET, deprecation_reason: Optional[str] = None, - directives: Iterable[object] = (), ) -> None: - self.python_name = python_name + self.python_name = python_name # type: ignore self.graphql_name = graphql_name self.is_subscription = is_subscription self.description = description self._type: Optional[StrawberryType] = None self.type_annotation = type_annotation self.deprecation_reason = deprecation_reason - self.directives = directives # TODO: Consider moving this logic to a function - self.default = ( - _deprecated_UNSET if default is inspect.Parameter.empty else default - ) + self.default = UNSET if default is inspect.Parameter.empty else default if self._annotation_is_annotated(type_annotation): self._parse_annotated() @@ -105,7 +96,6 @@ def _parse_annotated(self): # in the other Annotated args, raising an exception if there # are multiple StrawberryArgumentAnnotations argument_annotation_seen = False - for arg in annotated_args[1:]: if isinstance(arg, StrawberryArgumentAnnotation): if argument_annotation_seen: @@ -118,12 +108,6 @@ def _parse_annotated(self): self.description = arg.description self.graphql_name = arg.name self.deprecation_reason = arg.deprecation_reason - self.directives = arg.directives - - if isinstance(arg, StrawberryLazyReference): - self.type_annotation = StrawberryAnnotation( - arg.resolve_forward_ref(annotated_args[0]) - ) def convert_argument( @@ -132,13 +116,11 @@ def convert_argument( scalar_registry: Dict[object, Union[ScalarWrapper, ScalarDefinition]], config: StrawberryConfig, ) -> object: - # TODO: move this somewhere else and make it first class - if value is None: return None - if value is _deprecated_UNSET: - return _deprecated_UNSET + if is_unset(value): + return value if isinstance(type_, StrawberryOptional): return convert_argument(value, type_.of_type, scalar_registry, config) @@ -153,18 +135,18 @@ def convert_argument( if is_scalar(type_, scalar_registry): return value + # Convert Enum fields to instances using the value. This is safe + # because graphql-core has already validated the input. if isinstance(type_, EnumDefinition): - return value + return type_.wrapped_cls(value) if isinstance(type_, LazyType): return convert_argument(value, type_.resolve_type(), scalar_registry, config) - if hasattr(type_, "_enum_definition"): - enum_definition: EnumDefinition = type_._enum_definition - return convert_argument(value, enum_definition, scalar_registry, config) - if hasattr(type_, "_type_definition"): # TODO: Replace with StrawberryInputObject - type_definition: TypeDefinition = type_._type_definition + type_definition: TypeDefinition = type_._type_definition # type: ignore + + assert type_definition.is_input kwargs = {} @@ -221,28 +203,17 @@ def argument( description: Optional[str] = None, name: Optional[str] = None, deprecation_reason: Optional[str] = None, - directives: Iterable[object] = (), ) -> StrawberryArgumentAnnotation: return StrawberryArgumentAnnotation( - description=description, - name=name, - deprecation_reason=deprecation_reason, - directives=directives, + description=description, name=name, deprecation_reason=deprecation_reason ) -def __getattr__(name: str) -> Any: - if name in DEPRECATED_NAMES: - warnings.warn(DEPRECATED_NAMES[name], DeprecationWarning, stacklevel=2) - return globals()[f"_deprecated_{name}"] - raise AttributeError(f"module {__name__} has no attribute {name}") - - # TODO: check exports -__all__ = [ # noqa: F822 +__all__ = [ "StrawberryArgument", "StrawberryArgumentAnnotation", - "UNSET", # for backwards compatibility + "UNSET", "argument", - "is_unset", # for backwards compatibility + "is_unset", ] diff --git a/strawberry/asgi/__init__.py b/strawberry/asgi/__init__.py index 6c95e571cb..1cf3920fd1 100644 --- a/strawberry/asgi/__init__.py +++ b/strawberry/asgi/__init__.py @@ -1,4 +1,3 @@ -import json from datetime import timedelta from typing import Any, Optional, Union @@ -27,7 +26,6 @@ def __init__( self, schema: BaseSchema, graphiql: bool = True, - allow_queries_via_get: bool = True, keep_alive: bool = False, keep_alive_interval: float = 1, debug: bool = False, @@ -36,7 +34,6 @@ def __init__( ) -> None: self.schema = schema self.graphiql = graphiql - self.allow_queries_via_get = allow_queries_via_get self.keep_alive = keep_alive self.keep_alive_interval = keep_alive_interval self.debug = debug @@ -48,12 +45,10 @@ async def __call__(self, scope: Scope, receive: Receive, send: Send): await self.http_handler_class( schema=self.schema, graphiql=self.graphiql, - allow_queries_via_get=self.allow_queries_via_get, debug=self.debug, get_context=self.get_context, get_root_value=self.get_root_value, process_result=self.process_result, - encode_json=self.encode_json, ).handle(scope=scope, receive=receive, send=send) elif scope["type"] == "websocket": @@ -84,7 +79,7 @@ async def __call__(self, scope: Scope, receive: Receive, send: Send): await ws.close(code=4406) else: # pragma: no cover - raise ValueError("Unknown scope type: {!r}".format(scope["type"])) + raise ValueError("Unknown scope type: %r" % (scope["type"],)) def pick_preferred_protocol(self, ws: WebSocket) -> Optional[str]: protocols = ws["subprotocols"] @@ -106,6 +101,3 @@ async def process_result( self, request: Request, result: ExecutionResult ) -> GraphQLHTTPResponse: return process_result(result) - - def encode_json(self, response_data: GraphQLHTTPResponse) -> str: - return json.dumps(response_data) diff --git a/strawberry/asgi/handlers/__init__.py b/strawberry/asgi/handlers/__init__.py index f97bc7f181..f1bc310646 100644 --- a/strawberry/asgi/handlers/__init__.py +++ b/strawberry/asgi/handlers/__init__.py @@ -4,4 +4,5 @@ from strawberry.asgi.handlers.graphql_ws_handler import GraphQLWSHandler from strawberry.asgi.handlers.http_handler import HTTPHandler + __all__ = ["GraphQLTransportWSHandler", "GraphQLWSHandler", "HTTPHandler"] diff --git a/strawberry/asgi/handlers/graphql_transport_ws_handler.py b/strawberry/asgi/handlers/graphql_transport_ws_handler.py index 617124e737..668391fa74 100644 --- a/strawberry/asgi/handlers/graphql_transport_ws_handler.py +++ b/strawberry/asgi/handlers/graphql_transport_ws_handler.py @@ -56,4 +56,3 @@ async def handle_request(self) -> None: for operation_id in list(self.subscriptions.keys()): await self.cleanup_operation(operation_id) - await self.reap_completed_tasks() diff --git a/strawberry/asgi/handlers/http_handler.py b/strawberry/asgi/handlers/http_handler.py index fc7a5c9014..d4bc0a2c59 100644 --- a/strawberry/asgi/handlers/http_handler.py +++ b/strawberry/asgi/handlers/http_handler.py @@ -1,19 +1,17 @@ import json -from typing import Any, Callable, Dict, Iterable, Optional +from typing import Any, Callable, Optional from starlette import status from starlette.requests import Request -from starlette.responses import HTMLResponse, PlainTextResponse, Response +from starlette.responses import HTMLResponse, JSONResponse, PlainTextResponse, Response from starlette.types import Receive, Scope, Send +from strawberry.asgi.utils import get_graphiql_html from strawberry.exceptions import MissingQueryError from strawberry.file_uploads.utils import replace_placeholders_with_files -from strawberry.http import parse_query_params, parse_request_data +from strawberry.http import parse_request_data from strawberry.schema import BaseSchema -from strawberry.schema.exceptions import InvalidOperationTypeError -from strawberry.types.graphql import OperationType from strawberry.utils.debug import pretty_print_graphql_operation -from strawberry.utils.graphiql import get_graphiql_html class HTTPHandler: @@ -21,29 +19,29 @@ def __init__( self, schema: BaseSchema, graphiql: bool, - allow_queries_via_get: bool, debug: bool, get_context, get_root_value, process_result, - encode_json, ): self.schema = schema self.graphiql = graphiql - self.allow_queries_via_get = allow_queries_via_get self.debug = debug self.get_context = get_context self.get_root_value = get_root_value self.process_result = process_result - self.encode_json = encode_json async def handle(self, scope: Scope, receive: Receive, send: Send): request = Request(scope=scope, receive=receive) root_value = await self.get_root_value(request) - sub_response = Response() - sub_response.status_code = None # type: ignore - del sub_response.headers["content-length"] + sub_response = Response( + content=None, + status_code=None, # type: ignore + headers=None, + media_type=None, + background=None, + ) context = await self.get_context(request=request, response=sub_response) @@ -51,6 +49,7 @@ async def handle(self, scope: Scope, receive: Receive, send: Send): request=request, execute=self.execute, process_result=self.process_result, + graphiql=self.graphiql, root_value=root_value, context=context, ) @@ -70,26 +69,17 @@ async def get_http_response( request: Request, execute: Callable, process_result: Callable, + graphiql: bool, root_value: Optional[Any], context: Optional[Any], ) -> Response: - method = request.method + if request.method == "GET": + if not graphiql: + return HTMLResponse(status_code=status.HTTP_404_NOT_FOUND) - if method == "GET": - if request.query_params: - try: - data = parse_query_params(request.query_params._dict) - except json.JSONDecodeError: - return PlainTextResponse( - "Unable to parse request body as JSON", - status_code=status.HTTP_400_BAD_REQUEST, - ) + return self.get_graphiql_response() - elif self.should_render_graphiql(request): - return self.get_graphiql_response() - else: - return HTMLResponse(status_code=status.HTTP_404_NOT_FOUND) - elif method == "POST": + if request.method == "POST": content_type = request.headers.get("Content-Type", "") if "application/json" in content_type: try: @@ -101,25 +91,13 @@ async def get_http_response( ) elif content_type.startswith("multipart/form-data"): multipart_data = await request.form() - try: - operations_text = multipart_data.get("operations", "{}") - operations = json.loads(operations_text) # type: ignore - files_map = json.loads(multipart_data.get("map", "{}")) # type: ignore # noqa: E501 - except json.JSONDecodeError: - return PlainTextResponse( - "Unable to parse request body as JSON", - status_code=status.HTTP_400_BAD_REQUEST, - ) + operations = json.loads(multipart_data.get("operations", "{}")) + files_map = json.loads(multipart_data.get("map", "{}")) + + data = replace_placeholders_with_files( + operations, files_map, multipart_data + ) - try: - data = replace_placeholders_with_files( - operations, files_map, multipart_data - ) - except KeyError: - return PlainTextResponse( - "File(s) missing in form data", - status_code=status.HTTP_400_BAD_REQUEST, - ) else: return PlainTextResponse( "Unsupported Media Type", @@ -133,53 +111,23 @@ async def get_http_response( try: request_data = parse_request_data(data) - except json.JSONDecodeError: - return PlainTextResponse( - "Unable to parse request body as JSON", - status_code=status.HTTP_400_BAD_REQUEST, - ) - - allowed_operation_types = OperationType.from_http(method) - - if not self.allow_queries_via_get and method == "GET": - allowed_operation_types = allowed_operation_types - {OperationType.QUERY} - - try: - result = await execute( - request_data.query, - variables=request_data.variables, - context=context, - operation_name=request_data.operation_name, - root_value=root_value, - allowed_operation_types=allowed_operation_types, - ) - except InvalidOperationTypeError as e: - return PlainTextResponse( - e.as_http_error_reason(method), - status_code=status.HTTP_400_BAD_REQUEST, - ) except MissingQueryError: return PlainTextResponse( "No GraphQL query found in the request", status_code=status.HTTP_400_BAD_REQUEST, ) - response_data = await process_result(request=request, result=result) - - return Response( - self.encode_json(response_data), - status_code=status.HTTP_200_OK, - media_type="application/json", + result = await execute( + request_data.query, + variables=request_data.variables, + context=context, + operation_name=request_data.operation_name, + root_value=root_value, ) - def should_render_graphiql(self, request: Request) -> bool: - if not self.graphiql: - return False + response_data = await process_result(request=request, result=result) - return any( - supported_header in request.headers.get("accept", "") - for supported_header in ("text/html", "*/*") - ) + return JSONResponse(response_data, status_code=status.HTTP_200_OK) def get_graphiql_response(self) -> HTMLResponse: html = get_graphiql_html() @@ -187,13 +135,7 @@ def get_graphiql_response(self) -> HTMLResponse: return HTMLResponse(html) async def execute( - self, - query: str, - variables: Optional[Dict[str, Any]] = None, - context: Any = None, - operation_name: Optional[str] = None, - root_value: Any = None, - allowed_operation_types: Optional[Iterable[OperationType]] = None, + self, query, variables=None, context=None, operation_name=None, root_value=None ): if self.debug: pretty_print_graphql_operation(operation_name, query, variables) @@ -204,5 +146,4 @@ async def execute( variable_values=variables, operation_name=operation_name, context_value=context, - allowed_operation_types=allowed_operation_types, ) diff --git a/strawberry/asgi/test/__init__.py b/strawberry/asgi/test/__init__.py index 47b4c12cc0..d9954b9f51 100644 --- a/strawberry/asgi/test/__init__.py +++ b/strawberry/asgi/test/__init__.py @@ -1,3 +1,4 @@ from .client import GraphQLTestClient + __all__ = ["GraphQLTestClient"] diff --git a/strawberry/asgi/test/client.py b/strawberry/asgi/test/client.py index 3fc7760727..e83fdc32b9 100644 --- a/strawberry/asgi/test/client.py +++ b/strawberry/asgi/test/client.py @@ -1,5 +1,6 @@ import json from typing import Dict, Mapping, Optional + from typing_extensions import Literal from strawberry.test import BaseGraphQLTestClient @@ -36,7 +37,7 @@ def request( files: Optional[Dict[str, object]] = None, ): return self._client.post( - self.url, + "/graphql/", json=body if not files else None, data=body if files else None, files=files, diff --git a/strawberry/asgi/utils.py b/strawberry/asgi/utils.py new file mode 100644 index 0000000000..16a0478e8b --- /dev/null +++ b/strawberry/asgi/utils.py @@ -0,0 +1,11 @@ +import pathlib + + +def get_graphiql_html() -> str: + here = pathlib.Path(__file__).parents[1] + path = here / "static/graphiql.html" + + with open(path) as f: + template = f.read() + + return template.replace("{{ SUBSCRIPTION_ENABLED }}", "true") diff --git a/strawberry/auto.py b/strawberry/auto.py index f4b6f63a3a..179258d612 100644 --- a/strawberry/auto.py +++ b/strawberry/auto.py @@ -1,79 +1,4 @@ -from __future__ import annotations +import sentinel -from typing import Any, Optional, Union, cast -from typing_extensions import Annotated, get_args, get_origin -from strawberry.type import StrawberryType - -from .annotation import StrawberryAnnotation - - -class StrawberryAutoMeta(type): - """Metaclass for StrawberryAuto. - - This is used to make sure StrawberryAuto is a singleton and also to - override the behavior of `isinstance` so that it consider the following - cases: - - >> isinstance(StrawberryAuto(), StrawberryAuto) - True - >> isinstance(StrawberryAnnotation(StrawberryAuto()), StrawberryAuto) - True - >> isinstance(Annotated[StrawberryAuto(), object()), StrawberryAuto) - True - - """ - - def __init__(self, *args, **kwargs): - self._instance: Optional[StrawberryAuto] = None - super().__init__(*args, **kwargs) - - def __call__(cls, *args, **kwargs): - if cls._instance is None: - cls._instance = super().__call__(*args, **kwargs) - - return cls._instance - - def __instancecheck__( - self, - instance: Union[StrawberryAuto, StrawberryAnnotation, StrawberryType, type], - ): - if isinstance(instance, StrawberryAnnotation): - resolved = instance.annotation - if isinstance(resolved, str): - namespace = instance.namespace - resolved = namespace and namespace.get(resolved) - - if resolved is not None: - instance = cast(type, resolved) - - if instance is auto: - return True - - # Support uses of Annotated[auto, something()] - if get_origin(instance) is Annotated: - args = get_args(instance) - if args[0] is Any: - return any(isinstance(arg, StrawberryAuto) for arg in args[1:]) - - # StrawberryType's `__eq__` tries to find the string passed in the global - # namespace, which will fail with a `NameError` if "strawberry.auto" hasn't - # been imported. So we can't use `instance == "strawberry.auto"` here. - # Instead, we'll use `isinstance(instance, str)` to check if the instance - # is a StrawberryType, in that case we can return False since we know it - # won't be a StrawberryAuto. - if isinstance(instance, StrawberryType): - return False - - return instance == "strawberry.auto" - - -class StrawberryAuto(metaclass=StrawberryAutoMeta): - def __str__(self): - return "auto" - - def __repr__(self): - return "" - - -auto = Annotated[Any, StrawberryAuto()] +auto = sentinel.create("auto") diff --git a/strawberry/chalice/graphiql.py b/strawberry/chalice/graphiql.py new file mode 100644 index 0000000000..ac6be9627c --- /dev/null +++ b/strawberry/chalice/graphiql.py @@ -0,0 +1,18 @@ +import functools +from pathlib import Path + + +@functools.lru_cache() +def render_graphiql_page() -> str: + """ + Loads the graphiql html file into a string and returns it. Replacing subscription + enabled as false, this is because this chalice integration does not currently support + subscriptions. This function returns a static result, so cache it in ram, saving us + from loading the file from disk each time. + Returns: + A cached string containing a static graphiql page. + """ + graphiql_path = Path(__file__).parent.parent / "static/graphiql.html" + html_string = graphiql_path.read_text() + + return html_string.replace("{{ SUBSCRIPTION_ENABLED }}", "false") diff --git a/strawberry/chalice/views.py b/strawberry/chalice/views.py index 4f706ddfd2..7033628a48 100644 --- a/strawberry/chalice/views.py +++ b/strawberry/chalice/views.py @@ -1,61 +1,30 @@ -import json -import warnings -from typing import Dict, Mapping, Optional - -from chalice.app import BadRequestError, Request, Response -from strawberry.exceptions import MissingQueryError -from strawberry.http import ( - GraphQLHTTPResponse, - parse_query_params, - parse_request_data, - process_result, -) -from strawberry.http.temporal_response import TemporalResponse +from http import HTTPStatus + +from chalice.app import BadRequestError, CaseInsensitiveMapping, Request, Response +from strawberry.chalice.graphiql import render_graphiql_page +from strawberry.http import GraphQLHTTPResponse, process_result from strawberry.schema import BaseSchema -from strawberry.schema.exceptions import InvalidOperationTypeError from strawberry.types import ExecutionResult -from strawberry.types.graphql import OperationType -from strawberry.utils.graphiql import get_graphiql_html class GraphQLView: - def __init__( - self, - schema: BaseSchema, - graphiql: bool = True, - allow_queries_via_get: bool = True, - **kwargs - ): - if "render_graphiql" in kwargs: - self.graphiql = kwargs.pop("render_graphiql") - warnings.warn( - "The `render_graphiql` argument is deprecated. " - "Use `graphiql` instead.", - DeprecationWarning, - ) - else: - self.graphiql = graphiql - - self.allow_queries_via_get = allow_queries_via_get + def __init__(self, schema: BaseSchema, render_graphiql: bool = True): self._schema = schema - - def get_root_value(self, request: Request) -> Optional[object]: - return None + self.graphiql = render_graphiql @staticmethod def render_graphiql() -> str: """ - Returns a string containing the html for the graphiql webpage. It also caches - the result using lru cache. - This saves loading from disk each time it is invoked. - + Returns a string containing the html for the graphiql webpage. It also caches the + result using lru cache. This saves loading from disk each time it is invoked. Returns: The GraphiQL html page as a string """ - return get_graphiql_html(subscription_enabled=False) + result = render_graphiql_page() + return result @staticmethod - def should_render_graphiql(graphiql: bool, request: Request) -> bool: + def has_html_been_asked_for(headers: CaseInsensitiveMapping) -> bool: """ Do the headers indicate that the invoker has requested html? Args: @@ -64,39 +33,44 @@ def should_render_graphiql(graphiql: bool, request: Request) -> bool: Returns: Whether html has been requested True for yes, False for no """ - if not graphiql: + accept_headers = headers.get("accept", None) + + if accept_headers is None: return False - return any( - supported_header in request.headers.get("accept", "") - for supported_header in {"text/html", "*/*"} - ) + if "text/html" in accept_headers: + return True + + if "*/*" in accept_headers: + return True + + return False @staticmethod - def error_response( - message: str, - error_code: str, - http_status_code: int, - headers: Optional[Dict[str, str]] = None, - ) -> Response: + def invalid_query_response() -> Response: """ - A wrapper for error responses + A response for malformed queries Returns: An errors response """ - body = {"Code": error_code, "Message": message} - - return Response(body=body, status_code=http_status_code, headers=headers) + return Response( + body={ + "errors": ["Provide a valid graphql query in the body of your request"] + }, + status_code=HTTPStatus.OK, + ) - def get_context( - self, request: Request, response: TemporalResponse - ) -> Mapping[str, object]: - return {"request": request, "response": response} + @staticmethod + def invalid_rest_verb_response() -> Response: + """ + A response for calling the graphql endpoint with a non POST request + Returns: - def process_result( - self, request: Request, result: ExecutionResult - ) -> GraphQLHTTPResponse: - return process_result(result) + """ + return Response( + body={"errors": ["GraphQL queries must be of request type POST"]}, + status_code=HTTPStatus.OK, + ) def execute_request(self, request: Request) -> Response: """ @@ -107,100 +81,44 @@ def execute_request(self, request: Request) -> Response: Returns: A chalice response """ - - method = request.method - - if method not in {"POST", "GET"}: - return self.error_response( - error_code="MethodNotAllowedError", - message="Unsupported method, must be of request type POST or GET", - http_status_code=405, - ) - content_type = request.headers.get("content-type", "") - - if "application/json" in content_type: - try: - data = request.json_body - if not (isinstance(data, dict)): - return self.error_response( - error_code="BadRequestError", - message=( - "Provide a valid graphql query " - "in the body of your request" - ), - http_status_code=400, - ) - except BadRequestError: - return self.error_response( - error_code="BadRequestError", - message="Unable to parse request body as JSON", - http_status_code=400, - ) - elif method == "GET" and request.query_params: - try: - data = parse_query_params(request.query_params) # type: ignore - except json.JSONDecodeError: - return self.error_response( - error_code="BadRequestError", - message="Unable to parse request body as JSON", - http_status_code=400, + if self.graphiql: + if ( + self.has_html_been_asked_for(request.headers) + and request.method == "GET" + ): + graphiql_page: str = self.render_graphiql() + return Response( + body=graphiql_page, + headers={"content-type": "text/html"}, + status_code=200, ) - elif method == "GET" and self.should_render_graphiql(self.graphiql, request): - return Response( - body=self.render_graphiql(), - headers={"content-type": "text/html"}, - status_code=200, - ) - - else: - return self.error_response( - error_code="NotFoundError", - message="Not found", - http_status_code=404, - ) + if not request.method == "POST": + return self.invalid_rest_verb_response() - request_data = parse_request_data(data) - - allowed_operation_types = OperationType.from_http(method) + try: + request_data = request.json_body + except BadRequestError: + return self.invalid_query_response() - if not self.allow_queries_via_get and method == "GET": - allowed_operation_types = allowed_operation_types - {OperationType.QUERY} + if request_data is None: + return self.invalid_query_response() + try: + query = request_data["query"] + variables = request_data.get("variables") + operation_name = request_data.get("operationName") + + except (KeyError, TypeError): + return self.invalid_query_response() + + result: ExecutionResult = self._schema.execute_sync( + query, + variable_values=variables, + context_value=request, + operation_name=operation_name, + root_value=None, + ) - context = self.get_context(request, response=TemporalResponse()) + http_result: GraphQLHTTPResponse = process_result(result) - try: - result: ExecutionResult = self._schema.execute_sync( - request_data.query, - variable_values=request_data.variables, - context_value=context, - operation_name=request_data.operation_name, - root_value=self.get_root_value(request), - allowed_operation_types=allowed_operation_types, - ) - - except InvalidOperationTypeError as e: - return self.error_response( - error_code="BadRequestError", - message=e.as_http_error_reason(method), - http_status_code=400, - ) - except MissingQueryError: - return self.error_response( - error_code="BadRequestError", - message="No GraphQL query found in the request", - http_status_code=400, - ) - - http_result: GraphQLHTTPResponse = self.process_result(request, result) - - status_code = 200 - - if "response" in context: - # TODO: we might want to use typed dict for context - status_code = context["response"].status_code # type: ignore[attr-defined] - - return Response(body=self.encode_json(http_result), status_code=status_code) - - def encode_json(self, response_data: GraphQLHTTPResponse) -> str: - return json.dumps(response_data) + return Response(body=http_result) diff --git a/strawberry/channels/__init__.py b/strawberry/channels/__init__.py deleted file mode 100644 index bad0e48144..0000000000 --- a/strawberry/channels/__init__.py +++ /dev/null @@ -1,15 +0,0 @@ -from .context import StrawberryChannelsContext -from .handlers.graphql_transport_ws_handler import GraphQLTransportWSHandler -from .handlers.graphql_ws_handler import GraphQLWSHandler -from .handlers.http_handler import GraphQLHTTPConsumer -from .handlers.ws_handler import GraphQLWSConsumer -from .router import GraphQLProtocolTypeRouter - -__all__ = [ - "GraphQLProtocolTypeRouter", - "GraphQLWSHandler", - "GraphQLTransportWSHandler", - "GraphQLHTTPConsumer", - "GraphQLWSConsumer", - "StrawberryChannelsContext", -] diff --git a/strawberry/channels/context.py b/strawberry/channels/context.py deleted file mode 100644 index df376ef857..0000000000 --- a/strawberry/channels/context.py +++ /dev/null @@ -1,19 +0,0 @@ -from dataclasses import dataclass -from typing import TYPE_CHECKING, Any, Dict, Optional - -if TYPE_CHECKING: - from strawberry.channels.handlers.base import ChannelsConsumer - - -@dataclass -class StrawberryChannelsContext: - """ - A Channels context for GraphQL - """ - - request: "ChannelsConsumer" - connection_params: Optional[Dict[str, Any]] = None - - @property - def ws(self): - return self.request diff --git a/strawberry/channels/handlers/base.py b/strawberry/channels/handlers/base.py deleted file mode 100644 index 16e3854bd2..0000000000 --- a/strawberry/channels/handlers/base.py +++ /dev/null @@ -1,161 +0,0 @@ -import asyncio -import contextlib -from collections import defaultdict -from typing import ( - Any, - AsyncGenerator, - Awaitable, - Callable, - DefaultDict, - Dict, - List, - Optional, - Sequence, -) -from typing_extensions import Literal, Protocol, TypedDict - -from channels.consumer import AsyncConsumer -from channels.generic.websocket import AsyncJsonWebsocketConsumer -from strawberry.channels.context import StrawberryChannelsContext - - -class ChannelsMessage(TypedDict, total=False): - type: str - - -class ChannelsLayer(Protocol): # pragma: no cover - """Channels layer spec. - - Based on: https://channels.readthedocs.io/en/stable/channel_layer_spec.html - """ - - # Default channels API - - extensions: List[Literal["groups", "flush"]] - - async def send(self, channel: str, message: dict) -> None: - ... - - async def receive(self, channel: str) -> dict: - ... - - async def new_channel(self, prefix: str = ...) -> str: - ... - - # If groups extension is supported - - group_expiry: int - - async def group_add(self, group: str, channel: str) -> None: - ... - - async def group_discard(self, group: str, channel: str) -> None: - ... - - async def group_send(self, group: str, message: dict) -> None: - ... - - # If flush extension is supported - - async def flush(self) -> None: - ... - - -class ChannelsConsumer(AsyncConsumer): - """Base channels async consumer.""" - - channel_name: str - channel_layer: Optional[ChannelsLayer] - channel_receive: Callable[[], Awaitable[dict]] - - def __init__(self, *args, **kwargs): - self.listen_queues: DefaultDict[str, asyncio.Queue] = defaultdict(asyncio.Queue) - super().__init__(*args, **kwargs) - - @property - def headers(self) -> Dict[str, str]: - return { - header_name.decode().lower(): header_value.decode() - for header_name, header_value in self.scope["headers"] - } - - async def get_root_value(self, request: Optional["ChannelsConsumer"] = None) -> Any: - return None - - async def get_context( - self, - request: Optional["ChannelsConsumer"] = None, - connection_params: Optional[Dict[str, Any]] = None, - ) -> StrawberryChannelsContext: - return StrawberryChannelsContext( - request=request or self, connection_params=connection_params - ) - - async def dispatch(self, message: ChannelsMessage): - # AsyncConsumer will try to get a function for message["type"] to handle - # for both http/websocket types and also for layers communication. - # In case the type isn't one of those, pass it to the listen queue so - # that it can be consumed by self.channel_listen - type_ = message.get("type", "") - if type_ and not type_.startswith(("http.", "websocket.")): - self.listen_queues[type_].put_nowait(message) - return - - await super().dispatch(message) - - async def channel_listen( - self, - type: str, - *, - timeout: Optional[float] = None, - groups: Sequence[str] = (), - ) -> AsyncGenerator[Any, None]: - """Listen for messages sent to this consumer. - - Utility to listen for channels messages for this consumer inside - a resolver (usually inside a subscription). - - Parameters: - type: - The type of the message to wait for. - timeout: - An optional timeout to wait for each subsequent message - groups: - An optional sequence of groups to receive messages from. - When passing this parameter, the groups will be registered - using `self.channel_layer.group_add` at the beggining of the - execution and then discarded using `self.channel_layer.group_discard` - at the end of the execution. - - """ - if self.channel_layer is None: - raise RuntimeError( - "Layers integration is required listening for channels.\n" - "Check https://channels.readthedocs.io/en/stable/topics/channel_layers.html " # noqa:E501 - "for more information" - ) - - added_groups = [] - try: - for group in groups: - await self.channel_layer.group_add(group, self.channel_name) - added_groups.append(group) - - queue = self.listen_queues[type] - while True: - awaitable = queue.get() - if timeout is not None: - awaitable = asyncio.wait_for(awaitable, timeout) - try: - yield await awaitable - except asyncio.TimeoutError: - # TODO: shall we add log here and maybe in the suppress below? - return - finally: - for group in added_groups: - with contextlib.suppress(Exception): - await self.channel_layer.group_discard(group, self.channel_name) - - -class ChannelsWSConsumer(ChannelsConsumer, AsyncJsonWebsocketConsumer): - """Base channels websocket async consumer.""" diff --git a/strawberry/channels/handlers/graphql_transport_ws_handler.py b/strawberry/channels/handlers/graphql_transport_ws_handler.py deleted file mode 100644 index 02fbcb8b05..0000000000 --- a/strawberry/channels/handlers/graphql_transport_ws_handler.py +++ /dev/null @@ -1,56 +0,0 @@ -from datetime import timedelta -from typing import Any, Optional - -from strawberry.channels.handlers.base import ChannelsWSConsumer -from strawberry.schema import BaseSchema -from strawberry.subscriptions import GRAPHQL_TRANSPORT_WS_PROTOCOL -from strawberry.subscriptions.protocols.graphql_transport_ws.handlers import ( - BaseGraphQLTransportWSHandler, -) - - -class GraphQLTransportWSHandler(BaseGraphQLTransportWSHandler): - def __init__( - self, - schema: BaseSchema, - debug: bool, - connection_init_wait_timeout: timedelta, - get_context, - get_root_value, - ws: ChannelsWSConsumer, - ): - super().__init__(schema, debug, connection_init_wait_timeout) - self._get_context = get_context - self._get_root_value = get_root_value - self._ws = ws - - async def get_context(self) -> Any: - return await self._get_context( - request=self._ws, connection_params=self.connection_params - ) - - async def get_root_value(self) -> Any: - return await self._get_root_value(request=self._ws) - - async def send_json(self, data: dict) -> None: - await self._ws.send_json(data) - - async def close(self, code: int = 1000, reason: Optional[str] = None) -> None: - # FIXME: We are using `self._ws.base_send` directly instead of `self._ws.close` - # because the later doesn't accept the `reason` argument. - await self._ws.base_send( - { - "type": "websocket.close", - "code": code, - "reason": reason or "", - } - ) - - async def handle_request(self) -> Any: - await self._ws.accept(subprotocol=GRAPHQL_TRANSPORT_WS_PROTOCOL) - - async def handle_disconnect(self, code): - for operation_id in list(self.subscriptions.keys()): - await self.cleanup_operation(operation_id) - - await self.reap_completed_tasks() diff --git a/strawberry/channels/handlers/graphql_ws_handler.py b/strawberry/channels/handlers/graphql_ws_handler.py deleted file mode 100644 index f0183b5ada..0000000000 --- a/strawberry/channels/handlers/graphql_ws_handler.py +++ /dev/null @@ -1,58 +0,0 @@ -from contextlib import suppress -from typing import Any, Optional - -from strawberry.channels.handlers.base import ChannelsWSConsumer -from strawberry.schema import BaseSchema -from strawberry.subscriptions import GRAPHQL_WS_PROTOCOL -from strawberry.subscriptions.protocols.graphql_ws.handlers import BaseGraphQLWSHandler -from strawberry.subscriptions.protocols.graphql_ws.types import OperationMessage - - -class GraphQLWSHandler(BaseGraphQLWSHandler): - def __init__( - self, - schema: BaseSchema, - debug: bool, - keep_alive: bool, - keep_alive_interval: float, - get_context, - get_root_value, - ws: ChannelsWSConsumer, - ): - super().__init__(schema, debug, keep_alive, keep_alive_interval) - self._get_context = get_context - self._get_root_value = get_root_value - self._ws = ws - - async def get_context(self) -> Any: - return await self._get_context( - request=self._ws, connection_params=self.connection_params - ) - - async def get_root_value(self) -> Any: - return await self._get_root_value(request=self._ws) - - async def send_json(self, data: OperationMessage) -> None: - await self._ws.send_json(data) - - async def close(self, code: int = 1000, reason: Optional[str] = None) -> None: - # Close messages are not part of the ASGI ref yet - await self._ws.close(code=code) - - async def handle_request(self) -> Any: - await self._ws.accept(subprotocol=GRAPHQL_WS_PROTOCOL) - - async def handle_disconnect(self, code): - if self.keep_alive_task: - self.keep_alive_task.cancel() - with suppress(BaseException): - await self.keep_alive_task - - for operation_id in list(self.subscriptions.keys()): - await self.cleanup_operation(operation_id) - - async def handle_invalid_message(self, error_message: str) -> None: - # This is not part of the BaseGraphQLWSHandler's interface, but the - # channels integration is a high level wrapper that forwards this to - # both us and the BaseGraphQLTransportWSHandler. - pass diff --git a/strawberry/channels/handlers/http_handler.py b/strawberry/channels/handlers/http_handler.py deleted file mode 100644 index f45517897f..0000000000 --- a/strawberry/channels/handlers/http_handler.py +++ /dev/null @@ -1,234 +0,0 @@ -"""GraphQLHTTPHandler - -A consumer to provide a graphql endpoint, and optionally graphiql. -""" - -import dataclasses -import json -from typing import Any, Optional -from urllib.parse import parse_qs - -from channels.db import database_sync_to_async -from channels.generic.http import AsyncHttpConsumer -from strawberry.channels.context import StrawberryChannelsContext -from strawberry.exceptions import MissingQueryError -from strawberry.http import ( - GraphQLHTTPResponse, - GraphQLRequestData, - parse_query_params, - parse_request_data, - process_result, -) -from strawberry.schema import BaseSchema -from strawberry.schema.exceptions import InvalidOperationTypeError -from strawberry.types import ExecutionResult -from strawberry.types.graphql import OperationType -from strawberry.utils.graphiql import get_graphiql_html - -from .base import ChannelsConsumer - - -class MethodNotAllowed(Exception): - ... - - -class ExecutionError(Exception): - ... - - -@dataclasses.dataclass -class Result: - response: bytes - status: int = 200 - content_type: str = "application/json" - - -class GraphQLHTTPConsumer(ChannelsConsumer, AsyncHttpConsumer): - """A consumer to provide a view for GraphQL over HTTP. - - To use this, place it in your ProtocolTypeRouter for your channels project: - - ``` - from strawberry.channels import GraphQLHttpRouter - from channels.routing import ProtocolTypeRouter - from django.core.asgi import get_asgi_application - - application = ProtocolTypeRouter({ - "http": URLRouter([ - re_path("^graphql", GraphQLHTTPRouter(schema=schema)), - re_path("^", get_asgi_application()), - ]), - "websocket": URLRouter([ - re_path("^ws/graphql", GraphQLWebSocketRouter(schema=schema)), - ]), - }) - ``` - """ - - def __init__( - self, - schema: BaseSchema, - graphiql: bool = True, - allow_queries_via_get: bool = True, - subscriptions_enabled: bool = True, - **kwargs, - ): - self.schema = schema - self.graphiql = graphiql - self.allow_queries_via_get = allow_queries_via_get - self.subscriptions_enabled = subscriptions_enabled - super().__init__(**kwargs) - - async def handle(self, body: bytes): - try: - if self.scope["method"] == "GET": - result = await self.get(body) - elif self.scope["method"] == "POST": - result = await self.post(body) - else: - raise MethodNotAllowed() - except MethodNotAllowed: - await self.send_response( - 405, - b"Method not allowed", - headers=[(b"Allow", b"GET, POST")], - ) - except InvalidOperationTypeError as e: - error_str = e.as_http_error_reason(self.scope["method"]) - await self.send_response( - 406, - error_str.encode(), - ) - except ExecutionError as e: - await self.send_response( - 500, - str(e).encode(), - ) - else: - await self.send_response( - result.status, - result.response, - headers=[(b"Content-Type", result.content_type.encode())], - ) - - async def get(self, body: bytes) -> Result: - if self.should_render_graphiql(): - return await self.render_graphiql(body) - elif self.scope.get("query_string"): - params = parse_query_params( - { - k: v[0] - for k, v in parse_qs(self.scope["query_string"].decode()).items() - } - ) - - try: - result = await self.execute(parse_request_data(params)) - except MissingQueryError as e: - raise ExecutionError("No GraphQL query found in the request") from e - - return Result(response=json.dumps(result).encode()) - else: - raise MethodNotAllowed() - - async def post(self, body: bytes) -> Result: - request_data = await self.parse_body(body) - - try: - result = await self.execute(request_data) - except MissingQueryError as e: - raise ExecutionError("No GraphQL query found in the request") from e - - return Result(response=json.dumps(result).encode()) - - async def parse_body(self, body: bytes) -> GraphQLRequestData: - if self.headers.get("content-type", "").startswith("multipart/form-data"): - return await self.parse_multipart_body(body) - - try: - data = json.loads(body) - except json.JSONDecodeError as e: - raise ExecutionError("Unable to parse request body as JSON") from e - - return parse_request_data(data) - - async def parse_multipart_body(self, body: bytes) -> GraphQLRequestData: - raise ExecutionError("Unable to parse the multipart body") - - async def execute(self, request_data: GraphQLRequestData): - context = await self.get_context() - root_value = await self.get_root_value() - - method = self.scope["method"] - allowed_operation_types = OperationType.from_http(method) - if not self.allow_queries_via_get and method == "GET": - allowed_operation_types = allowed_operation_types - {OperationType.QUERY} - - result = await self.schema.execute( - query=request_data.query, - root_value=root_value, - variable_values=request_data.variables, - context_value=context, - operation_name=request_data.operation_name, - allowed_operation_types=allowed_operation_types, - ) - return await self.process_result(result) - - async def process_result(self, result: ExecutionResult) -> GraphQLHTTPResponse: - return process_result(result) - - async def render_graphiql(self, body): - html = get_graphiql_html(self.subscriptions_enabled) - return Result(response=html.encode(), content_type="text/html") - - def should_render_graphiql(self): - accept_list = self.headers.get("accept", "").split(",") - return self.graphiql and any( - accepted in accept_list for accepted in ["text/html", "*/*"] - ) - - -class SyncGraphQLHTTPConsumer(GraphQLHTTPConsumer): - """Synchronous version of the HTTPConsumer. - - This is the same as `GraphQLHTTPConsumer`, but it can be used with - synchronous schemas (i.e. the schema's resolvers are espected to be - synchronous and not asynchronous). - """ - - def get_root_value(self, request: Optional["ChannelsConsumer"] = None) -> Any: - return None - - def get_context( # type: ignore[override] - self, - request: Optional["ChannelsConsumer"] = None, - ) -> StrawberryChannelsContext: - return StrawberryChannelsContext(request=request or self) - - def process_result( # type:ignore [override] - self, result: ExecutionResult - ) -> GraphQLHTTPResponse: - return process_result(result) - - # Sync channels is actually async, but it uses database_sync_to_async to call - # handlers in a threadpool. Check SyncConsumer's documentation for more info: - # https://github.com/django/channels/blob/main/channels/consumer.py#L104 - @database_sync_to_async - def execute(self, request_data: GraphQLRequestData): - context = self.get_context(self) - root_value = self.get_root_value(self) - - method = self.scope["method"] - allowed_operation_types = OperationType.from_http(method) - if not self.allow_queries_via_get and method == "GET": - allowed_operation_types = allowed_operation_types - {OperationType.QUERY} - - result = self.schema.execute_sync( - query=request_data.query, - root_value=root_value, - variable_values=request_data.variables, - context_value=context, - operation_name=request_data.operation_name, - allowed_operation_types=allowed_operation_types, - ) - return self.process_result(result) diff --git a/strawberry/channels/handlers/ws_handler.py b/strawberry/channels/handlers/ws_handler.py deleted file mode 100644 index 5fda900efd..0000000000 --- a/strawberry/channels/handlers/ws_handler.py +++ /dev/null @@ -1,108 +0,0 @@ -import datetime -from typing import Optional, Sequence, Union - -from strawberry.schema import BaseSchema -from strawberry.subscriptions import GRAPHQL_TRANSPORT_WS_PROTOCOL, GRAPHQL_WS_PROTOCOL - -from .base import ChannelsWSConsumer -from .graphql_transport_ws_handler import GraphQLTransportWSHandler -from .graphql_ws_handler import GraphQLWSHandler - - -class GraphQLWSConsumer(ChannelsWSConsumer): - """A channels websocket consumer for GraphQL - - This handles the connections, then hands off to the appropriate - handler based on the subprotocol. - - To use this, place it in your ProtocolTypeRouter for your channels project, e.g: - - ``` - from strawberry.channels import GraphQLHttpRouter - from channels.routing import ProtocolTypeRouter - from django.core.asgi import get_asgi_application - - application = ProtocolTypeRouter({ - "http": URLRouter([ - re_path("^graphql", GraphQLHTTPRouter(schema=schema)), - re_path("^", get_asgi_application()), - ]), - "websocket": URLRouter([ - re_path("^ws/graphql", GraphQLWebSocketRouter(schema=schema)), - ]), - }) - ``` - """ - - graphql_transport_ws_handler_class = GraphQLTransportWSHandler - graphql_ws_handler_class = GraphQLWSHandler - _handler: Union[GraphQLWSHandler, GraphQLTransportWSHandler] - - def __init__( - self, - schema: BaseSchema, - keep_alive: bool = False, - keep_alive_interval: float = 1, - debug: bool = False, - subscription_protocols=(GRAPHQL_TRANSPORT_WS_PROTOCOL, GRAPHQL_WS_PROTOCOL), - connection_init_wait_timeout: Optional[datetime.timedelta] = None, - ): - if connection_init_wait_timeout is None: - connection_init_wait_timeout = datetime.timedelta(minutes=1) - self.connection_init_wait_timeout = connection_init_wait_timeout - self.schema = schema - self.keep_alive = keep_alive - self.keep_alive_interval = keep_alive_interval - self.debug = debug - self.protocols = subscription_protocols - - super().__init__() - - def pick_preferred_protocol( - self, accepted_subprotocols: Sequence[str] - ) -> Optional[str]: - intersection = set(accepted_subprotocols) & set(self.protocols) - sorted_intersection = sorted(intersection, key=accepted_subprotocols.index) - return next(iter(sorted_intersection), None) - - async def connect(self): - preferred_protocol = self.pick_preferred_protocol(self.scope["subprotocols"]) - - if preferred_protocol == GRAPHQL_TRANSPORT_WS_PROTOCOL: - self._handler = self.graphql_transport_ws_handler_class( - schema=self.schema, - debug=self.debug, - connection_init_wait_timeout=self.connection_init_wait_timeout, - get_context=self.get_context, - get_root_value=self.get_root_value, - ws=self, - ) - elif preferred_protocol == GRAPHQL_WS_PROTOCOL: - self._handler = self.graphql_ws_handler_class( - schema=self.schema, - debug=self.debug, - keep_alive=self.keep_alive, - keep_alive_interval=self.keep_alive_interval, - get_context=self.get_context, - get_root_value=self.get_root_value, - ws=self, - ) - else: - # Subprotocol not acceptable - return await self.close(code=4406) - - await self._handler.handle() - return None - - async def receive(self, *args, **kwargs): - # Overriding this so that we can pass the errors to handle_invalid_message - try: - await super().receive(*args, **kwargs) - except ValueError as e: - await self._handler.handle_invalid_message(str(e)) - - async def receive_json(self, content, **kwargs): - await self._handler.handle_message(content) - - async def disconnect(self, code): - await self._handler.handle_disconnect(code) diff --git a/strawberry/channels/router.py b/strawberry/channels/router.py deleted file mode 100644 index a31e14e6fd..0000000000 --- a/strawberry/channels/router.py +++ /dev/null @@ -1,59 +0,0 @@ -"""GraphQLWebSocketRouter - -This is a simple router class that might be better placed as part of Channels itself. -It's a simple "SubProtocolRouter" that selects the websocket subprotocol based -on preferences and client support. Then it hands off to the appropriate consumer. -""" - -from django.urls import re_path - -from channels.routing import ProtocolTypeRouter, URLRouter -from strawberry.schema import BaseSchema - -from .handlers.http_handler import GraphQLHTTPConsumer -from .handlers.ws_handler import GraphQLWSConsumer - - -class GraphQLProtocolTypeRouter(ProtocolTypeRouter): - """ - Convenience class to set up GraphQL on both HTTP and Websocket, optionally with a - Django application for all other HTTP routes: - - ``` - from strawberry.channels import GraphQLProtocolTypeRouter - from django.core.asgi import get_asgi_application - - django_asgi = get_asgi_application() - - from myapi import schema - - application = GraphQLProtocolTypeRouter( - 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=None, - url_pattern="^graphql", - ): - http_urls = [re_path(url_pattern, GraphQLHTTPConsumer.as_asgi(schema=schema))] - if django_application is not None: - http_urls.append(re_path("^", django_application)) - - super().__init__( - { - "http": URLRouter(http_urls), - "websocket": URLRouter( - [ - re_path(url_pattern, GraphQLWSConsumer.as_asgi(schema=schema)), - ] - ), - } - ) diff --git a/strawberry/cli/__init__.py b/strawberry/cli/__init__.py index 0bc46d35f2..688e04b067 100644 --- a/strawberry/cli/__init__.py +++ b/strawberry/cli/__init__.py @@ -1,6 +1,5 @@ import click -from .commands.codegen import codegen as cmd_codegen from .commands.export_schema import export_schema as cmd_export_schema from .commands.server import server as cmd_server @@ -12,4 +11,3 @@ def run(): # pragma: no cover run.add_command(cmd_server) run.add_command(cmd_export_schema) -run.add_command(cmd_codegen) diff --git a/strawberry/cli/commands/codegen.py b/strawberry/cli/commands/codegen.py deleted file mode 100644 index 4c1e852ef2..0000000000 --- a/strawberry/cli/commands/codegen.py +++ /dev/null @@ -1,155 +0,0 @@ -import importlib -import inspect -from pathlib import Path -from typing import List, Optional, Type - -import click - -from strawberry.cli.utils import load_schema -from strawberry.codegen import CodegenResult, QueryCodegen, QueryCodegenPlugin - - -def _is_codegen_plugin(obj: object) -> bool: - return ( - inspect.isclass(obj) - and issubclass(obj, QueryCodegenPlugin) - and obj is not QueryCodegenPlugin - ) - - -def _import_plugin(plugin: str) -> Optional[Type[QueryCodegenPlugin]]: - module_name = plugin - symbol_name: Optional[str] = None - - if ":" in plugin: - module_name, symbol_name = plugin.split(":", 1) - - try: - module = importlib.import_module(module_name) - except ModuleNotFoundError: - return None - - if symbol_name: - obj = getattr(module, symbol_name) - - assert _is_codegen_plugin(obj) - return obj - else: - symbols = { - key: value - for key, value in module.__dict__.items() - if not key.startswith("__") - } - - if "__all__" in module.__dict__: - symbols = { - name: symbol - for name, symbol in symbols.items() - if name in module.__dict__["__all__"] - } - - for obj in symbols.values(): - if _is_codegen_plugin(obj): - return obj - - return None - - -def _load_plugin(plugin_path: str) -> Type[QueryCodegenPlugin]: - # try to import plugin_name from current folder - # then try to import from strawberry.codegen.plugins - - plugin = _import_plugin(plugin_path) - - if plugin is None and "." not in plugin_path: - plugin = _import_plugin(f"strawberry.codegen.plugins.{plugin_path}") - - if plugin is None: - raise click.ClickException(f"Plugin {plugin_path} not found") - - return plugin - - -def _load_plugins(plugins: List[str]) -> List[QueryCodegenPlugin]: - return [_load_plugin(plugin)() for plugin in plugins] - - -class ConsolePlugin(QueryCodegenPlugin): - def __init__( - self, query: Path, output_dir: Path, plugins: List[QueryCodegenPlugin] - ): - self.query = query - self.output_dir = output_dir - self.plugins = plugins - - def on_start(self): - click.echo( - click.style( - "The codegen is experimental. Please submit any bug at " - "https://github.com/strawberry-graphql/strawberry\n", - fg="yellow", - bold=True, - ) - ) - - plugin_names = [plugin.__class__.__name__ for plugin in self.plugins] - - click.echo( - click.style( - f"Generating code for {self.query} using " - f"{', '.join(plugin_names)} plugin(s)", - fg="green", - ) - ) - - def on_end(self, result: CodegenResult): - self.output_dir.mkdir(parents=True, exist_ok=True) - result.write(self.output_dir) - - click.echo( - click.style( - f"Generated {len(result.files)} files in {self.output_dir}", fg="green" - ) - ) - - -@click.command(short_help="Generate code from a query") -@click.option("--plugins", "-p", "selected_plugins", multiple=True, required=True) -@click.option("--cli-plugin", "cli_plugin", required=False) -@click.option( - "--output-dir", - "-o", - default=".", - help="Output directory", - type=click.Path(path_type=Path, exists=False, dir_okay=True, file_okay=False), -) -@click.option("--schema", type=str, required=True) -@click.argument("query", type=click.Path(path_type=Path, exists=True)) -@click.option( - "--app-dir", - default=".", - type=str, - show_default=True, - help=( - "Look for the module in the specified directory, by adding this to the " - "PYTHONPATH. Defaults to the current working directory. " - "Works the same as `--app-dir` in uvicorn." - ), -) -def codegen( - schema: str, - query: Path, - app_dir: str, - output_dir: Path, - selected_plugins: List[str], - cli_plugin: Optional[str] = None, -): - schema_symbol = load_schema(schema, app_dir) - - console_plugin = _load_plugin(cli_plugin) if cli_plugin else ConsolePlugin - - plugins = _load_plugins(selected_plugins) - plugins.append(console_plugin(query, output_dir, plugins)) - - code_generator = QueryCodegen(schema_symbol, plugins=plugins) - code_generator.run(query.read_text()) diff --git a/strawberry/cli/commands/export_schema.py b/strawberry/cli/commands/export_schema.py index 07e917c10f..f4039ac310 100644 --- a/strawberry/cli/commands/export_schema.py +++ b/strawberry/cli/commands/export_schema.py @@ -1,7 +1,10 @@ +import sys + import click -from strawberry.cli.utils import load_schema +from strawberry import Schema from strawberry.printer import print_schema +from strawberry.utils.importer import import_module_symbol @click.command(short_help="Exports the schema") @@ -17,7 +20,15 @@ "Works the same as `--app-dir` in uvicorn." ), ) -def export_schema(schema: str, app_dir: str): - schema_symbol = load_schema(schema, app_dir) +def export_schema(schema: str, app_dir): + sys.path.insert(0, app_dir) - print(print_schema(schema_symbol)) # noqa: T201 + try: + schema_symbol = import_module_symbol(schema, default_symbol_name="schema") + except (ImportError, AttributeError) as exc: + message = str(exc) + raise click.BadArgumentUsage(message) + if not isinstance(schema_symbol, Schema): + message = "The `schema` must be an instance of strawberry.Schema" + raise click.BadArgumentUsage(message) + print(print_schema(schema_symbol)) diff --git a/strawberry/cli/commands/server.py b/strawberry/cli/commands/server.py index f64f90d152..98adac574e 100644 --- a/strawberry/cli/commands/server.py +++ b/strawberry/cli/commands/server.py @@ -3,11 +3,9 @@ import click -from strawberry.cli.constants import ( - DEBUG_SERVER_LOG_OPERATIONS, - DEBUG_SERVER_SCHEMA_ENV_VAR_KEY, -) -from strawberry.cli.utils import load_schema +from strawberry import Schema +from strawberry.cli.constants import DEBUG_SERVER_SCHEMA_ENV_VAR_KEY +from strawberry.utils.importer import import_module_symbol @click.command("server", short_help="Starts debug server") @@ -31,14 +29,7 @@ "Works the same as `--app-dir` in uvicorn." ), ) -@click.option( - "--log-operations", - default=True, - type=bool, - show_default=True, - help="Log GraphQL operations", -) -def server(schema, host, port, log_level, app_dir, log_operations): +def server(schema, host, port, log_level, app_dir): sys.path.insert(0, app_dir) try: @@ -51,15 +42,22 @@ def server(schema, host, port, log_level, app_dir, log_operations): ) raise click.ClickException(message) - load_schema(schema, app_dir=app_dir) + try: + schema_symbol = import_module_symbol(schema, default_symbol_name="schema") + except (ImportError, AttributeError) as exc: + message = str(exc) + raise click.BadArgumentUsage(message) + + if not isinstance(schema_symbol, Schema): + message = "The `schema` must be an instance of strawberry.Schema" + raise click.BadArgumentUsage(message) os.environ[DEBUG_SERVER_SCHEMA_ENV_VAR_KEY] = schema - os.environ[DEBUG_SERVER_LOG_OPERATIONS] = str(log_operations) app = "strawberry.cli.debug_server:app" # Windows doesn't support UTF-8 by default endl = " ๐Ÿ“\n" if sys.platform != "win32" else "\n" - print(f"Running strawberry on http://{host}:{port}/graphql", end=endl) # noqa: T201 + print(f"Running strawberry on http://{host}:{port}/graphql", end=endl) uvicorn.run( app, diff --git a/strawberry/cli/constants.py b/strawberry/cli/constants.py index 79a8b962b9..79a7d3d856 100644 --- a/strawberry/cli/constants.py +++ b/strawberry/cli/constants.py @@ -1,2 +1 @@ DEBUG_SERVER_SCHEMA_ENV_VAR_KEY = "STRAWBERRY_DEBUG_SERVER_SCHEMA" -DEBUG_SERVER_LOG_OPERATIONS = "STRAWBERRY_DEBUG_SERVER_LOG_OPERATIONS" diff --git a/strawberry/cli/debug_server.py b/strawberry/cli/debug_server.py index 4436e8413b..f9ce0c4e02 100644 --- a/strawberry/cli/debug_server.py +++ b/strawberry/cli/debug_server.py @@ -5,12 +5,10 @@ from strawberry import Schema from strawberry.asgi import GraphQL -from strawberry.cli.constants import ( - DEBUG_SERVER_LOG_OPERATIONS, - DEBUG_SERVER_SCHEMA_ENV_VAR_KEY, -) +from strawberry.cli.constants import DEBUG_SERVER_SCHEMA_ENV_VAR_KEY from strawberry.utils.importer import import_module_symbol + app = Starlette(debug=True) app.add_middleware( CORSMiddleware, allow_headers=["*"], allow_origins=["*"], allow_methods=["*"] @@ -18,10 +16,9 @@ schema_import_string = os.environ[DEBUG_SERVER_SCHEMA_ENV_VAR_KEY] schema_symbol = import_module_symbol(schema_import_string, default_symbol_name="schema") -log_operations = os.environ[DEBUG_SERVER_LOG_OPERATIONS] == "True" assert isinstance(schema_symbol, Schema) -graphql_app = GraphQL(schema_symbol, debug=log_operations) +graphql_app = GraphQL(schema_symbol, debug=True) paths = ["/", "/graphql"] for path in paths: diff --git a/strawberry/cli/utils/__init__.py b/strawberry/cli/utils/__init__.py deleted file mode 100644 index 486de561e9..0000000000 --- a/strawberry/cli/utils/__init__.py +++ /dev/null @@ -1,23 +0,0 @@ -import sys - -import click - -from strawberry import Schema -from strawberry.utils.importer import import_module_symbol - - -def load_schema(schema: str, app_dir: str) -> Schema: - sys.path.insert(0, app_dir) - - try: - schema_symbol = import_module_symbol(schema, default_symbol_name="schema") - except (ImportError, AttributeError) as exc: - message = str(exc) - - raise click.BadArgumentUsage(message) - - if not isinstance(schema_symbol, Schema): - message = "The `schema` must be an instance of strawberry.Schema" - raise click.BadArgumentUsage(message) - - return schema_symbol diff --git a/strawberry/cli/utils/load_schema.py b/strawberry/cli/utils/load_schema.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/strawberry/codegen/__init__.py b/strawberry/codegen/__init__.py deleted file mode 100644 index c21594351c..0000000000 --- a/strawberry/codegen/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .query_codegen import CodegenFile, CodegenResult, QueryCodegen, QueryCodegenPlugin - -__all__ = ["QueryCodegen", "QueryCodegenPlugin", "CodegenFile", "CodegenResult"] diff --git a/strawberry/codegen/exceptions.py b/strawberry/codegen/exceptions.py deleted file mode 100644 index c78454a965..0000000000 --- a/strawberry/codegen/exceptions.py +++ /dev/null @@ -1,14 +0,0 @@ -class CodegenError(Exception): - pass - - -class NoOperationProvidedError(CodegenError): - pass - - -class NoOperationNameProvidedError(CodegenError): - pass - - -class MultipleOperationsProvidedError(CodegenError): - pass diff --git a/strawberry/codegen/plugins/print_operation.py b/strawberry/codegen/plugins/print_operation.py deleted file mode 100644 index 136ffc8887..0000000000 --- a/strawberry/codegen/plugins/print_operation.py +++ /dev/null @@ -1,154 +0,0 @@ -import textwrap -from typing import List, Optional - -from strawberry.codegen import CodegenFile, QueryCodegenPlugin -from strawberry.codegen.types import ( - GraphQLArgument, - GraphQLArgumentValue, - GraphQLBoolValue, - GraphQLDirective, - GraphQLEnumValue, - GraphQLFieldSelection, - GraphQLInlineFragment, - GraphQLIntValue, - GraphQLList, - GraphQLListValue, - GraphQLOperation, - GraphQLOptional, - GraphQLSelection, - GraphQLStringValue, - GraphQLType, - GraphQLVariableReference, -) - - -class PrintOperationPlugin(QueryCodegenPlugin): - def generate_code( - self, types: List[GraphQLType], operation: GraphQLOperation - ) -> List[CodegenFile]: - code = "\n".join( - [ - ( - f"{operation.kind} {operation.name}" - f"{self._print_operation_variables(operation)}" - f"{self._print_directives(operation.directives)} {{" - ), - self._print_selections(operation.selections), - "}", - ] - ) - return [CodegenFile("query.graphql", code)] - - def _print_operation_variables(self, operation: GraphQLOperation) -> str: - if not operation.variables: - return "" - - variables = ", ".join( - f"${v.name}: {self._print_graphql_type(v.type)}" - for v in operation.variables - ) - - return f"({variables})" - - def _print_graphql_type( - self, type: GraphQLType, parent_type: Optional[GraphQLType] = None - ) -> str: - if isinstance(type, GraphQLOptional): - return self._print_graphql_type(type.of_type, type) - - if isinstance(type, GraphQLList): - type_name = f"[{self._print_graphql_type(type.of_type, type)}]" - else: - type_name = type.name - - if parent_type and isinstance(parent_type, GraphQLOptional): - return type_name - - return f"{type_name}!" - - def _print_argument_value(self, value: GraphQLArgumentValue) -> str: - if isinstance(value, GraphQLStringValue): - return f'"{value.value}"' - - if isinstance(value, GraphQLIntValue): - return str(value.value) - - if isinstance(value, GraphQLVariableReference): - return f"${value.value}" - - if isinstance(value, GraphQLListValue): - return f"[{', '.join(self._print_argument_value(v) for v in value.values)}]" - - if isinstance(value, GraphQLEnumValue): - return value.name - - if isinstance(value, GraphQLBoolValue): - return str(value.value).lower() - - raise ValueError(f"not supported: {type(value)}") # pragma: no cover - - def _print_arguments(self, arguments: List[GraphQLArgument]) -> str: - if not arguments: - return "" - - return ( - "(" - + ", ".join( - [ - f"{argument.name}: {self._print_argument_value(argument.value)}" - for argument in arguments - ] - ) - + ")" - ) - - def _print_directives(self, directives: List[GraphQLDirective]) -> str: - if not directives: - return "" - - return " " + " ".join( - [ - f"@{directive.name}{self._print_arguments(directive.arguments)}" - for directive in directives - ] - ) - - def _print_field_selection(self, selection: GraphQLFieldSelection) -> str: - field = ( - f"{selection.field}" - f"{self._print_arguments(selection.arguments)}" - f"{self._print_directives(selection.directives)}" - ) - - if selection.alias: - field = f"{selection.alias}: {field}" - - if selection.selections: - return field + f" {{\n{self._print_selections(selection.selections)}\n}}" - - return field - - def _print_inline_fragment(self, fragment: GraphQLInlineFragment) -> str: - return "\n".join( - [ - f"... on {fragment.type_condition} {{", - self._print_selections(fragment.selections), - "}", - ] - ) - - def _print_selection(self, selection: GraphQLSelection) -> str: - if isinstance(selection, GraphQLFieldSelection): - return self._print_field_selection(selection) - - if isinstance(selection, GraphQLInlineFragment): - return self._print_inline_fragment(selection) - - raise ValueError(f"Unsupported selection: {selection}") # pragma: no cover - - def _print_selections(self, selections: List[GraphQLSelection]) -> str: - selections_text = "\n".join( - [self._print_selection(selection) for selection in selections] - ) - - return textwrap.indent(selections_text, " " * 2) diff --git a/strawberry/codegen/plugins/python.py b/strawberry/codegen/plugins/python.py deleted file mode 100644 index 7fd893d2ed..0000000000 --- a/strawberry/codegen/plugins/python.py +++ /dev/null @@ -1,159 +0,0 @@ -import textwrap -from collections import defaultdict -from dataclasses import dataclass -from typing import Dict, List, Optional, Set - -from strawberry.codegen import CodegenFile, QueryCodegenPlugin -from strawberry.codegen.types import ( - GraphQLEnum, - GraphQLField, - GraphQLList, - GraphQLObjectType, - GraphQLOperation, - GraphQLOptional, - GraphQLScalar, - GraphQLType, - GraphQLUnion, -) - - -@dataclass -class PythonType: - type: str - module: Optional[str] = None - - -class PythonPlugin(QueryCodegenPlugin): - SCALARS_TO_PYTHON_TYPES = { - "ID": PythonType("str"), - "Int": PythonType("int"), - "String": PythonType("str"), - "Float": PythonType("float"), - "Boolean": PythonType("bool"), - "UUID": PythonType("UUID", "uuid"), - "Date": PythonType("date", "datetime"), - "DateTime": PythonType("datetime", "datetime"), - "Time": PythonType("time", "datetime"), - "Decimal": PythonType("Decimal", "decimal"), - } - - def __init__(self) -> None: - self.imports: Dict[str, Set[str]] = defaultdict(set) - - def generate_code( - self, types: List[GraphQLType], operation: GraphQLOperation - ) -> List[CodegenFile]: - printed_types = list(filter(None, (self._print_type(type) for type in types))) - imports = self._print_imports() - - code = imports + "\n\n" + "\n\n".join(printed_types) - - return [CodegenFile("types.py", code.strip())] - - def _print_imports(self) -> str: - imports = [ - f'from {import_} import {", ".join(sorted(types))}' - for import_, types in self.imports.items() - ] - - return "\n".join(imports) - - def _get_type_name(self, type_: GraphQLType) -> str: - if isinstance(type_, GraphQLOptional): - self.imports["typing"].add("Optional") - - return f"Optional[{self._get_type_name(type_.of_type)}]" - - if isinstance(type_, GraphQLList): - self.imports["typing"].add("List") - - return f"List[{self._get_type_name(type_.of_type)}]" - - if isinstance(type_, GraphQLUnion): - # TODO: wrong place for this - self.imports["typing"].add("Union") - - return type_.name - - if isinstance(type_, (GraphQLObjectType, GraphQLEnum)): - if isinstance(type_, GraphQLEnum): - self.imports["enum"].add("Enum") - - return type_.name - - if ( - isinstance(type_, GraphQLScalar) - and type_.name in self.SCALARS_TO_PYTHON_TYPES - ): - python_type = self.SCALARS_TO_PYTHON_TYPES[type_.name] - - if python_type.module is not None: - self.imports[python_type.module].add(python_type.type) - - return python_type.type - - self.imports["typing"].add("NewType") - - return type_.name - - def _print_field(self, field: GraphQLField) -> str: - name = field.name - - if field.alias: - name = f"# alias for {field.name}\n{field.alias}" - - return f"{name}: {self._get_type_name(field.type)}" - - def _print_enum_value(self, value: str) -> str: - return f'{value} = "{value}"' - - def _print_object_type(self, type_: GraphQLObjectType) -> str: - fields = "\n".join(self._print_field(field) for field in type_.fields) - - return "\n".join( - [ - f"class {type_.name}:", - textwrap.indent(fields, " " * 4), - ] - ) - - def _print_enum_type(self, type_: GraphQLEnum) -> str: - values = "\n".join(self._print_enum_value(value) for value in type_.values) - - return "\n".join( - [ - f"class {type_.name}(Enum):", - textwrap.indent(values, " " * 4), - ] - ) - - def _print_scalar_type(self, type_: GraphQLScalar) -> str: - if type_.name in self.SCALARS_TO_PYTHON_TYPES: - return "" - - assert ( - type_.python_type is not None - ), f"Scalar type must have a python type: {type_.name}" - - return f'{type_.name} = NewType("{type_.name}", {type_.python_type.__name__})' - - def _print_union_type(self, type_: GraphQLUnion) -> str: - return f"{type_.name} = Union[{', '.join([t.name for t in type_.types])}]" - - def _print_type(self, type_: GraphQLType) -> str: - if isinstance(type_, GraphQLUnion): - return self._print_union_type(type_) - - if isinstance(type_, GraphQLObjectType): - return self._print_object_type(type_) - - if isinstance(type_, GraphQLEnum): - return self._print_enum_type(type_) - - if isinstance(type_, GraphQLScalar): - return self._print_scalar_type(type_) - - raise ValueError(f"Unknown type: {type}") # pragma: no cover - - -__all__ = ["PythonPlugin"] diff --git a/strawberry/codegen/plugins/typescript.py b/strawberry/codegen/plugins/typescript.py deleted file mode 100644 index aa16e687f5..0000000000 --- a/strawberry/codegen/plugins/typescript.py +++ /dev/null @@ -1,116 +0,0 @@ -import textwrap -from typing import List - -from strawberry.codegen import CodegenFile, QueryCodegenPlugin -from strawberry.codegen.types import ( - GraphQLEnum, - GraphQLField, - GraphQLList, - GraphQLObjectType, - GraphQLOperation, - GraphQLOptional, - GraphQLScalar, - GraphQLType, - GraphQLUnion, -) - - -class TypeScriptPlugin(QueryCodegenPlugin): - SCALARS_TO_TS_TYPE = { - "ID": "string", - "Int": "number", - "String": "string", - "Float": "number", - "Boolean": "boolean", - "UUID": "string", - "Date": "string", - "DateTime": "string", - "Time": "string", - "Decimal": "string", - str: "string", - float: "number", - } - - def generate_code( - self, types: List[GraphQLType], operation: GraphQLOperation - ) -> List[CodegenFile]: - printed_types = list(filter(None, (self._print_type(type) for type in types))) - - return [CodegenFile("types.ts", "\n\n".join(printed_types))] - - def _get_type_name(self, type_: GraphQLType) -> str: - if isinstance(type_, GraphQLOptional): - - return f"{self._get_type_name(type_.of_type)} | undefined" - - if isinstance(type_, GraphQLList): - child_type = self._get_type_name(type_.of_type) - - if "|" in child_type: - child_type = f"({child_type})" - - return f"{child_type}[]" - - if isinstance(type_, GraphQLUnion): - return type_.name - - if isinstance(type_, (GraphQLObjectType, GraphQLEnum)): - return type_.name - - if isinstance(type_, GraphQLScalar) and type_.name in self.SCALARS_TO_TS_TYPE: - return self.SCALARS_TO_TS_TYPE[type_.name] - - return type_.name - - def _print_field(self, field: GraphQLField) -> str: - name = field.name - - if field.alias: - name = f"// alias for {field.name}\n{field.alias}" - - return f"{name}: {self._get_type_name(field.type)}" - - def _print_enum_value(self, value: str) -> str: - return f'{value} = "{value}",' - - def _print_object_type(self, type_: GraphQLObjectType) -> str: - fields = "\n".join(self._print_field(field) for field in type_.fields) - - return "\n".join( - [f"type {type_.name} = {{", textwrap.indent(fields, " " * 4), "}"], - ) - - def _print_enum_type(self, type_: GraphQLEnum) -> str: - values = "\n".join(self._print_enum_value(value) for value in type_.values) - - return "\n".join( - [ - f"enum {type_.name} {{", - textwrap.indent(values, " " * 4), - "}", - ] - ) - - def _print_scalar_type(self, type_: GraphQLScalar) -> str: - if type_.name in self.SCALARS_TO_TS_TYPE: - return "" - - return f"type {type_.name} = {self.SCALARS_TO_TS_TYPE[type_.python_type]}" - - def _print_union_type(self, type_: GraphQLUnion) -> str: - return f"type {type_.name} = {' | '.join([t.name for t in type_.types])}" - - def _print_type(self, type_: GraphQLType) -> str: - if isinstance(type_, GraphQLUnion): - return self._print_union_type(type_) - - if isinstance(type_, GraphQLObjectType): - return self._print_object_type(type_) - - if isinstance(type_, GraphQLEnum): - return self._print_enum_type(type_) - - if isinstance(type_, GraphQLScalar): - return self._print_scalar_type(type_) - - raise ValueError(f"Unknown type: {type}") # pragma: no cover diff --git a/strawberry/codegen/query_codegen.py b/strawberry/codegen/query_codegen.py deleted file mode 100644 index a58a389529..0000000000 --- a/strawberry/codegen/query_codegen.py +++ /dev/null @@ -1,609 +0,0 @@ -from __future__ import annotations - -from dataclasses import dataclass -from pathlib import Path -from typing import Callable, Iterable, List, Optional, Tuple, Type, Union, cast -from typing_extensions import Literal, Protocol - -from graphql import ( - ArgumentNode, - BooleanValueNode, - DirectiveNode, - DocumentNode, - EnumValueNode, - FieldNode, - InlineFragmentNode, - IntValueNode, - ListTypeNode, - ListValueNode, - NamedTypeNode, - NonNullTypeNode, - OperationDefinitionNode, - SelectionNode, - SelectionSetNode, - StringValueNode, - TypeNode, - ValueNode, - VariableDefinitionNode, - VariableNode, - parse, -) - -import strawberry -from strawberry.custom_scalar import ScalarDefinition, ScalarWrapper -from strawberry.enum import EnumDefinition -from strawberry.lazy_type import LazyType -from strawberry.type import StrawberryList, StrawberryOptional, StrawberryType -from strawberry.types.types import TypeDefinition -from strawberry.union import StrawberryUnion -from strawberry.utils.str_converters import capitalize_first, to_camel_case - -from .exceptions import ( - MultipleOperationsProvidedError, - NoOperationNameProvidedError, - NoOperationProvidedError, -) -from .types import ( - GraphQLArgument, - GraphQLArgumentValue, - GraphQLBoolValue, - GraphQLDirective, - GraphQLEnum, - GraphQLEnumValue, - GraphQLField, - GraphQLFieldSelection, - GraphQLInlineFragment, - GraphQLIntValue, - GraphQLList, - GraphQLListValue, - GraphQLObjectType, - GraphQLOperation, - GraphQLOptional, - GraphQLScalar, - GraphQLSelection, - GraphQLStringValue, - GraphQLType, - GraphQLUnion, - GraphQLVariable, - GraphQLVariableReference, -) - - -@dataclass -class CodegenFile: - path: str - content: str - - -@dataclass -class CodegenResult: - files: List[CodegenFile] - - def to_string(self) -> str: - return "\n".join(f.content for f in self.files) + "\n" - - def write(self, folder: Path) -> None: - for file in self.files: - destination = folder / file.path - destination.write_text(file.content) - - -class HasSelectionSet(Protocol): - selection_set: Optional[SelectionSetNode] - - -class QueryCodegenPlugin: - def on_start(self) -> None: - ... - - def on_end(self, result: CodegenResult) -> None: - ... - - def generate_code( - self, types: List[GraphQLType], operation: GraphQLOperation - ) -> List[CodegenFile]: - return [] - - -class QueryCodegenPluginManager: - def __init__(self, plugins: List[QueryCodegenPlugin]) -> None: - self.plugins = plugins - - def generate_code( - self, types: List[GraphQLType], operation: GraphQLOperation - ) -> CodegenResult: - result = CodegenResult(files=[]) - - for plugin in self.plugins: - files = plugin.generate_code(types, operation) - - result.files.extend(files) - - return result - - def on_start(self) -> None: - for plugin in self.plugins: - plugin.on_start() - - def on_end(self, result: CodegenResult) -> None: - for plugin in self.plugins: - plugin.on_end(result) - - -class QueryCodegen: - def __init__(self, schema: strawberry.Schema, plugins: List[QueryCodegenPlugin]): - self.schema = schema - self.plugin_manager = QueryCodegenPluginManager(plugins) - self.types: List[GraphQLType] = [] - - def run(self, query: str) -> CodegenResult: - self.plugin_manager.on_start() - - ast = parse(query) - - operations = self._get_operations(ast) - - if not operations: - raise NoOperationProvidedError() - - if len(operations) > 1: - raise MultipleOperationsProvidedError() - - operation = operations[0] - - if operation.name is None: - raise NoOperationNameProvidedError() - - self.operation = self._convert_operation(operation) - - result = self.generate_code() - self.plugin_manager.on_end(result) - - return result - - def _collect_type(self, type_: GraphQLType) -> None: - if type_ in self.types: - return - - self.types.append(type_) - - def _convert_selection(self, selection: SelectionNode) -> GraphQLSelection: - if isinstance(selection, FieldNode): - return GraphQLFieldSelection( - selection.name.value, - selection.alias.value if selection.alias else None, - selections=self._convert_selection_set(selection.selection_set), - directives=self._convert_directives(selection.directives), - arguments=self._convert_arguments(selection.arguments), - ) - - if isinstance(selection, InlineFragmentNode): - return GraphQLInlineFragment( - selection.type_condition.name.value, - self._convert_selection_set(selection.selection_set), - ) - - raise ValueError(f"Unsupported type: {type(selection)}") # pragma: no cover - - def _convert_selection_set( - self, selection_set: Optional[SelectionSetNode] - ) -> List[GraphQLSelection]: - - if selection_set is None: - return [] - - return [ - self._convert_selection(selection) for selection in selection_set.selections - ] - - def _convert_value(self, value: ValueNode) -> GraphQLArgumentValue: - if isinstance(value, StringValueNode): - return GraphQLStringValue(value.value) - - if isinstance(value, IntValueNode): - return GraphQLIntValue(int(value.value)) - - if isinstance(value, VariableNode): - return GraphQLVariableReference(value.name.value) - - if isinstance(value, ListValueNode): - return GraphQLListValue( - [self._convert_value(item) for item in value.values] - ) - - if isinstance(value, EnumValueNode): - return GraphQLEnumValue(value.value) - - if isinstance(value, BooleanValueNode): - return GraphQLBoolValue(value.value) - - raise ValueError(f"Unsupported type: {type(value)}") # pragma: no cover - - def _convert_arguments( - self, arguments: Iterable[ArgumentNode] - ) -> List[GraphQLArgument]: - return [ - GraphQLArgument(argument.name.value, self._convert_value(argument.value)) - for argument in arguments - ] - - def _convert_directives( - self, directives: Iterable[DirectiveNode] - ) -> List[GraphQLDirective]: - return [ - GraphQLDirective( - directive.name.value, - self._convert_arguments(directive.arguments), - ) - for directive in directives - ] - - def _convert_operation( - self, operation_definition: OperationDefinitionNode - ) -> GraphQLOperation: - query_type = self.schema.get_type_by_name("Query") - assert isinstance(query_type, TypeDefinition) - - assert operation_definition.name is not None - operation_name = operation_definition.name.value - result_class_name = f"{operation_name}Result" - - operation_type = self._collect_types( - cast(HasSelectionSet, operation_definition), - parent_type=query_type, - class_name=result_class_name, - ) - - operation_kind = cast( - Literal["query", "mutation", "subscription"], - operation_definition.operation.value, - ) - - variables, variables_type = self._convert_variable_definitions( - operation_definition.variable_definitions, operation_name=operation_name - ) - - return GraphQLOperation( - operation_definition.name.value, - kind=operation_kind, - selections=self._convert_selection_set(operation_definition.selection_set), - directives=self._convert_directives(operation_definition.directives), - variables=variables, - type=cast(GraphQLObjectType, operation_type), - variables_type=variables_type, - ) - - def _convert_variable_definitions( - self, - variable_definitions: Optional[Iterable[VariableDefinitionNode]], - operation_name: str, - ) -> Tuple[List[GraphQLVariable], Optional[GraphQLObjectType]]: - if not variable_definitions: - return [], None - - type_ = GraphQLObjectType(f"{operation_name}Variables", []) - - self._collect_type(type_) - - variables: List[GraphQLVariable] = [] - - for variable_definition in variable_definitions: - variable_type = self._collect_type_from_variable(variable_definition.type) - variable = GraphQLVariable( - variable_definition.variable.name.value, - variable_type, - ) - - type_.fields.append(GraphQLField(variable.name, None, variable_type)) - - variables.append(variable) - - return variables, type_ - - def _get_operations(self, ast: DocumentNode) -> List[OperationDefinitionNode]: - return [ - definition - for definition in ast.definitions - if isinstance(definition, OperationDefinitionNode) - ] - - def _get_field_type( - self, - field_type: Union[StrawberryType, type], - ) -> GraphQLType: - if isinstance(field_type, StrawberryOptional): - return GraphQLOptional(self._get_field_type(field_type.of_type)) - - if isinstance(field_type, StrawberryList): - return GraphQLList(self._get_field_type(field_type.of_type)) - - if ( - not isinstance(field_type, StrawberryType) - and field_type in self.schema.schema_converter.scalar_registry - ): - field_type = self.schema.schema_converter.scalar_registry[field_type] # type: ignore # noqa: E501 - - if isinstance(field_type, ScalarWrapper): - python_type = field_type.wrap - if hasattr(python_type, "__supertype__"): - python_type = python_type.__supertype__ - - return self._collect_scalar(field_type._scalar_definition, python_type) # type: ignore # noqa: E501 - - if isinstance(field_type, ScalarDefinition): - return self._collect_scalar(field_type, None) - - elif isinstance(field_type, EnumDefinition): - return self._collect_enum(field_type) - - raise ValueError(f"Unsupported type: {field_type}") # pragma: no cover - - def _collect_type_from_strawberry_type( - self, strawberry_type: Union[type, StrawberryType] - ) -> GraphQLType: - type_: GraphQLType - - if isinstance(strawberry_type, StrawberryOptional): - return GraphQLOptional( - self._collect_type_from_strawberry_type(strawberry_type.of_type) - ) - - if isinstance(strawberry_type, StrawberryList): - return GraphQLOptional( - self._collect_type_from_strawberry_type(strawberry_type.of_type) - ) - - if hasattr(strawberry_type, "_type_definition"): - strawberry_type = strawberry_type._type_definition - - if isinstance(strawberry_type, TypeDefinition): - type_ = GraphQLObjectType( - strawberry_type.name, - [], - ) - - for field in strawberry_type.fields: - field_type = self._collect_type_from_strawberry_type(field.type) - type_.fields.append(GraphQLField(field.name, None, field_type)) - - self._collect_type(type_) - else: - type_ = self._get_field_type(strawberry_type) - - return type_ - - def _collect_type_from_variable( - self, variable_type: TypeNode, parent_type: Optional[TypeNode] = None - ) -> GraphQLType: - type_: Optional[GraphQLType] = None - - if isinstance(variable_type, ListTypeNode): - type_ = GraphQLList( - self._collect_type_from_variable(variable_type.type, variable_type) - ) - - elif isinstance(variable_type, NonNullTypeNode): - return self._collect_type_from_variable(variable_type.type, variable_type) - - elif isinstance(variable_type, NamedTypeNode): - strawberry_type = self.schema.get_type_by_name(variable_type.name.value) - - assert strawberry_type - - type_ = self._collect_type_from_strawberry_type(strawberry_type) - - assert type_ - - if parent_type is not None and isinstance(parent_type, NonNullTypeNode): - return type_ - - return GraphQLOptional(type_) - - def _field_from_selection( - self, selection: FieldNode, parent_type: TypeDefinition - ) -> GraphQLField: - field = self.schema.get_field_for_type(selection.name.value, parent_type.name) - assert field - - field_type = self._get_field_type(field.type) - - return GraphQLField( - field.name, selection.alias.value if selection.alias else None, field_type - ) - - def _unwrap_type( - self, type_: Union[type, StrawberryType] - ) -> Tuple[ - Union[type, StrawberryType], Optional[Callable[[GraphQLType], GraphQLType]] - ]: - wrapper = None - - if isinstance(type_, StrawberryOptional): - type_, wrapper = self._unwrap_type(type_.of_type) - wrapper = ( - GraphQLOptional - if wrapper is None - else lambda t: GraphQLOptional(wrapper(t)) # type: ignore[misc] - ) - - elif isinstance(type_, StrawberryList): - type_, wrapper = self._unwrap_type(type_.of_type) - wrapper = ( - GraphQLList - if wrapper is None - else lambda t: GraphQLList(wrapper(t)) # type: ignore[misc] - ) - - elif isinstance(type_, LazyType): - return self._unwrap_type(type_.resolve_type()) - - return type_, wrapper - - def _field_from_selection_set( - self, selection: FieldNode, class_name: str, parent_type: TypeDefinition - ) -> GraphQLField: - assert selection.selection_set is not None - - selected_field = self.schema.get_field_for_type( - selection.name.value, parent_type.name - ) - assert selected_field - - selected_field_type, wrapper = self._unwrap_type(selected_field.type) - name = capitalize_first(to_camel_case(selection.name.value)) - class_name = f"{class_name}{(name)}" - - field_type: GraphQLType - - if isinstance(selected_field_type, StrawberryUnion): - field_type = self._collect_types_with_inline_fragments( - selection, parent_type, class_name - ) - else: - parent_type = cast( - TypeDefinition, selected_field_type._type_definition # type: ignore - ) - - field_type = self._collect_types(selection, parent_type, class_name) - - if wrapper: - field_type = wrapper(field_type) - - return GraphQLField( - selected_field.name, - selection.alias.value if selection.alias else None, - field_type, - ) - - def _get_field( - self, selection: FieldNode, class_name: str, parent_type: TypeDefinition - ) -> GraphQLField: - - if selection.selection_set: - return self._field_from_selection_set(selection, class_name, parent_type) - - return self._field_from_selection(selection, parent_type) - - def _collect_types_with_inline_fragments( - self, - selection: HasSelectionSet, - parent_type: TypeDefinition, - class_name: str, - ) -> Union[GraphQLObjectType, GraphQLUnion]: - sub_types = self._collect_types_using_fragments( - selection, parent_type, class_name - ) - - if len(sub_types) == 1: - return sub_types[0] - - union = GraphQLUnion(class_name, sub_types) - - self._collect_type(union) - - return union - - def _collect_types( - self, - selection: HasSelectionSet, - parent_type: TypeDefinition, - class_name: str, - ) -> GraphQLType: - assert selection.selection_set is not None - selection_set = selection.selection_set - - if any( - isinstance(selection, InlineFragmentNode) - for selection in selection_set.selections - ): - return self._collect_types_with_inline_fragments( - selection, parent_type, class_name - ) - - current_type = GraphQLObjectType(class_name, []) - - for sub_selection in selection_set.selections: - assert isinstance(sub_selection, FieldNode) - - field = self._get_field(sub_selection, class_name, parent_type) - - current_type.fields.append(field) - - self._collect_type(current_type) - - return current_type - - def generate_code(self) -> CodegenResult: - return self.plugin_manager.generate_code( - types=self.types, operation=self.operation - ) - - def _collect_types_using_fragments( - self, - selection: HasSelectionSet, - parent_type: TypeDefinition, - class_name: str, - ) -> List[GraphQLObjectType]: - assert selection.selection_set - - common_fields: List[GraphQLField] = [] - fragments: List[InlineFragmentNode] = [] - sub_types: List[GraphQLObjectType] = [] - - for sub_selection in selection.selection_set.selections: - if isinstance(sub_selection, FieldNode): - common_fields.append( - self._get_field(sub_selection, class_name, parent_type) - ) - - if isinstance(sub_selection, InlineFragmentNode): - fragments.append(sub_selection) - - for fragment in fragments: - fragment_class_name = class_name + fragment.type_condition.name.value - current_type = GraphQLObjectType(fragment_class_name, []) - - for sub_selection in fragment.selection_set.selections: - # TODO: recurse, use existing method ? - assert isinstance(sub_selection, FieldNode) - - current_type.fields = list(common_fields) - - parent_type = cast( - TypeDefinition, - self.schema.get_type_by_name(fragment.type_condition.name.value), - ) - - assert parent_type - - current_type.fields.append( - self._get_field( - selection=sub_selection, - class_name=fragment_class_name, - parent_type=parent_type, - ) - ) - - sub_types.append(current_type) - - self.types.extend(sub_types) - - return sub_types - - def _collect_scalar( - self, scalar_definition: ScalarDefinition, python_type: Optional[Type] - ) -> GraphQLScalar: - graphql_scalar = GraphQLScalar(scalar_definition.name, python_type=python_type) - - self._collect_type(graphql_scalar) - - return graphql_scalar - - def _collect_enum(self, enum: EnumDefinition) -> GraphQLEnum: - graphql_enum = GraphQLEnum( - enum.name, - [value.name for value in enum.values], - python_type=enum.wrapped_cls, - ) - self._collect_type(graphql_enum) - return graphql_enum diff --git a/strawberry/codegen/types.py b/strawberry/codegen/types.py deleted file mode 100644 index 31cb151659..0000000000 --- a/strawberry/codegen/types.py +++ /dev/null @@ -1,145 +0,0 @@ -from __future__ import annotations - -from dataclasses import dataclass -from enum import EnumMeta -from typing import List, Optional, Type, Union -from typing_extensions import Literal - - -@dataclass -class GraphQLOptional: - of_type: GraphQLType - - -@dataclass -class GraphQLList: - of_type: GraphQLType - - -@dataclass -class GraphQLUnion: - name: str - types: List[GraphQLObjectType] - - -@dataclass -class GraphQLField: - name: str - alias: Optional[str] - type: GraphQLType - - -@dataclass -class GraphQLObjectType: - name: str - fields: List[GraphQLField] - - -@dataclass -class GraphQLEnum: - name: str - values: List[str] - python_type: EnumMeta - - -@dataclass -class GraphQLScalar: - name: str - python_type: Optional[Type] - - -GraphQLType = Union[ - GraphQLObjectType, - GraphQLEnum, - GraphQLScalar, - GraphQLOptional, - GraphQLList, - GraphQLUnion, -] - - -@dataclass -class GraphQLFieldSelection: - field: str - alias: Optional[str] - selections: List[GraphQLSelection] - directives: List[GraphQLDirective] - arguments: List[GraphQLArgument] - - -@dataclass -class GraphQLInlineFragment: - type_condition: str - selections: List[GraphQLSelection] - - -GraphQLSelection = Union[GraphQLFieldSelection, GraphQLInlineFragment] - - -@dataclass -class GraphQLStringValue: - value: str - - -@dataclass -class GraphQLIntValue: - value: int - - -@dataclass -class GraphQLEnumValue: - name: str - - -@dataclass -class GraphQLBoolValue: - value: bool - - -@dataclass -class GraphQLListValue: - values: List[GraphQLArgumentValue] - - -@dataclass -class GraphQLVariableReference: - value: str - - -GraphQLArgumentValue = Union[ - GraphQLStringValue, - GraphQLIntValue, - GraphQLVariableReference, - GraphQLListValue, - GraphQLEnumValue, - GraphQLBoolValue, -] - - -@dataclass -class GraphQLArgument: - name: str - value: GraphQLArgumentValue - - -@dataclass -class GraphQLDirective: - name: str - arguments: List[GraphQLArgument] - - -@dataclass -class GraphQLVariable: - name: str - type: GraphQLType - - -@dataclass -class GraphQLOperation: - name: str - kind: Literal["query", "mutation", "subscription"] - selections: List[GraphQLSelection] - directives: List[GraphQLDirective] - variables: List[GraphQLVariable] - type: GraphQLObjectType - variables_type: Optional[GraphQLObjectType] diff --git a/strawberry/custom_scalar.py b/strawberry/custom_scalar.py index 21a1d60e2e..5cc58f40ed 100644 --- a/strawberry/custom_scalar.py +++ b/strawberry/custom_scalar.py @@ -1,31 +1,12 @@ -import sys from dataclasses import dataclass -from typing import ( - Any, - Callable, - Iterable, - Mapping, - NewType, - Optional, - Type, - TypeVar, - Union, - overload, -) +from typing import Callable, Mapping, Optional, TypeVar, Union from graphql import GraphQLScalarType -from strawberry.exceptions import InvalidUnionTypeError -from strawberry.type import StrawberryOptional, StrawberryType +from strawberry.type import StrawberryType from .utils.str_converters import to_camel_case -# in python 3.10+ NewType is a class -if sys.version_info >= (3, 10): - _T = TypeVar("_T", bound=Union[type, NewType]) -else: - _T = TypeVar("_T", bound=type) - def identity(x): return x @@ -35,24 +16,18 @@ def identity(x): class ScalarDefinition(StrawberryType): name: str description: Optional[str] - specified_by_url: Optional[str] serialize: Optional[Callable] parse_value: Optional[Callable] parse_literal: Optional[Callable] - directives: Iterable[object] = () # Optionally store the GraphQLScalarType instance so that we don't get # duplicates implementation: Optional[GraphQLScalarType] = None - # used for better error messages - _source_file: Optional[str] = None - _source_line: Optional[int] = None - def copy_with( self, type_var_map: Mapping[TypeVar, Union[StrawberryType, type]] ) -> Union[StrawberryType, type]: - return super().copy_with(type_var_map) # type: ignore[safe-super] + return super().copy_with(type_var_map) @property def is_generic(self) -> bool: @@ -62,106 +37,46 @@ def is_generic(self) -> bool: class ScalarWrapper: _scalar_definition: ScalarDefinition - def __init__(self, wrap: Callable[[Any], Any]): + def __init__(self, wrap): self.wrap = wrap def __call__(self, *args, **kwargs): return self.wrap(*args, **kwargs) - def __or__(self, other: Union[StrawberryType, type]) -> StrawberryType: - if other is None: - # Return the correct notation when using `StrawberryUnion | None`. - return StrawberryOptional(of_type=self) - - # Raise an error in any other case. - # There is Work in progress to deal with more merging cases, see: - # https://github.com/strawberry-graphql/strawberry/pull/1455 - raise InvalidUnionTypeError(str(other), self.wrap) - def _process_scalar( - cls: Type[_T], + cls, *, - name: Optional[str] = None, - description: Optional[str] = None, - specified_by_url: Optional[str] = None, - serialize: Optional[Callable] = None, - parse_value: Optional[Callable] = None, - parse_literal: Optional[Callable] = None, - directives: Iterable[object] = (), + name: str = None, + description: str = None, + serialize: Callable = None, + parse_value: Callable = None, + parse_literal: Callable = None ): - from strawberry.exceptions.handler import should_use_rich_exceptions name = name or to_camel_case(cls.__name__) - _source_file = None - _source_line = None - - if should_use_rich_exceptions(): - frame = sys._getframe(3) - - _source_file = frame.f_code.co_filename - _source_line = frame.f_lineno - wrapper = ScalarWrapper(cls) wrapper._scalar_definition = ScalarDefinition( name=name, description=description, - specified_by_url=specified_by_url, serialize=serialize, parse_literal=parse_literal, parse_value=parse_value, - directives=directives, - _source_file=_source_file, - _source_line=_source_line, ) return wrapper -@overload -def scalar( - *, - name: Optional[str] = None, - description: Optional[str] = None, - specified_by_url: Optional[str] = None, - serialize: Callable = identity, - parse_value: Optional[Callable] = None, - parse_literal: Optional[Callable] = None, - directives: Iterable[object] = (), -) -> Callable[[_T], _T]: - ... - - -@overload -def scalar( - cls: _T, - *, - name: Optional[str] = None, - description: Optional[str] = None, - specified_by_url: Optional[str] = None, - serialize: Callable = identity, - parse_value: Optional[Callable] = None, - parse_literal: Optional[Callable] = None, - directives: Iterable[object] = (), -) -> _T: - ... - - -# FIXME: We are tricking pyright into thinking that we are returning the given type -# here or else it won't let us use any custom scalar to annotate attributes in -# dataclasses/types. This should be properly solved when implementing StrawberryScalar def scalar( cls=None, *, - name: Optional[str] = None, - description: Optional[str] = None, - specified_by_url: Optional[str] = None, + name: str = None, + description: str = None, serialize: Callable = identity, parse_value: Optional[Callable] = None, - parse_literal: Optional[Callable] = None, - directives: Iterable[object] = (), -) -> Any: + parse_literal: Optional[Callable] = None +): """Annotates a class or type as a GraphQL custom scalar. Example usages: @@ -196,11 +111,9 @@ def wrap(cls): cls, name=name, description=description, - specified_by_url=specified_by_url, serialize=serialize, parse_value=parse_value, parse_literal=parse_literal, - directives=directives, ) if cls is None: diff --git a/strawberry/dataloader.py b/strawberry/dataloader.py index 264bc46e75..250e3b1974 100644 --- a/strawberry/dataloader.py +++ b/strawberry/dataloader.py @@ -1,7 +1,4 @@ -from __future__ import annotations - import dataclasses -from abc import ABC, abstractmethod from asyncio import create_task, gather, get_event_loop from asyncio.events import AbstractEventLoop from asyncio.futures import Future @@ -12,19 +9,15 @@ Callable, Dict, Generic, - Hashable, Iterable, List, - Mapping, Optional, - Sequence, TypeVar, - Union, - overload, ) from .exceptions import WrongNumberOfResultsReturned + T = TypeVar("T") K = TypeVar("K") @@ -48,83 +41,18 @@ def __len__(self) -> int: return len(self.tasks) -class AbstractCache(Generic[K, T], ABC): - @abstractmethod - def get(self, key: K) -> Union[Future[T], None]: - pass - - @abstractmethod - def set(self, key: K, value: Future[T]) -> None: - pass - - @abstractmethod - def delete(self, key: K) -> None: - pass - - @abstractmethod - def clear(self) -> None: - pass - - -class DefaultCache(AbstractCache[K, T]): - def __init__(self, cache_key_fn: Optional[Callable[[K], Hashable]] = None): - self.cache_key_fn: Callable[[K], Hashable] = ( - cache_key_fn if cache_key_fn is not None else lambda x: x - ) - self.cache_map: Dict[Hashable, Future[T]] = {} - - def get(self, key: K) -> Union[Future[T], None]: - return self.cache_map.get(self.cache_key_fn(key)) - - def set(self, key: K, value: Future[T]) -> None: - self.cache_map[self.cache_key_fn(key)] = value - - def delete(self, key: K) -> None: - del self.cache_map[self.cache_key_fn(key)] - - def clear(self): - self.cache_map.clear() - - class DataLoader(Generic[K, T]): + queue: List[LoaderTask] = [] batch: Optional[Batch[K, T]] = None cache: bool = False - cache_map: AbstractCache[K, T] + cache_map: Dict[K, Future] - @overload def __init__( self, - # any BaseException is rethrown in 'load', so should be excluded from the T type - load_fn: Callable[[List[K]], Awaitable[Sequence[Union[T, BaseException]]]], + load_fn: Callable[[List[K]], Awaitable[List[T]]], max_batch_size: Optional[int] = None, cache: bool = True, - loop: Optional[AbstractEventLoop] = None, - cache_map: Optional[AbstractCache[K, T]] = None, - cache_key_fn: Optional[Callable[[K], Hashable]] = None, - ) -> None: - ... - - # fallback if load_fn is untyped and there's no other info for inference - @overload - def __init__( - self: DataLoader[K, Any], - load_fn: Callable[[List[K]], Awaitable[List[Any]]], - max_batch_size: Optional[int] = None, - cache: bool = True, - loop: Optional[AbstractEventLoop] = None, - cache_map: Optional[AbstractCache[K, T]] = None, - cache_key_fn: Optional[Callable[[K], Hashable]] = None, - ) -> None: - ... - - def __init__( - self, - load_fn: Callable[[List[K]], Awaitable[Sequence[Union[T, BaseException]]]], - max_batch_size: Optional[int] = None, - cache: bool = True, - loop: Optional[AbstractEventLoop] = None, - cache_map: Optional[AbstractCache[K, T]] = None, - cache_key_fn: Optional[Callable[[K], Hashable]] = None, + loop: AbstractEventLoop = None, ): self.load_fn = load_fn self.max_batch_size = max_batch_size @@ -134,9 +62,7 @@ def __init__( self.cache = cache if self.cache: - self.cache_map = ( - DefaultCache(cache_key_fn) if cache_map is None else cache_map - ) + self.cache_map = {} @property def loop(self) -> AbstractEventLoop: @@ -149,13 +75,13 @@ def load(self, key: K) -> Awaitable[T]: if self.cache: future = self.cache_map.get(key) - if future and not future.cancelled(): + if future: return future future = self.loop.create_future() if self.cache: - self.cache_map.set(key, future) + self.cache_map[key] = future batch = get_current_batch(self) batch.add_task(key, future) @@ -165,45 +91,6 @@ def load(self, key: K) -> Awaitable[T]: def load_many(self, keys: Iterable[K]) -> Awaitable[List[T]]: return gather(*map(self.load, keys)) - def clear(self, key: K): - if self.cache: - self.cache_map.delete(key) - - def clear_many(self, keys: Iterable[K]): - if self.cache: - for key in keys: - self.cache_map.delete(key) - - def clear_all(self): - if self.cache: - self.cache_map.clear() - - def prime(self, key: K, value: T, force: bool = False): - self.prime_many({key: value}, force) - - def prime_many(self, data: Mapping[K, T], force: bool = False): - # Populate the cache with the specified values - if self.cache: - for key, value in data.items(): - if not self.cache_map.get(key) or force: - future: Future = Future(loop=self.loop) - future.set_result(value) - self.cache_map.set(key, future) - - # For keys that are pending on the current batch, but the - # batch hasn't started fetching yet: Remove it from the - # batch and set to the specified value - if self.batch is not None and not self.batch.dispatched: - batch_updated = False - for task in self.batch.tasks: - if task.key in data: - batch_updated = True - task.future.set_result(data[task.key]) - if batch_updated: - self.batch.tasks = [ - task for task in self.batch.tasks if not task.future.done() - ] - def should_create_new_batch(loader: DataLoader, batch: Batch) -> bool: if ( @@ -228,18 +115,16 @@ def get_current_batch(loader: DataLoader) -> Batch: def dispatch(loader: DataLoader, batch: Batch): - loader.loop.call_soon(create_task, dispatch_batch(loader, batch)) + async def dispatch(): + await dispatch_batch(loader, batch) + + loader.loop.call_soon(create_task, dispatch()) async def dispatch_batch(loader: DataLoader, batch: Batch) -> None: batch.dispatched = True keys = [task.key for task in batch.tasks] - if len(keys) == 0: - # Ensure batch is not empty - # Unlikely, but could happen if the tasks are - # overriden with preset values - return # TODO: check if load_fn return an awaitable and it is a list @@ -253,11 +138,6 @@ async def dispatch_batch(loader: DataLoader, batch: Batch) -> None: ) for task, value in zip(batch.tasks, values): - # Trying to set_result in a cancelled future would raise - # asyncio.exceptions.InvalidStateError - if task.future.cancelled(): - continue - if isinstance(value, BaseException): task.future.set_exception(value) else: diff --git a/strawberry/directive.py b/strawberry/directive.py index b08343d075..923f1441a1 100644 --- a/strawberry/directive.py +++ b/strawberry/directive.py @@ -2,69 +2,53 @@ import dataclasses import inspect -from typing import Any, Callable, List, Optional, TypeVar -from typing_extensions import Annotated +import sys +from itertools import islice +from typing import Callable, List, Optional, TypeVar from graphql import DirectiveLocation +from strawberry.annotation import StrawberryAnnotation from strawberry.arguments import StrawberryArgument -from strawberry.field import StrawberryField -from strawberry.types.fields.resolver import ( - INFO_PARAMSPEC, - ReservedType, - StrawberryResolver, -) -from strawberry.unset import UNSET -from strawberry.utils.cached_property import cached_property - - -def directive_field(name: str, default: object = UNSET) -> Any: - return StrawberryField( - python_name=None, - graphql_name=name, - default=default, - ) - - -T = TypeVar("T") - - -class StrawberryDirectiveValue: - ... - - -DirectiveValue = Annotated[T, StrawberryDirectiveValue()] -DirectiveValue.__doc__ = ( - """Represents the ``value`` argument for a GraphQL query directive.""" -) - -# Registers `DirectiveValue[...]` annotated arguments as reserved -VALUE_PARAMSPEC = ReservedType(name="value", type=StrawberryDirectiveValue) - - -class StrawberryDirectiveResolver(StrawberryResolver[T]): - - RESERVED_PARAMSPEC = ( - INFO_PARAMSPEC, - VALUE_PARAMSPEC, - ) - - @cached_property - def value_parameter(self) -> Optional[inspect.Parameter]: - return self.reserved_parameters.get(VALUE_PARAMSPEC) @dataclasses.dataclass class StrawberryDirective: python_name: str graphql_name: Optional[str] - resolver: StrawberryDirectiveResolver + resolver: Callable locations: List[DirectiveLocation] description: Optional[str] = None - @cached_property + @property def arguments(self) -> List[StrawberryArgument]: - return self.resolver.arguments + annotations = self.resolver.__annotations__ + annotations = dict(islice(annotations.items(), 1, None)) + annotations.pop("return", None) + + parameters = inspect.signature(self.resolver).parameters + + module = sys.modules[self.resolver.__module__] + annotation_namespace = module.__dict__ + arguments = [] + for arg_name, annotation in annotations.items(): + parameter = parameters[arg_name] + + argument = StrawberryArgument( + python_name=arg_name, + graphql_name=None, + type_annotation=StrawberryAnnotation( + annotation=annotation, namespace=annotation_namespace + ), + default=parameter.default, + ) + + arguments.append(argument) + + return arguments + + +T = TypeVar("T") def directive( @@ -79,7 +63,7 @@ def _wrap(f: Callable[..., T]) -> T: graphql_name=name, locations=locations, description=description, - resolver=StrawberryDirectiveResolver(f), + resolver=f, ) return _wrap diff --git a/strawberry/django/__init__.py b/strawberry/django/__init__.py index bcbd0bbc5e..9509ee0cd2 100644 --- a/strawberry/django/__init__.py +++ b/strawberry/django/__init__.py @@ -1,7 +1,7 @@ try: # import modules and objects from external strawberry-graphql-django # package so that it can be used through strawberry.django namespace - from strawberry_django import * # noqa: F403 + from strawberry_django import * # noqa: F401, F403 except ModuleNotFoundError: import importlib diff --git a/strawberry/django/test/__init__.py b/strawberry/django/test/__init__.py index 47b4c12cc0..d9954b9f51 100644 --- a/strawberry/django/test/__init__.py +++ b/strawberry/django/test/__init__.py @@ -1,3 +1,4 @@ from .client import GraphQLTestClient + __all__ = ["GraphQLTestClient"] diff --git a/strawberry/django/test/client.py b/strawberry/django/test/client.py index 99dd015775..42dec7a6f1 100644 --- a/strawberry/django/test/client.py +++ b/strawberry/django/test/client.py @@ -12,9 +12,9 @@ def request( ): if files: return self._client.post( - self.url, data=body, format="multipart", headers=headers + "/graphql/", data=body, format="multipart", headers=headers ) return self._client.post( - self.url, data=body, content_type="application/json", headers=headers + "/graphql/", data=body, content_type="application/json", headers=headers ) diff --git a/strawberry/django/views.py b/strawberry/django/views.py index 76854b2426..c1fded6783 100644 --- a/strawberry/django/views.py +++ b/strawberry/django/views.py @@ -1,9 +1,9 @@ import asyncio import json -import warnings +import os from typing import Any, Dict, Optional, Type -from django.core.exceptions import BadRequest, SuspiciousOperation +from django.core.exceptions import SuspiciousOperation from django.core.serializers.json import DjangoJSONEncoder from django.http import Http404, HttpRequest, HttpResponseNotAllowed, JsonResponse from django.http.response import HttpResponse @@ -15,19 +15,16 @@ from django.views.decorators.csrf import csrf_exempt from django.views.generic import View +import strawberry from strawberry.exceptions import MissingQueryError from strawberry.file_uploads.utils import replace_placeholders_with_files from strawberry.http import ( GraphQLHTTPResponse, GraphQLRequestData, - parse_query_params, parse_request_data, process_result, ) -from strawberry.schema.exceptions import InvalidOperationTypeError from strawberry.types import ExecutionResult -from strawberry.types.graphql import OperationType -from strawberry.utils.graphiql import get_graphiql_html from ..schema import BaseSchema from .context import StrawberryDjangoContext @@ -39,72 +36,34 @@ class TemporalHttpResponse(JsonResponse): def __init__(self) -> None: super().__init__({}) - def __repr__(self) -> str: - """Adopted from Django to handle `status_code=None`.""" - if self.status_code is not None: - return super().__repr__() - return "<{cls} status_code={status_code}{content_type}>".format( - cls=self.__class__.__name__, - status_code=self.status_code, - content_type=self._content_type_for_repr, - ) - class BaseView(View): subscriptions_enabled = False graphiql = True - allow_queries_via_get = True schema: Optional[BaseSchema] = None - json_encoder: Optional[Type[json.JSONEncoder]] = None + json_encoder: Type[json.JSONEncoder] = DjangoJSONEncoder json_dumps_params: Optional[Dict[str, Any]] = None def __init__( self, schema: BaseSchema, - graphiql: bool = True, - allow_queries_via_get: bool = True, - subscriptions_enabled: bool = False, + graphiql=True, + subscriptions_enabled=False, **kwargs: Any, ): self.schema = schema self.graphiql = graphiql - self.allow_queries_via_get = allow_queries_via_get self.subscriptions_enabled = subscriptions_enabled - super().__init__(**kwargs) - self.json_dumps_params = kwargs.pop("json_dumps_params", self.json_dumps_params) - - if self.json_dumps_params: - warnings.warn( - "json_dumps_params is deprecated, override encode_json instead", - DeprecationWarning, - ) - - self.json_encoder = DjangoJSONEncoder - - self.json_encoder = kwargs.pop("json_encoder", self.json_encoder) - - if self.json_encoder is not None: - warnings.warn( - "json_encoder is deprecated, override encode_json instead", - DeprecationWarning, - ) - - def parse_body(self, request: HttpRequest) -> Dict[str, Any]: - content_type = request.content_type or "" - - if "application/json" in content_type: - return json.loads(request.body) - elif content_type.startswith("multipart/form-data"): + def parse_body(self, request) -> Dict[str, Any]: + if request.content_type.startswith("multipart/form-data"): data = json.loads(request.POST.get("operations", "{}")) files_map = json.loads(request.POST.get("map", "{}")) data = replace_placeholders_with_files(data, files_map, request.FILES) return data - elif request.method.lower() == "get" and request.META.get("QUERY_STRING"): - return parse_query_params(request.GET.copy()) return json.loads(request.body) @@ -112,26 +71,20 @@ def is_request_allowed(self, request: HttpRequest) -> bool: return request.method.lower() in ("get", "post") def should_render_graphiql(self, request: HttpRequest) -> bool: - if request.method.lower() != "get": - return False - - if self.allow_queries_via_get and request.META.get("QUERY_STRING"): - return False - - return any( - supported_header in request.META.get("HTTP_ACCEPT", "") - for supported_header in ("text/html", "*/*") - ) + return "text/html" in request.META.get("HTTP_ACCEPT", "") def get_request_data(self, request: HttpRequest) -> GraphQLRequestData: try: data = self.parse_body(request) except json.decoder.JSONDecodeError: raise SuspiciousOperation("Unable to parse request body as JSON") - except KeyError: - raise BadRequest("File(s) missing in form data") - return parse_request_data(data) + try: + request_data = parse_request_data(data) + except MissingQueryError: + raise SuspiciousOperation("No GraphQL query found in the request") + + return request_data def _render_graphiql(self, request: HttpRequest, context=None): if not self.graphiql: @@ -140,7 +93,15 @@ def _render_graphiql(self, request: HttpRequest, context=None): try: template = Template(render_to_string("graphql/graphiql.html")) except TemplateDoesNotExist: - template = Template(get_graphiql_html(replace_variables=False)) + template = Template( + open( + os.path.join( + os.path.dirname(os.path.abspath(strawberry.__file__)), + "static/graphiql.html", + ), + "r", + ).read() + ) context = context or {} context.update({"SUBSCRIPTION_ENABLED": json.dumps(self.subscriptions_enabled)}) @@ -152,12 +113,11 @@ def _render_graphiql(self, request: HttpRequest, context=None): def _create_response( self, response_data: GraphQLHTTPResponse, sub_response: HttpResponse - ) -> HttpResponse: - data = self.encode_json(response_data) - - response = HttpResponse( - data, - content_type="application/json", + ) -> JsonResponse: + response = JsonResponse( + response_data, + encoder=self.json_encoder, + json_dumps_params=self.json_dumps_params, ) for name, value in sub_response.items(): @@ -171,19 +131,6 @@ def _create_response( return response - def encode_json(self, response_data: GraphQLHTTPResponse) -> str: - if self.json_dumps_params: - assert self.json_encoder - - return json.dumps( - response_data, cls=self.json_encoder, **self.json_dumps_params - ) - - if self.json_encoder: - return json.dumps(response_data, cls=self.json_encoder) - - return json.dumps(response_data) - class GraphQLView(BaseView): def get_root_value(self, request: HttpRequest) -> Any: @@ -211,29 +158,14 @@ def dispatch(self, request, *args, **kwargs): sub_response = TemporalHttpResponse() context = self.get_context(request, response=sub_response) - root_value = self.get_root_value(request) - - method = request.method - allowed_operation_types = OperationType.from_http(method) - - if not self.allow_queries_via_get and method == "GET": - allowed_operation_types = allowed_operation_types - {OperationType.QUERY} - assert self.schema - - try: - result = self.schema.execute_sync( - request_data.query, - root_value=root_value, - variable_values=request_data.variables, - context_value=context, - operation_name=request_data.operation_name, - allowed_operation_types=allowed_operation_types, - ) - except InvalidOperationTypeError as e: - raise BadRequest(e.as_http_error_reason(method)) from e - except MissingQueryError: - raise SuspiciousOperation("No GraphQL query found in the request") + result = self.schema.execute_sync( + request_data.query, + root_value=self.get_root_value(request), + variable_values=request_data.variables, + context_value=context, + operation_name=request_data.operation_name, + ) response_data = self.process_result(request=request, result=result) @@ -249,7 +181,7 @@ def as_view(cls, **initkwargs): # https://docs.djangoproject.com/en/3.1/topics/async/#async-views view = super().as_view(**initkwargs) - view._is_coroutine = asyncio.coroutines._is_coroutine # type: ignore[attr-defined] # noqa: E501 + view._is_coroutine = asyncio.coroutines._is_coroutine return view @method_decorator(csrf_exempt) @@ -268,28 +200,13 @@ async def dispatch(self, request, *args, **kwargs): context = await self.get_context(request, response=sub_response) root_value = await self.get_root_value(request) - method = request.method - - allowed_operation_types = OperationType.from_http(method) - - if not self.allow_queries_via_get and method == "GET": - allowed_operation_types = allowed_operation_types - {OperationType.QUERY} - - assert self.schema - - try: - result = await self.schema.execute( - request_data.query, - root_value=root_value, - variable_values=request_data.variables, - context_value=context, - operation_name=request_data.operation_name, - allowed_operation_types=allowed_operation_types, - ) - except InvalidOperationTypeError as e: - raise BadRequest(e.as_http_error_reason(method)) from e - except MissingQueryError: - raise SuspiciousOperation("No GraphQL query found in the request") + result = await self.schema.execute( + request_data.query, + root_value=root_value, + variable_values=request_data.variables, + context_value=context, + operation_name=request_data.operation_name, + ) response_data = await self.process_result(request=request, result=result) diff --git a/strawberry/enum.py b/strawberry/enum.py index bbf527ff0f..b236dec9c0 100644 --- a/strawberry/enum.py +++ b/strawberry/enum.py @@ -1,16 +1,6 @@ import dataclasses from enum import EnumMeta -from typing import ( - Any, - Callable, - Iterable, - List, - Mapping, - Optional, - TypeVar, - Union, - overload, -) +from typing import Any, Callable, List, Mapping, Optional, TypeVar, Union, overload from strawberry.type import StrawberryType @@ -21,9 +11,6 @@ class EnumValue: name: str value: Any - deprecation_reason: Optional[str] = None - directives: Iterable[object] = () - description: Optional[str] = None @dataclasses.dataclass @@ -32,7 +19,6 @@ class EnumDefinition(StrawberryType): name: str values: List[EnumValue] description: Optional[str] - directives: Iterable[object] = () def __hash__(self) -> int: # TODO: Is this enough for unique-ness? @@ -41,45 +27,18 @@ def __hash__(self) -> int: def copy_with( self, type_var_map: Mapping[TypeVar, Union[StrawberryType, type]] ) -> Union[StrawberryType, type]: - # enum don't support type parameters, so we can safely return self - return self + return super().copy_with(type_var_map) @property def is_generic(self) -> bool: return False -# TODO: remove duplication of EnumValueDefinition and EnumValue -@dataclasses.dataclass -class EnumValueDefinition: - value: Any - deprecation_reason: Optional[str] = None - directives: Iterable[object] = () - description: Optional[str] = None - - -def enum_value( - value: Any, - deprecation_reason: Optional[str] = None, - directives: Iterable[object] = (), - description: Optional[str] = None, -) -> EnumValueDefinition: - return EnumValueDefinition( - value=value, - deprecation_reason=deprecation_reason, - directives=directives, - description=description, - ) - - EnumType = TypeVar("EnumType", bound=EnumMeta) def _process_enum( - cls: EnumType, - name: Optional[str] = None, - description: Optional[str] = None, - directives: Iterable[object] = (), + cls: EnumType, name: Optional[str] = None, description: Optional[str] = None ) -> EnumType: if not isinstance(cls, EnumMeta): raise ObjectIsNotAnEnumError(cls) @@ -89,73 +48,32 @@ def _process_enum( description = description - values = [] - for item in cls: # type: ignore - item_value = item.value - item_name = item.name - deprecation_reason = None - item_directives: Iterable[object] = () - enum_value_description = None - - if isinstance(item_value, EnumValueDefinition): - item_directives = item_value.directives - enum_value_description = item_value.description - deprecation_reason = item_value.deprecation_reason - item_value = item_value.value - - # update _value2member_map_ so that doing `MyEnum.MY_VALUE` and - # `MyEnum['MY_VALUE']` both work - cls._value2member_map_[item_value] = item - cls._member_map_[item_name]._value_ = item_value - - value = EnumValue( - item_name, - item_value, - deprecation_reason=deprecation_reason, - directives=item_directives, - description=enum_value_description, - ) - values.append(value) + values = [EnumValue(item.name, item.value) for item in cls] # type: ignore cls._enum_definition = EnumDefinition( # type: ignore wrapped_cls=cls, name=name, values=values, description=description, - directives=directives, ) return cls @overload -def enum( - _cls: EnumType, - *, - name: Optional[str] = None, - description: Optional[str] = None, - directives: Iterable[object] = () -) -> EnumType: +def enum(_cls: EnumType, *, name=None, description=None) -> EnumType: ... @overload def enum( - _cls: None = None, - *, - name: Optional[str] = None, - description: Optional[str] = None, - directives: Iterable[object] = () + _cls: None = None, *, name=None, description=None ) -> Callable[[EnumType], EnumType]: ... def enum( - _cls: Optional[EnumType] = None, - *, - name: Optional[str] = None, - description: Optional[str] = None, - directives: Iterable[object] = () + _cls: Optional[EnumType] = None, *, name=None, description=None ) -> Union[EnumType, Callable[[EnumType], EnumType]]: """Registers the enum in the GraphQL type system. @@ -164,7 +82,7 @@ def enum( """ def wrap(cls: EnumType) -> EnumType: - return _process_enum(cls, name, description, directives=directives) + return _process_enum(cls, name, description) if not _cls: return wrap diff --git a/strawberry/exceptions.py b/strawberry/exceptions.py new file mode 100644 index 0000000000..6c121477a8 --- /dev/null +++ b/strawberry/exceptions.py @@ -0,0 +1,233 @@ +from __future__ import annotations + +from enum import Enum +from typing import List, Set, Union + +from graphql import GraphQLObjectType + +from strawberry.type import StrawberryType + + +# TODO: add links to docs +# https://github.com/strawberry-graphql/strawberry/issues/1298 + + +class ObjectIsNotAnEnumError(Exception): + def __init__(self, obj: object): + message = ( + "strawberry.enum can only be used with subclasses of Enum. " + f"Provided object {obj} is not an enum." + ) + + super().__init__(message) + + +class ObjectIsNotClassError(Exception): + class MethodType(Enum): + INPUT = "input" + INTERFACE = "interface" + TYPE = "type" + + def __init__(self, obj: object, method_type: MethodType): + message = ( + f"strawberry.{method_type.value} can only be used with class types. " + f"Provided object {obj} is not a type." + ) + + super().__init__(message) + + @classmethod + def input(cls, obj: object) -> ObjectIsNotClassError: + return cls(obj, cls.MethodType.INPUT) + + @classmethod + def interface(cls, obj: object) -> ObjectIsNotClassError: + return cls(obj, cls.MethodType.INTERFACE) + + @classmethod + def type(cls, obj: object) -> ObjectIsNotClassError: + return cls(obj, cls.MethodType.TYPE) + + +class MissingReturnAnnotationError(Exception): + """The field is missing the return annotation""" + + def __init__(self, field_name: str): + message = ( + f'Return annotation missing for field "{field_name}", ' + "did you forget to add it?" + ) + + super().__init__(message) + + +class MissingArgumentsAnnotationsError(Exception): + """The field is missing the annotation for one or more arguments""" + + def __init__(self, field_name: str, arguments: Set[str]): + arguments_list: List[str] = sorted(list(arguments)) + + if len(arguments_list) == 1: + argument = f'argument "{arguments_list[0]}"' + else: + head = ", ".join(arguments_list[:-1]) + argument = f'arguments "{head}" and "{arguments_list[-1]}"' + + message = ( + f"Missing annotation for {argument} " + f'in field "{field_name}", did you forget to add it?' + ) + + super().__init__(message) + + +class WrongReturnTypeForUnion(Exception): + """The Union type cannot be resolved because it's not a field""" + + def __init__(self, field_name: str, result_type: str): + message = ( + f'The type "{result_type}" cannot be resolved for the field "{field_name}" ' + ", are you using a strawberry.field?" + ) + + super().__init__(message) + + +class UnallowedReturnTypeForUnion(Exception): + """The return type is not in the list of Union types""" + + def __init__( + self, field_name: str, result_type: str, allowed_types: Set[GraphQLObjectType] + ): + formatted_allowed_types = list(sorted(type_.name for type_ in allowed_types)) + + message = ( + f'The type "{result_type}" of the field "{field_name}" ' + f'is not in the list of the types of the union: "{formatted_allowed_types}"' + ) + + super().__init__(message) + + +class InvalidUnionType(Exception): + """The union is constructed with an invalid type""" + + +class MissingTypesForGenericError(Exception): + """Raised when a generic types was used without passing any type.""" + + def __init__(self, annotation: Union[StrawberryType, type]): + message = ( + f'The type "{repr(annotation)}" is generic, but no type has been passed' + ) + + super().__init__(message) + + +class UnsupportedTypeError(Exception): + def __init__(self, annotation): + message = f"{annotation} conversion is not supported" + + super().__init__(message) + + +class MissingFieldAnnotationError(Exception): + def __init__(self, field_name: str): + message = ( + f'Unable to determine the type of field "{field_name}". Either ' + f"annotate it directly, or provide a typed resolver using " + f"@strawberry.field." + ) + + super().__init__(message) + + +class PrivateStrawberryFieldError(Exception): + def __init__(self, field_name: str, type_name: str): + message = ( + f"Field {field_name} on type {type_name} cannot be both " + "private and a strawberry.field" + ) + + super().__init__(message) + + +class MultipleStrawberryArgumentsError(Exception): + def __init__(self, argument_name: str): + message = ( + f"Annotation for argument `{argument_name}` cannot have multiple " + f"`strawberry.argument`s" + ) + + super().__init__(message) + + +class ScalarAlreadyRegisteredError(Exception): + def __init__(self, scalar_name: str): + message = f"Scalar `{scalar_name}` has already been registered" + + super().__init__(message) + + +class WrongNumberOfResultsReturned(Exception): + def __init__(self, expected: int, received: int): + message = ( + "Received wrong number of results in dataloader, " + f"expected: {expected}, received: {received}" + ) + + super().__init__(message) + + +class FieldWithResolverAndDefaultValueError(Exception): + def __init__(self, field_name: str, type_name: str): + message = ( + f'Field "{field_name}" on type "{type_name}" cannot define a default ' + "value and a resolver." + ) + + super().__init__(message) + + +class FieldWithResolverAndDefaultFactoryError(Exception): + def __init__(self, field_name: str, type_name: str): + message = ( + f'Field "{field_name}" on type "{type_name}" cannot define a ' + "default_factory and a resolver." + ) + + super().__init__(message) + + +class MissingQueryError(Exception): + def __init__(self): + message = 'Request data is missing a "query" value' + + super().__init__(message) + + +class InvalidFieldArgument(Exception): + def __init__(self, field_name: str, argument_name: str, argument_type: str): + message = ( + f'Argument "{argument_name}" on field "{field_name}" cannot be of type ' + f'"{argument_type}"' + ) + super().__init__(message) + + +class InvalidDefaultFactoryError(Exception): + def __init__(self): + message = "`default_factory` must be a callable that requires no arguments" + + super().__init__(message) + + +class InvalidCustomContext(Exception): + """Raised when a custom context object is of the wrong python type""" + + def __init__(self): + message = ( + "The custom context must be either a class " + "that inherits from BaseContext or a dictionary" + ) + super().__init__(message) diff --git a/strawberry/exceptions/__init__.py b/strawberry/exceptions/__init__.py deleted file mode 100644 index 8b34c08549..0000000000 --- a/strawberry/exceptions/__init__.py +++ /dev/null @@ -1,186 +0,0 @@ -from __future__ import annotations - -from typing import Optional, Set, Union - -from graphql import GraphQLError, GraphQLInputObjectType, GraphQLObjectType - -from strawberry.type import StrawberryType -from strawberry.utils.cached_property import cached_property - -from .duplicated_type_name import DuplicatedTypeName -from .exception import StrawberryException, UnableToFindExceptionSource -from .exception_source import ExceptionSource -from .handler import setup_exception_handler -from .invalid_argument_type import InvalidArgumentTypeError -from .invalid_union_type import InvalidTypeForUnionMergeError, InvalidUnionTypeError -from .missing_arguments_annotations import MissingArgumentsAnnotationsError -from .missing_field_annotation import MissingFieldAnnotationError -from .missing_return_annotation import MissingReturnAnnotationError -from .not_a_strawberry_enum import NotAStrawberryEnumError -from .object_is_not_a_class import ObjectIsNotClassError -from .object_is_not_an_enum import ObjectIsNotAnEnumError -from .private_strawberry_field import PrivateStrawberryFieldError -from .scalar_already_registered import ScalarAlreadyRegisteredError -from .unresolved_field_type import UnresolvedFieldTypeError - -setup_exception_handler() - - -# TODO: this doesn't seem to be tested -class WrongReturnTypeForUnion(Exception): - """The Union type cannot be resolved because it's not a field""" - - def __init__(self, field_name: str, result_type: str): - message = ( - f'The type "{result_type}" cannot be resolved for the field "{field_name}" ' - ", are you using a strawberry.field?" - ) - - super().__init__(message) - - -# TODO: this doesn't seem to be tested -class UnallowedReturnTypeForUnion(Exception): - """The return type is not in the list of Union types""" - - def __init__( - self, field_name: str, result_type: str, allowed_types: Set[GraphQLObjectType] - ): - formatted_allowed_types = list(sorted(type_.name for type_ in allowed_types)) - - message = ( - f'The type "{result_type}" of the field "{field_name}" ' - f'is not in the list of the types of the union: "{formatted_allowed_types}"' - ) - - super().__init__(message) - - -# TODO: this doesn't seem to be tested -class InvalidTypeInputForUnion(Exception): - def __init__(self, annotation: GraphQLInputObjectType): - message = f"Union for {annotation} is not supported because it is an Input type" - super().__init__(message) - - -# TODO: this doesn't seem to be tested -class MissingTypesForGenericError(Exception): - """Raised when a generic types was used without passing any type.""" - - def __init__(self, annotation: Union[StrawberryType, type]): - message = ( - f'The type "{repr(annotation)}" is generic, but no type has been passed' - ) - - super().__init__(message) - - -class UnsupportedTypeError(StrawberryException): - def __init__(self, annotation: Union[StrawberryType, type]): - message = f"{annotation} conversion is not supported" - - super().__init__(message) - - @cached_property - def exception_source(self) -> Optional[ExceptionSource]: - return None - - -class MultipleStrawberryArgumentsError(Exception): - def __init__(self, argument_name: str): - message = ( - f"Annotation for argument `{argument_name}` cannot have multiple " - f"`strawberry.argument`s" - ) - - super().__init__(message) - - -class WrongNumberOfResultsReturned(Exception): - def __init__(self, expected: int, received: int): - message = ( - "Received wrong number of results in dataloader, " - f"expected: {expected}, received: {received}" - ) - - super().__init__(message) - - -class FieldWithResolverAndDefaultValueError(Exception): - def __init__(self, field_name: str, type_name: str): - message = ( - f'Field "{field_name}" on type "{type_name}" cannot define a default ' - "value and a resolver." - ) - - super().__init__(message) - - -class FieldWithResolverAndDefaultFactoryError(Exception): - def __init__(self, field_name: str, type_name: str): - message = ( - f'Field "{field_name}" on type "{type_name}" cannot define a ' - "default_factory and a resolver." - ) - - super().__init__(message) - - -class MissingQueryError(Exception): - def __init__(self): - message = 'Request data is missing a "query" value' - - super().__init__(message) - - -class InvalidDefaultFactoryError(Exception): - def __init__(self): - message = "`default_factory` must be a callable that requires no arguments" - - super().__init__(message) - - -class InvalidCustomContext(Exception): - """Raised when a custom context object is of the wrong python type""" - - def __init__(self): - message = ( - "The custom context must be either a class " - "that inherits from BaseContext or a dictionary" - ) - super().__init__(message) - - -class StrawberryGraphQLError(GraphQLError): - """Use it when you want to override the graphql.GraphQLError in custom extensions""" - - -__all__ = [ - "StrawberryException", - "UnableToFindExceptionSource", - "MissingArgumentsAnnotationsError", - "MissingReturnAnnotationError", - "WrongReturnTypeForUnion", - "UnallowedReturnTypeForUnion", - "ObjectIsNotAnEnumError", - "ObjectIsNotClassError", - "InvalidUnionTypeError", - "InvalidTypeForUnionMergeError", - "MissingTypesForGenericError", - "UnsupportedTypeError", - "UnresolvedFieldTypeError", - "PrivateStrawberryFieldError", - "MultipleStrawberryArgumentsError", - "NotAStrawberryEnumError", - "ScalarAlreadyRegisteredError", - "WrongNumberOfResultsReturned", - "FieldWithResolverAndDefaultValueError", - "FieldWithResolverAndDefaultFactoryError", - "MissingQueryError", - "InvalidArgumentTypeError", - "InvalidDefaultFactoryError", - "InvalidCustomContext", - "MissingFieldAnnotationError", - "DuplicatedTypeName", - "StrawberryGraphQLError", -] diff --git a/strawberry/exceptions/duplicated_type_name.py b/strawberry/exceptions/duplicated_type_name.py deleted file mode 100644 index 63bc9e79b9..0000000000 --- a/strawberry/exceptions/duplicated_type_name.py +++ /dev/null @@ -1,75 +0,0 @@ -from __future__ import annotations - -from typing import TYPE_CHECKING, Optional, Type - -from strawberry.utils.cached_property import cached_property - -from .exception import StrawberryException -from .exception_source import ExceptionSource -from .utils.source_finder import SourceFinder - -if TYPE_CHECKING: - from rich.console import RenderableType - - -class DuplicatedTypeName(StrawberryException): - """Raised when the same type with different definition is reused inside a schema""" - - def __init__( - self, - first_cls: Optional[Type], - second_cls: Optional[Type], - duplicated_type_name: str, - ): - self.first_cls = first_cls - self.second_cls = second_cls - - self.message = ( - f"Type {duplicated_type_name} is defined multiple times in the schema" - ) - - self.rich_message = ( - f"Type `[underline]{duplicated_type_name}[/]` " - "is defined multiple times in the schema" - ) - - self.suggestion = ( - "To fix this error you should either rename the type or " - "remove the duplicated definition." - ) - - super().__init__(self.message) - - @property - def __rich_body__(self) -> RenderableType: - if self.first_cls is None or self.second_cls is None: - return "" - - from rich.console import Group - - source_finder = SourceFinder() - - first_class_source = self.exception_source - assert first_class_source - - second_class_source = source_finder.find_class_from_object(self.second_cls) - - if second_class_source is None: - return self._get_error_inline( - first_class_source, "first class defined here" - ) - - return Group( - self._get_error_inline(first_class_source, "first class defined here"), - "", - self._get_error_inline(second_class_source, "second class defined here"), - ) - - @cached_property - def exception_source(self) -> Optional[ExceptionSource]: - if self.first_cls is None: - return None # pragma: no cover - - source_finder = SourceFinder() - - return source_finder.find_class_from_object(self.first_cls) diff --git a/strawberry/exceptions/exception.py b/strawberry/exceptions/exception.py deleted file mode 100644 index 13bec919ac..0000000000 --- a/strawberry/exceptions/exception.py +++ /dev/null @@ -1,120 +0,0 @@ -from abc import ABC, abstractmethod -from typing import TYPE_CHECKING, Optional - -from strawberry.utils.cached_property import cached_property -from strawberry.utils.str_converters import to_kebab_case - -from .exception_source import ExceptionSource - -if TYPE_CHECKING: - from rich.console import RenderableType - - -class UnableToFindExceptionSource(Exception): - """Internal exception raised when we can't find the exception source.""" - - -class StrawberryException(Exception, ABC): - message: str - rich_message: str - suggestion: str - annotation_message: str - - def __init__(self, message: str) -> None: - self.message = message - - def __str__(self) -> str: - return self.message - - @property - def documentation_path(self) -> str: - return to_kebab_case(self.__class__.__name__.replace("Error", "")) - - @property - def documentation_url(self) -> str: - prefix = "https://errors.strawberry.rocks/" - - return prefix + self.documentation_path - - @cached_property - @abstractmethod - def exception_source(self) -> Optional[ExceptionSource]: - return None - - @property - def __rich_header__(self) -> "RenderableType": - return f"[bold red]error: {self.rich_message}" - - @property - def __rich_body__(self) -> "RenderableType": - assert self.exception_source - - return self._get_error_inline(self.exception_source, self.annotation_message) - - @property - def __rich_footer__(self) -> "RenderableType": - return ( - f"{self.suggestion}\n\n" - "Read more about this error on [bold underline]" - f"[link={self.documentation_url}]{self.documentation_url}" - ).strip() - - def __rich__(self) -> Optional["RenderableType"]: - from rich.box import SIMPLE - from rich.console import Group - from rich.panel import Panel - - if self.exception_source is None: - raise UnableToFindExceptionSource() from self - - content = ( - self.__rich_header__, - "", - self.__rich_body__, - "", - "", - self.__rich_footer__, - ) - - return Panel.fit( - Group(*content), - box=SIMPLE, - ) - - def _get_error_inline( - self, exception_source: ExceptionSource, message: str - ) -> "RenderableType": - source_file = exception_source.path - relative_path = exception_source.path_relative_to_cwd - error_line = exception_source.error_line - - from rich.console import Group - - from .syntax import Syntax - - path = f"[white] @ [link=file://{source_file}]{relative_path}:{error_line}" - - prefix = " " * exception_source.error_column - caret = "^" * ( - exception_source.error_column_end - exception_source.error_column - ) - - message = f"{prefix}[bold]{caret}[/] {message}" - - error_line = exception_source.error_line - line_annotations = {error_line: message} - - return Group( - path, - "", - Syntax( - code=exception_source.code, - highlight_lines={error_line}, - line_offset=exception_source.start_line - 1, - line_annotations=line_annotations, - line_range=( - exception_source.start_line - 1, - exception_source.end_line, - ), - ), - ) diff --git a/strawberry/exceptions/exception_source.py b/strawberry/exceptions/exception_source.py deleted file mode 100644 index 01a68d231c..0000000000 --- a/strawberry/exceptions/exception_source.py +++ /dev/null @@ -1,20 +0,0 @@ -from dataclasses import dataclass -from pathlib import Path - - -@dataclass -class ExceptionSource: - path: Path - code: str - start_line: int - end_line: int - error_line: int - error_column: int - error_column_end: int - - @property - def path_relative_to_cwd(self) -> Path: - if self.path.is_absolute(): - return self.path.relative_to(Path.cwd()) - - return self.path diff --git a/strawberry/exceptions/handler.py b/strawberry/exceptions/handler.py deleted file mode 100644 index be87962ada..0000000000 --- a/strawberry/exceptions/handler.py +++ /dev/null @@ -1,96 +0,0 @@ -import os -import sys -import threading -from types import TracebackType -from typing import Any, Callable, Optional, Tuple, Type, cast - -from .exception import StrawberryException, UnableToFindExceptionSource - -if sys.version_info >= (3, 8): - original_threading_exception_hook = threading.excepthook -else: - original_threading_exception_hook = None - - -ExceptionHandler = Callable[ - [Type[BaseException], BaseException, Optional[TracebackType]], None -] - - -def should_use_rich_exceptions(): - errors_disabled = os.environ.get("STRAWBERRY_DISABLE_RICH_ERRORS", "") - - return errors_disabled.lower() not in ["true", "1", "yes"] - - -def _get_handler(exception_type: Type[BaseException]) -> ExceptionHandler: - if issubclass(exception_type, StrawberryException): - try: - import rich - except ImportError: - pass - else: - - def _handler( - exception_type: Type[BaseException], - exception: BaseException, - traceback: Optional[TracebackType], - ): - try: - rich.print(exception) - - # we check if weren't able to find the exception source - # in that case we fallback to the original exception handler - except UnableToFindExceptionSource: - sys.__excepthook__(exception_type, exception, traceback) - - return _handler - - return sys.__excepthook__ - - -def strawberry_exception_handler( - exception_type: Type[BaseException], - exception: BaseException, - traceback: Optional[TracebackType], -): - _get_handler(exception_type)(exception_type, exception, traceback) - - -def strawberry_threading_exception_handler( - args: Tuple[ - Type[BaseException], - Optional[BaseException], - Optional[TracebackType], - Optional[threading.Thread], - ] -): - (exception_type, exception, traceback, _) = args - - if exception is None: - if sys.version_info >= (3, 8): - # this cast is only here because some weird issue with mypy - # and the inability to disable this error based on the python version - # (we'd need to do type ignore for python 3.8 and above, but mypy - # doesn't seem to be able to handle that and will complain in python 3.7) - - cast(Any, original_threading_exception_hook)(args) - - return - - _get_handler(exception_type)(exception_type, exception, traceback) - - -def reset_exception_handler(): - sys.excepthook = sys.__excepthook__ - - if sys.version_info >= (3, 8): - threading.excepthook = original_threading_exception_hook - - -def setup_exception_handler(): - if should_use_rich_exceptions(): - sys.excepthook = strawberry_exception_handler - - if sys.version_info >= (3, 8): - threading.excepthook = strawberry_threading_exception_handler diff --git a/strawberry/exceptions/invalid_argument_type.py b/strawberry/exceptions/invalid_argument_type.py deleted file mode 100644 index a1aac87c33..0000000000 --- a/strawberry/exceptions/invalid_argument_type.py +++ /dev/null @@ -1,68 +0,0 @@ -from __future__ import annotations - -from typing import TYPE_CHECKING, Optional - -from strawberry.utils.cached_property import cached_property - -from .exception import StrawberryException -from .exception_source import ExceptionSource -from .utils.source_finder import SourceFinder - -if TYPE_CHECKING: - from strawberry.arguments import StrawberryArgument - from strawberry.types.fields.resolver import StrawberryResolver - from strawberry.types.types import TypeDefinition - - -class InvalidArgumentTypeError(StrawberryException): - def __init__( - self, - resolver: StrawberryResolver, - argument: StrawberryArgument, - ): - from strawberry.union import StrawberryUnion - - self.function = resolver.wrapped_func - self.argument_name = argument.python_name - # argument_type: Literal["union", "interface"], - - argument_type = "unknown" - - if isinstance(argument.type, StrawberryUnion): - argument_type = "union" - else: - type_definition: Optional[TypeDefinition] = getattr( - argument.type, "_type_definition", None - ) - - if type_definition and type_definition.is_interface: - argument_type = "interface" - - self.message = ( - f'Argument "{self.argument_name}" on field ' - f'"{resolver.name}" cannot be of type ' - f'"{argument_type}"' - ) - self.rich_message = self.message - - if argument_type == "union": - self.suggestion = "Unions are not supported as arguments in GraphQL." - elif argument_type == "interface": - self.suggestion = "Interfaces are not supported as arguments in GraphQL." - else: - self.suggestion = f"{self.argument_name} is not supported as an argument." - - self.annotation_message = ( - f'Argument "{self.argument_name}" cannot be of type "{argument_type}"' - ) - - @cached_property - def exception_source(self) -> Optional[ExceptionSource]: - if self.function is None: - return None # pragma: no cover - - source_finder = SourceFinder() - - return source_finder.find_argument_from_object( - self.function, self.argument_name # type: ignore - ) diff --git a/strawberry/exceptions/invalid_union_type.py b/strawberry/exceptions/invalid_union_type.py deleted file mode 100644 index c7f9eb1bfc..0000000000 --- a/strawberry/exceptions/invalid_union_type.py +++ /dev/null @@ -1,82 +0,0 @@ -from __future__ import annotations - -from inspect import getframeinfo, stack -from pathlib import Path -from typing import TYPE_CHECKING, Optional, Type - -from strawberry.exceptions.utils.source_finder import SourceFinder -from strawberry.utils.cached_property import cached_property - -from .exception import StrawberryException -from .exception_source import ExceptionSource - -if TYPE_CHECKING: - from strawberry.union import StrawberryUnion - - -class InvalidUnionTypeError(StrawberryException): - """The union is constructed with an invalid type""" - - invalid_type: object - - def __init__(self, union_name: str, invalid_type: object) -> None: - from strawberry.custom_scalar import ScalarWrapper - - self.union_name = union_name - self.invalid_type = invalid_type - - # assuming that the exception happens two stack frames above the current one. - # one is our code checking for invalid types, the other is the caller - self.frame = getframeinfo(stack()[2][0]) - - if isinstance(invalid_type, ScalarWrapper): - type_name = invalid_type.wrap.__name__ - else: - type_name = invalid_type.__name__ # type: ignore - - self.message = f"Type `{type_name}` cannot be used in a GraphQL Union" - self.rich_message = ( - f"Type `[underline]{type_name}[/]` cannot be used in a GraphQL Union" - ) - self.suggestion = ( - "To fix this error you should replace the type a strawberry.type" - ) - self.annotation_message = "invalid type here" - - @cached_property - def exception_source(self) -> Optional[ExceptionSource]: - path = Path(self.frame.filename) - - source_finder = SourceFinder() - - return source_finder.find_union_call(path, self.union_name, self.invalid_type) - - -class InvalidTypeForUnionMergeError(StrawberryException): - """A specialized version of InvalidUnionTypeError for when trying - to merge unions using the pipe operator.""" - - invalid_type: Type - - def __init__(self, union: StrawberryUnion, other: object) -> None: - self.union = union - self.other = other - - # assuming that the exception happens two stack frames above the current one. - # one is our code checking for invalid types, the other is the caller - self.frame = getframeinfo(stack()[2][0]) - - other_name = getattr(other, "__name__", str(other)) - - self.message = f"`{other_name}` cannot be used when merging GraphQL Unions" - self.rich_message = ( - f"`[underline]{other_name}[/]` cannot be used when merging GraphQL Unions" - ) - self.suggestion = "" - self.annotation_message = "invalid type here" - - @cached_property - def exception_source(self) -> Optional[ExceptionSource]: - source_finder = SourceFinder() - - return source_finder.find_union_merge(self.union, self.other, frame=self.frame) diff --git a/strawberry/exceptions/missing_arguments_annotations.py b/strawberry/exceptions/missing_arguments_annotations.py deleted file mode 100644 index 58983398b7..0000000000 --- a/strawberry/exceptions/missing_arguments_annotations.py +++ /dev/null @@ -1,57 +0,0 @@ -from typing import TYPE_CHECKING, List, Optional - -from strawberry.utils.cached_property import cached_property - -from .exception import StrawberryException -from .exception_source import ExceptionSource -from .utils.source_finder import SourceFinder - -if TYPE_CHECKING: - from strawberry.types.fields.resolver import StrawberryResolver - - -class MissingArgumentsAnnotationsError(StrawberryException): - """The field is missing the annotation for one or more arguments""" - - def __init__(self, resolver: "StrawberryResolver", arguments: List[str]): - self.missing_arguments = arguments - self.function = resolver.wrapped_func - self.argument_name = arguments[0] - - self.message = ( - f"Missing annotation for {self.missing_arguments_str} " - f'in field "{resolver.name}", did you forget to add it?' - ) - self.rich_message = ( - f"Missing annotation for {self.missing_arguments_str} in " - f"`[underline]{resolver.name}[/]`" - ) - self.suggestion = ( - "To fix this error you can add an annotation to the argument " - f"like so [italic]`{self.missing_arguments[0]}: str`" - ) - - first = "first " if len(self.missing_arguments) > 1 else "" - - self.annotation_message = f"{first}argument missing annotation" - - @property - def missing_arguments_str(self): - arguments = self.missing_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) -> Optional[ExceptionSource]: - if self.function is None: - return None # pragma: no cover - - source_finder = SourceFinder() - - return source_finder.find_argument_from_object( - self.function, self.argument_name # type: ignore - ) diff --git a/strawberry/exceptions/missing_field_annotation.py b/strawberry/exceptions/missing_field_annotation.py deleted file mode 100644 index 738cbb50e4..0000000000 --- a/strawberry/exceptions/missing_field_annotation.py +++ /dev/null @@ -1,38 +0,0 @@ -from typing import Optional, Type - -from strawberry.utils.cached_property import cached_property - -from .exception import StrawberryException -from .exception_source import ExceptionSource -from .utils.source_finder import SourceFinder - - -class MissingFieldAnnotationError(StrawberryException): - def __init__(self, field_name: str, cls: Type): - self.cls = cls - self.field_name = field_name - - self.message = ( - f'Unable to determine the type of field "{field_name}". Either ' - f"annotate it directly, or provide a typed resolver using " - f"@strawberry.field." - ) - self.rich_message = ( - f"Missing annotation for field `[underline]{self.field_name}[/]`" - ) - self.suggestion = ( - "To fix this error you can add an annotation, " - f"like so [italic]`{self.field_name}: str`" - ) - self.annotation_message = "field missing annotation" - - super().__init__(self.message) - - @cached_property - def exception_source(self) -> Optional[ExceptionSource]: - if self.cls is None: - return None # pragma: no cover - - source_finder = SourceFinder() - - return source_finder.find_class_attribute_from_object(self.cls, self.field_name) diff --git a/strawberry/exceptions/missing_return_annotation.py b/strawberry/exceptions/missing_return_annotation.py deleted file mode 100644 index ab114d76a0..0000000000 --- a/strawberry/exceptions/missing_return_annotation.py +++ /dev/null @@ -1,40 +0,0 @@ -from typing import TYPE_CHECKING, Optional - -from strawberry.utils.cached_property import cached_property - -from .exception import StrawberryException -from .exception_source import ExceptionSource -from .utils.source_finder import SourceFinder - -if TYPE_CHECKING: - from strawberry.types.fields.resolver import StrawberryResolver - - -class MissingReturnAnnotationError(StrawberryException): - """The field is missing the return annotation""" - - def __init__(self, field_name: str, resolver: "StrawberryResolver"): - self.function = resolver.wrapped_func - - self.message = ( - f'Return annotation missing for field "{field_name}", ' - "did you forget to add it?" - ) - self.rich_message = ( - "[bold red]Missing annotation for field " f"`[underline]{resolver.name}[/]`" - ) - - self.suggestion = ( - "To fix this error you can add an annotation, " - f"like so [italic]`def {resolver.name}(...) -> str:`" - ) - self.annotation_message = "resolver missing annotation" - - @cached_property - def exception_source(self) -> Optional[ExceptionSource]: - if self.function is None: - return None # pragma: no cover - - source_finder = SourceFinder() - - return source_finder.find_function_from_object(self.function) # type: ignore diff --git a/strawberry/exceptions/not_a_strawberry_enum.py b/strawberry/exceptions/not_a_strawberry_enum.py deleted file mode 100644 index 206642ab1f..0000000000 --- a/strawberry/exceptions/not_a_strawberry_enum.py +++ /dev/null @@ -1,34 +0,0 @@ -from enum import EnumMeta -from typing import Optional - -from strawberry.utils.cached_property import cached_property - -from .exception import StrawberryException -from .exception_source import ExceptionSource -from .utils.source_finder import SourceFinder - - -class NotAStrawberryEnumError(StrawberryException): - def __init__(self, enum: EnumMeta): - self.enum = enum - - self.message = f'Enum "{enum.__name__}" is not a Strawberry enum.' - self.rich_message = ( - f"Enum `[underline]{enum.__name__}[/]` is not a Strawberry enum." - ) - self.suggestion = ( - "To fix this error you can declare the enum using `@strawberry.enum`." - ) - - self.annotation_message = "enum defined here" - - super().__init__(self.message) - - @cached_property - def exception_source(self) -> Optional[ExceptionSource]: - if self.enum is None: - return None # pragma: no cover - - source_finder = SourceFinder() - - return source_finder.find_class_from_object(self.enum) diff --git a/strawberry/exceptions/object_is_not_a_class.py b/strawberry/exceptions/object_is_not_a_class.py deleted file mode 100644 index 5bc97e8c6f..0000000000 --- a/strawberry/exceptions/object_is_not_a_class.py +++ /dev/null @@ -1,64 +0,0 @@ -from __future__ import annotations - -from enum import Enum -from typing import Optional - -from strawberry.utils.cached_property import cached_property - -from .exception import StrawberryException -from .exception_source import ExceptionSource -from .utils.source_finder import SourceFinder - - -class ObjectIsNotClassError(StrawberryException): - class MethodType(Enum): - INPUT = "input" - INTERFACE = "interface" - TYPE = "type" - - def __init__(self, obj: object, method_type: MethodType): - self.obj = obj - self.function = obj - - # TODO: assert obj is a function for now and skip the error if it is - # something else - obj_name = obj.__name__ # type: ignore - - self.message = ( - f"strawberry.{method_type.value} can only be used with class types. " - f"Provided object {obj_name} is not a type." - ) - - self.rich_message = ( - f"strawberry.{method_type.value} can only be used with class types. " - f"Provided object `[underline]{obj_name}[/]` is not a type." - ) - - self.annotation_message = "function defined here" - self.suggestion = ( - "To fix this error, make sure your use " - f"strawberry.{method_type.value} on a class." - ) - - super().__init__(self.message) - - @classmethod - def input(cls, obj: object) -> ObjectIsNotClassError: - return cls(obj, cls.MethodType.INPUT) - - @classmethod - def interface(cls, obj: object) -> ObjectIsNotClassError: - return cls(obj, cls.MethodType.INTERFACE) - - @classmethod - def type(cls, obj: object) -> ObjectIsNotClassError: - return cls(obj, cls.MethodType.TYPE) - - @cached_property - def exception_source(self) -> Optional[ExceptionSource]: - if self.function is None: - return None # pragma: no cover - - source_finder = SourceFinder() - - return source_finder.find_function_from_object(self.function) # type: ignore diff --git a/strawberry/exceptions/object_is_not_an_enum.py b/strawberry/exceptions/object_is_not_an_enum.py deleted file mode 100644 index 02ca074fa2..0000000000 --- a/strawberry/exceptions/object_is_not_an_enum.py +++ /dev/null @@ -1,36 +0,0 @@ -from enum import Enum -from typing import Optional, Type - -from strawberry.utils.cached_property import cached_property - -from .exception import StrawberryException -from .exception_source import ExceptionSource -from .utils.source_finder import SourceFinder - - -class ObjectIsNotAnEnumError(StrawberryException): - def __init__(self, cls: Type[Enum]): - self.cls = cls - self.message = ( - "strawberry.enum can only be used with subclasses of Enum. " - f"Provided object {cls.__name__} is not an enum." - ) - self.rich_message = ( - "strawberry.enum can only be used with subclasses of Enum. " - f"Provided object `[underline]{cls.__name__}[/]` is not an enum." - ) - self.annotation_message = "class defined here" - self.suggestion = ( - "To fix this error, make sure your class is a subclass of enum.Enum." - ) - - super().__init__(self.message) - - @cached_property - def exception_source(self) -> Optional[ExceptionSource]: - if self.cls is None: - return None # pragma: no cover - - source_finder = SourceFinder() - - return source_finder.find_class_from_object(self.cls) diff --git a/strawberry/exceptions/private_strawberry_field.py b/strawberry/exceptions/private_strawberry_field.py deleted file mode 100644 index 231154978e..0000000000 --- a/strawberry/exceptions/private_strawberry_field.py +++ /dev/null @@ -1,38 +0,0 @@ -from typing import Optional, Type - -from strawberry.utils.cached_property import cached_property - -from .exception import StrawberryException -from .exception_source import ExceptionSource -from .utils.source_finder import SourceFinder - - -class PrivateStrawberryFieldError(StrawberryException): - def __init__(self, field_name: str, cls: Type): - self.cls = cls - self.field_name = field_name - - self.message = ( - f"Field {field_name} on type {cls.__name__} cannot be both " - "private and a strawberry.field" - ) - self.rich_message = ( - f"`[underline]{self.field_name}[/]` field cannot be both " - "private and a strawberry.field " - ) - self.annotation_message = "private field defined here" - self.suggestion = ( - "To fix this error you should either make the field non private, " - "or remove the strawberry.field annotation." - ) - - super().__init__(self.message) - - @cached_property - def exception_source(self) -> Optional[ExceptionSource]: - if self.cls is None: - return None # pragma: no cover - - source_finder = SourceFinder() - - return source_finder.find_class_attribute_from_object(self.cls, self.field_name) diff --git a/strawberry/exceptions/scalar_already_registered.py b/strawberry/exceptions/scalar_already_registered.py deleted file mode 100644 index f407b3012d..0000000000 --- a/strawberry/exceptions/scalar_already_registered.py +++ /dev/null @@ -1,55 +0,0 @@ -from __future__ import annotations - -from pathlib import Path -from typing import TYPE_CHECKING, Optional - -from strawberry.exceptions.utils.source_finder import SourceFinder -from strawberry.utils.cached_property import cached_property - -from .exception import StrawberryException -from .exception_source import ExceptionSource - -if TYPE_CHECKING: - from strawberry.custom_scalar import ScalarDefinition - - -class ScalarAlreadyRegisteredError(StrawberryException): - def __init__( - self, - scalar_definition: ScalarDefinition, - other_scalar_definition: ScalarDefinition, - ): - self.scalar_definition = scalar_definition - - scalar_name = scalar_definition.name - - self.message = f"Scalar `{scalar_name}` has already been registered" - self.rich_message = ( - f"Scalar `[underline]{scalar_name}[/]` has already been registered" - ) - self.annotation_message = "scalar defined here" - self.suggestion = ( - "To fix this error you should either rename the scalar, " - "or reuse the existing one" - ) - if other_scalar_definition._source_file: - other_path = Path(other_scalar_definition._source_file) - other_line = other_scalar_definition._source_line - - self.suggestion += ( - f", defined in [bold white][link=file://{other_path}]" - f"{other_path.relative_to(Path.cwd())}:{other_line}[/]" - ) - - super().__init__(self.message) - - @cached_property - def exception_source(self) -> Optional[ExceptionSource]: - if not all( - (self.scalar_definition._source_file, self.scalar_definition._source_line) - ): - return None # pragma: no cover - - source_finder = SourceFinder() - - return source_finder.find_scalar_call(self.scalar_definition) diff --git a/strawberry/exceptions/syntax.py b/strawberry/exceptions/syntax.py deleted file mode 100644 index 52cc395f9e..0000000000 --- a/strawberry/exceptions/syntax.py +++ /dev/null @@ -1,59 +0,0 @@ -from typing import Dict, Optional, Set, Tuple - -from pygments.lexers import PythonLexer -from rich.console import Console, ConsoleOptions, RenderResult -from rich.segment import Segment -from rich.syntax import Syntax as RichSyntax - - -class Syntax(RichSyntax): - def __init__( - self, - code: str, - line_range: Tuple[int, int], - highlight_lines: Optional[Set[int]] = None, - line_offset: int = 0, - line_annotations: Optional[Dict[int, str]] = None, - ) -> None: - self.line_offset = line_offset - self.line_annotations = line_annotations or {} - - super().__init__( - code=code, - lexer=PythonLexer(), - line_numbers=True, - word_wrap=False, - theme="ansi_light", - highlight_lines=highlight_lines, - line_range=line_range, - ) - - def __rich_console__( - self, console: Console, options: ConsoleOptions - ) -> RenderResult: - assert self.line_range - - segments = self._get_syntax(console, options) - annotations = self.line_annotations.copy() - current_line = self.line_range[0] or 0 - - for segment in segments: - - if segment.text == "\n": - # 3 = | + space + space - prefix = " " * (self._numbers_column_width + 3) - - annotation = annotations.pop(current_line, None) - - current_line += 1 - - if annotation: - yield "" - yield prefix + annotation - - continue - - yield segment - - if segment.text.strip() == str(current_line): - yield Segment("| ") diff --git a/strawberry/exceptions/unresolved_field_type.py b/strawberry/exceptions/unresolved_field_type.py deleted file mode 100644 index affd471a85..0000000000 --- a/strawberry/exceptions/unresolved_field_type.py +++ /dev/null @@ -1,59 +0,0 @@ -from __future__ import annotations - -from typing import TYPE_CHECKING, Optional - -from strawberry.exceptions.utils.source_finder import SourceFinder -from strawberry.utils.cached_property import cached_property - -from .exception import StrawberryException -from .exception_source import ExceptionSource - -if TYPE_CHECKING: - from strawberry.field import StrawberryField - from strawberry.object_type import TypeDefinition - - -class UnresolvedFieldTypeError(StrawberryException): - def __init__( - self, - type_definition: TypeDefinition, - field: StrawberryField, - ): - self.type_definition = type_definition - self.field = field - - self.message = ( - f"Could not resolve the type of '{self.field.name}'. " - "Check that the class is accessible from the global module scope." - ) - - self.rich_message = ( - f"Could not resolve the type of [underline]'{self.field.name}'[/]. " - "Check that the class is accessible from the global module scope." - ) - self.annotation_message = "field defined here" - self.suggestion = ( - "To fix this error you should either import the type or use LazyType." - ) - - super().__init__(self.message) - - @cached_property - def exception_source(self) -> Optional[ExceptionSource]: - source_finder = SourceFinder() - - # field could be attached to the class or not - - source = source_finder.find_class_attribute_from_object( - self.type_definition.origin, self.field.name - ) - - if source is not None: - return source - - if self.field.base_resolver: - return source_finder.find_function_from_object( - self.field.base_resolver.wrapped_func # type: ignore - ) - - return None # pragma: no cover diff --git a/strawberry/exceptions/utils/source_finder.py b/strawberry/exceptions/utils/source_finder.py deleted file mode 100644 index 650e1386ba..0000000000 --- a/strawberry/exceptions/utils/source_finder.py +++ /dev/null @@ -1,474 +0,0 @@ -from __future__ import annotations - -import importlib -import importlib.util -import sys -from dataclasses import dataclass -from inspect import Traceback -from pathlib import Path -from typing import TYPE_CHECKING, Any, Callable, Optional, Sequence, Type, cast - -from strawberry.utils.cached_property import cached_property - -from ..exception_source import ExceptionSource - -if TYPE_CHECKING: - from libcst import CSTNode, FunctionDef - - from strawberry.custom_scalar import ScalarDefinition - from strawberry.union import StrawberryUnion - - -@dataclass -class SourcePath: - path: Path - code: str - - -class LibCSTSourceFinder: - def __init__(self) -> None: - self.cst = importlib.import_module("libcst") - - def find_source(self, module: str) -> Optional[SourcePath]: - # todo: support for pyodide - - source_module = sys.modules.get(module) - - path = None - - if source_module is None: - spec = importlib.util.find_spec(module) - - if spec is not None and spec.origin is not None: - path = Path(spec.origin) - elif source_module.__file__ is not None: - path = Path(source_module.__file__) - - if path is None: - return None - - if not path.exists() or path.suffix != ".py": - return None # pragma: no cover - - source = path.read_text() - - return SourcePath(path=path, code=source) - - def _find(self, source: str, matcher: Any) -> Sequence[CSTNode]: - from libcst.metadata import ( - MetadataWrapper, - ParentNodeProvider, - PositionProvider, - ) - - module = self.cst.parse_module(source) - self._metadata_wrapper = MetadataWrapper(module) - self._position_metadata = self._metadata_wrapper.resolve(PositionProvider) - self._parent_metadata = self._metadata_wrapper.resolve(ParentNodeProvider) - - import libcst.matchers as m - - return m.findall(self._metadata_wrapper, matcher) - - def _find_definition_by_qualname( - self, qualname: str, nodes: Sequence[CSTNode] - ) -> Optional[CSTNode]: - from libcst import ClassDef, CSTNode, FunctionDef - - for definition in nodes: - parent: Optional[CSTNode] = definition - stack = [] - - while parent: - if isinstance(parent, ClassDef): - stack.append(parent.name.value) - - if isinstance(parent, FunctionDef): - stack.extend(("", parent.name.value)) - - parent = self._parent_metadata.get(parent) - - if stack[0] == "": - stack.pop(0) - - found_class_name = ".".join(reversed(stack)) - - if found_class_name == qualname: - return definition - - return None - - def _find_function_definition( - self, source: SourcePath, function: Callable - ) -> Optional[FunctionDef]: - import libcst.matchers as m - from libcst import FunctionDef - - matcher = m.FunctionDef(name=m.Name(value=function.__name__)) - - function_defs = self._find(source.code, matcher) - - return cast( - FunctionDef, - self._find_definition_by_qualname(function.__qualname__, function_defs), - ) - - def _find_class_definition( - self, source: SourcePath, cls: Type - ) -> Optional[CSTNode]: - import libcst.matchers as m - - matcher = m.ClassDef(name=m.Name(value=cls.__name__)) - - class_defs = self._find(source.code, matcher) - return self._find_definition_by_qualname(cls.__qualname__, class_defs) - - def find_class(self, cls: Type) -> Optional[ExceptionSource]: - source = self.find_source(cls.__module__) - - if source is None: - return None # pragma: no cover - - class_def = self._find_class_definition(source, cls) - - if class_def is None: - return None # pragma: no cover - - position = self._position_metadata[class_def] - column_start = position.start.column + len("class ") - - return ExceptionSource( - path=source.path, - code=source.code, - start_line=position.start.line, - error_line=position.start.line, - end_line=position.end.line, - error_column=column_start, - error_column_end=column_start + len(cls.__name__), - ) - - def find_class_attribute( - self, cls: Type, attribute_name: str - ) -> Optional[ExceptionSource]: - source = self.find_source(cls.__module__) - - if source is None: - return None # pragma: no cover - - class_def = self._find_class_definition(source, cls) - - if class_def is None: - return None # pragma: no cover - - import libcst.matchers as m - from libcst import AnnAssign - - attribute_definitions = m.findall( - class_def, - m.AssignTarget(target=m.Name(value=attribute_name)) - | m.AnnAssign(target=m.Name(value=attribute_name)), - ) - - if not attribute_definitions: - return None - - attribute_definition = attribute_definitions[0] - - if isinstance(attribute_definition, AnnAssign): - attribute_definition = attribute_definition.target - - class_position = self._position_metadata[class_def] - attribute_position = self._position_metadata[attribute_definition] - - return ExceptionSource( - path=source.path, - code=source.code, - start_line=class_position.start.line, - error_line=attribute_position.start.line, - end_line=class_position.end.line, - error_column=attribute_position.start.column, - error_column_end=attribute_position.end.column, - ) - - def find_function(self, function: Callable) -> Optional[ExceptionSource]: - source = self.find_source(function.__module__) - - if source is None: - return None # pragma: no cover - - function_def = self._find_function_definition(source, function) - - if function_def is None: - return None # pragma: no cover - - position = self._position_metadata[function_def] - - prefix = f"def{function_def.whitespace_after_def.value}" - - if function_def.asynchronous: - prefix = f"async{function_def.asynchronous.whitespace_after.value}{prefix}" - - function_prefix = len(prefix) - error_column = position.start.column + function_prefix - error_column_end = error_column + len(function.__name__) - - return ExceptionSource( - path=source.path, - code=source.code, - start_line=position.start.line, - error_line=position.start.line, - end_line=position.end.line, - error_column=error_column, - error_column_end=error_column_end, - ) - - def find_argument( - self, function: Callable, argument_name: str - ) -> Optional[ExceptionSource]: - source = self.find_source(function.__module__) - - if source is None: - return None # pragma: no cover - - function_def = self._find_function_definition(source, function) - - if function_def is None: - return None # pragma: no cover - - import libcst.matchers as m - - argument_defs = m.findall( - function_def, - m.Param(name=m.Name(value=argument_name)), - ) - - if not argument_defs: - return None # pragma: no cover - - argument_def = argument_defs[0] - - function_position = self._position_metadata[function_def] - position = self._position_metadata[argument_def] - - return ExceptionSource( - path=source.path, - code=source.code, - start_line=function_position.start.line, - end_line=function_position.end.line, - error_line=position.start.line, - error_column=position.start.column, - error_column_end=position.end.column, - ) - - def find_union_call( - self, path: Path, union_name: str, invalid_type: object - ) -> Optional[ExceptionSource]: - import libcst.matchers as m - from libcst import Call - - source = path.read_text() - - invalid_type_name = getattr(invalid_type, "__name__", None) - - types_arg_matcher = ( - [ - m.Tuple( - elements=[ - m.ZeroOrMore(), - m.Element(value=m.Name(value=invalid_type_name)), - m.ZeroOrMore(), - ], - ) - | m.List( - elements=[ - m.ZeroOrMore(), - m.Element(value=m.Name(value=invalid_type_name)), - m.ZeroOrMore(), - ], - ) - ] - if invalid_type_name is not None - else [] - ) - - matcher = m.Call( - func=m.Attribute( - value=m.Name(value="strawberry"), - attr=m.Name(value="union"), - ) - | m.Name(value="union"), - args=[ - m.Arg(value=m.SimpleString(value=f"'{union_name}'")) - | m.Arg(value=m.SimpleString(value=f'"{union_name}"')), - m.Arg(*types_arg_matcher), # type: ignore - ], - ) - - union_calls = self._find(source, matcher) - - if not union_calls: - return None # pragma: no cover - - union_call = cast(Call, union_calls[0]) - - if invalid_type_name: - invalid_type_nodes = m.findall( - union_call.args[1], - m.Element(value=m.Name(value=invalid_type_name)), - ) - - if not invalid_type_nodes: - return None # pragma: no cover - - invalid_type_node = invalid_type_nodes[0] - else: - invalid_type_node = union_call - - position = self._position_metadata[union_call] - invalid_type_node_position = self._position_metadata[invalid_type_node] - - return ExceptionSource( - path=path, - code=source, - start_line=position.start.line, - error_line=invalid_type_node_position.start.line, - end_line=position.end.line, - error_column=invalid_type_node_position.start.column, - error_column_end=invalid_type_node_position.end.column, - ) - - def find_union_merge( - self, union: StrawberryUnion, other: object, frame: Traceback - ) -> Optional[ExceptionSource]: - import libcst.matchers as m - from libcst import BinaryOperation - - path = Path(frame.filename) - source = path.read_text() - - other_name = getattr(other, "__name__", None) - - if other_name is None: - return None # pragma: no cover - - matcher = m.BinaryOperation(operator=m.BitOr(), right=m.Name(value=other_name)) - - merge_calls = self._find(source, matcher) - - if not merge_calls: - return None # pragma: no cover - - merge_call_node = cast(BinaryOperation, merge_calls[0]) - invalid_type_node = merge_call_node.right - - position = self._position_metadata[merge_call_node] - invalid_type_node_position = self._position_metadata[invalid_type_node] - - return ExceptionSource( - path=path, - code=source, - start_line=position.start.line, - error_line=invalid_type_node_position.start.line, - end_line=position.end.line, - error_column=invalid_type_node_position.start.column, - error_column_end=invalid_type_node_position.end.column, - ) - - def find_scalar_call( - self, scalar_definition: ScalarDefinition - ) -> Optional[ExceptionSource]: - if scalar_definition._source_file is None: - return None # pragma: no cover - - import libcst.matchers as m - - path = Path(scalar_definition._source_file) - source = path.read_text() - - matcher = m.Call( - func=m.Attribute(value=m.Name(value="strawberry"), attr=m.Name("scalar")) - | m.Name("scalar"), - args=[ - m.ZeroOrMore(), - m.Arg( - keyword=m.Name(value="name"), - value=m.SimpleString(value=f"'{scalar_definition.name}'") - | m.SimpleString(value=f'"{scalar_definition.name}"'), - ), - m.ZeroOrMore(), - ], - ) - - scalar_calls = self._find(source, matcher) - - if not scalar_calls: - return None # pragma: no cover - - scalar_call_node = scalar_calls[0] - - argument_node = m.findall( - scalar_call_node, - m.Arg( - keyword=m.Name(value="name"), - ), - ) - - position = self._position_metadata[scalar_call_node] - argument_node_position = self._position_metadata[argument_node[0]] - - return ExceptionSource( - path=path, - code=source, - start_line=position.start.line, - end_line=position.end.line, - error_line=argument_node_position.start.line, - error_column=argument_node_position.start.column, - error_column_end=argument_node_position.end.column, - ) - - -class SourceFinder: - # TODO: this might need to become a getter - @cached_property - def cst(self) -> Optional[LibCSTSourceFinder]: - try: - return LibCSTSourceFinder() - except ImportError: - return None # pragma: no cover - - def find_class_from_object(self, cls: Type) -> Optional[ExceptionSource]: - return self.cst.find_class(cls) if self.cst else None - - def find_class_attribute_from_object( - self, cls: Type, attribute_name: str - ) -> Optional[ExceptionSource]: - return self.cst.find_class_attribute(cls, attribute_name) if self.cst else None - - def find_function_from_object( - self, function: Callable - ) -> Optional[ExceptionSource]: - return self.cst.find_function(function) if self.cst else None - - def find_argument_from_object( - self, function: Callable, argument_name: str - ) -> Optional[ExceptionSource]: - return self.cst.find_argument(function, argument_name) if self.cst else None - - def find_union_call( - self, path: Path, union_name: str, invalid_type: object - ) -> Optional[ExceptionSource]: - return ( - self.cst.find_union_call(path, union_name, invalid_type) - if self.cst - else None - ) - - def find_union_merge( - self, union: StrawberryUnion, other: object, frame: Traceback - ) -> Optional[ExceptionSource]: - return self.cst.find_union_merge(union, other, frame) if self.cst else None - - def find_scalar_call( - self, scalar_definition: ScalarDefinition - ) -> Optional[ExceptionSource]: - return self.cst.find_scalar_call(scalar_definition) if self.cst else None diff --git a/strawberry/experimental/pydantic/__init__.py b/strawberry/experimental/pydantic/__init__.py index 10f382650d..579052b095 100644 --- a/strawberry/experimental/pydantic/__init__.py +++ b/strawberry/experimental/pydantic/__init__.py @@ -2,6 +2,7 @@ from .exceptions import UnregisteredTypeException from .object_type import input, interface, type + __all__ = [ "error_type", "UnregisteredTypeException", diff --git a/strawberry/experimental/pydantic/conversion.py b/strawberry/experimental/pydantic/conversion.py index 0e34eaec02..91b81742bb 100644 --- a/strawberry/experimental/pydantic/conversion.py +++ b/strawberry/experimental/pydantic/conversion.py @@ -22,7 +22,7 @@ def _convert_from_pydantic_to_strawberry_type( if isinstance(type_, StrawberryUnion): for option_type in type_.types: if hasattr(option_type, "_pydantic_type"): - source_type = option_type._pydantic_type + source_type = option_type._pydantic_type # type: ignore else: source_type = cast(type, option_type) if isinstance(data, source_type): @@ -51,7 +51,7 @@ def _convert_from_pydantic_to_strawberry_type( if hasattr(type(data), "_strawberry_type"): type_ = type(data)._strawberry_type if hasattr(type_, "from_pydantic"): - return type_.from_pydantic(data_from_model, extra) + return type_.from_pydantic(data_from_model, extra) # type: ignore return convert_pydantic_model_to_strawberry_class( type_, model_instance=data_from_model, extra=extra ) diff --git a/strawberry/experimental/pydantic/conversion_types.py b/strawberry/experimental/pydantic/conversion_types.py index 91516f8bab..4db1afb46b 100644 --- a/strawberry/experimental/pydantic/conversion_types.py +++ b/strawberry/experimental/pydantic/conversion_types.py @@ -1,12 +1,13 @@ from __future__ import annotations -from typing import Any, Dict, Optional, TypeVar -from typing_extensions import Protocol +from typing import Any, Dict, TypeVar from pydantic import BaseModel +from typing_extensions import Protocol from strawberry.types.types import TypeDefinition + PydanticModel = TypeVar("PydanticModel", bound=BaseModel) @@ -19,11 +20,11 @@ def __init__(self, **kwargs): @staticmethod def from_pydantic( - instance: PydanticModel, extra: Optional[Dict[str, Any]] = None + instance: PydanticModel, extra: Dict[str, Any] = None ) -> StrawberryTypeFromPydantic[PydanticModel]: ... - def to_pydantic(self, **kwargs) -> PydanticModel: + def to_pydantic(self) -> PydanticModel: ... @property diff --git a/strawberry/experimental/pydantic/error_type.py b/strawberry/experimental/pydantic/error_type.py index 13b8b7836e..b8e2927c9a 100644 --- a/strawberry/experimental/pydantic/error_type.py +++ b/strawberry/experimental/pydantic/error_type.py @@ -4,15 +4,15 @@ from pydantic import BaseModel from pydantic.fields import ModelField -from pydantic.utils import lenient_issubclass -from strawberry.auto import StrawberryAuto +import strawberry from strawberry.experimental.pydantic.utils import ( get_private_fields, get_strawberry_type_from_model, normalize_type, ) from strawberry.object_type import _process_type, _wrap_dataclass +from strawberry.schema_directive import StrawberrySchemaDirective from strawberry.types.type_resolver import _get_fields from strawberry.utils.typing import get_list_annotation, is_list @@ -34,13 +34,13 @@ def field_type_to_type(type_): if is_list(child_type): strawberry_type = field_type_to_type(child_type) - elif lenient_issubclass(child_type, BaseModel): + elif issubclass(child_type, BaseModel): strawberry_type = get_strawberry_type_from_model(child_type) else: strawberry_type = List[error_class] strawberry_type = Optional[strawberry_type] - elif lenient_issubclass(type_, BaseModel): + elif issubclass(type_, BaseModel): strawberry_type = get_strawberry_type_from_model(type_) return Optional[strawberry_type] @@ -50,15 +50,15 @@ def field_type_to_type(type_): def error_type( model: Type[BaseModel], *, - fields: Optional[List[str]] = None, + fields: List[str] = None, name: Optional[str] = None, description: Optional[str] = None, - directives: Optional[Sequence[object]] = (), + directives: Optional[Sequence[StrawberrySchemaDirective]] = (), all_fields: bool = False ): def wrap(cls): model_fields = model.__fields__ - fields_set = set(fields) if fields else set() + fields_set = set(fields) if fields else set([]) if fields: warnings.warn( @@ -68,11 +68,7 @@ def wrap(cls): existing_fields = getattr(cls, "__annotations__", {}) fields_set = fields_set.union( - { - name - for name, type_ in existing_fields.items() - if isinstance(type_, StrawberryAuto) - } + set(name for name, typ in existing_fields.items() if typ == strawberry.auto) ) if all_fields: @@ -91,7 +87,7 @@ def wrap(cls): ( name, get_type_for_field(field), - dataclasses.field(default=None), # type: ignore[arg-type] + dataclasses.field(default=None), # type: ignore ) for name, field in model_fields.items() if name in fields_set @@ -103,12 +99,14 @@ def wrap(cls): all_model_fields.extend( ( - field.name, - field.type, - field, + ( + field.name, + field.type, + field, + ) + for field in extra_fields + private_fields + if field.type != strawberry.auto ) - for field in extra_fields + private_fields - if not isinstance(field.type, StrawberryAuto) ) cls = dataclasses.make_dataclass( @@ -126,8 +124,8 @@ def wrap(cls): directives=directives, ) - model._strawberry_type = cls # type: ignore[attr-defined] - cls._pydantic_type = model + model._strawberry_type = cls # type: ignore + cls._pydantic_type = model # type: ignore return cls return wrap diff --git a/strawberry/experimental/pydantic/exceptions.py b/strawberry/experimental/pydantic/exceptions.py index 007dd69abb..f112807be0 100644 --- a/strawberry/experimental/pydantic/exceptions.py +++ b/strawberry/experimental/pydantic/exceptions.py @@ -19,7 +19,7 @@ class UnsupportedTypeError(Exception): class UnregisteredTypeException(Exception): - def __init__(self, type: Type[BaseModel]): + def __init__(self, type: BaseModel): message = ( f"Cannot find a Strawberry Type for {type} did you forget to register it?" ) diff --git a/strawberry/experimental/pydantic/fields.py b/strawberry/experimental/pydantic/fields.py index f883f78e20..4898d543de 100644 --- a/strawberry/experimental/pydantic/fields.py +++ b/strawberry/experimental/pydantic/fields.py @@ -1,24 +1,11 @@ -import builtins from decimal import Decimal -from typing import Any, List, Optional, Type +from typing import Optional from uuid import UUID import pydantic -from pydantic import BaseModel -from pydantic.typing import get_args, get_origin, is_new_type, new_type_supertype -from pydantic.utils import lenient_issubclass +from pydantic.typing import is_new_type, new_type_supertype -from strawberry.experimental.pydantic.exceptions import ( - UnregisteredTypeException, - UnsupportedTypeError, -) -from strawberry.types.types import TypeDefinition - -try: - from typing import GenericAlias as TypingGenericAlias # type: ignore -except ImportError: - # python < 3.9 does not have GenericAlias (list[int], tuple[str, ...] and so on) - TypingGenericAlias = () +from .exceptions import UnsupportedTypeError ATTR_TO_TYPE_MAP = { @@ -29,6 +16,10 @@ "StrictStr": str, "ConstrainedBytes": bytes, "conbytes": bytes, + "ConstrainedList": None, + "conlist": None, + "ConstrainedSet": None, + "conset": None, "ConstrainedStr": str, "constr": str, "EmailStr": str, @@ -73,15 +64,12 @@ } -def get_basic_type(type_) -> Type[Any]: - if lenient_issubclass(type_, pydantic.ConstrainedInt): - return int - if lenient_issubclass(type_, pydantic.ConstrainedFloat): - return float - if lenient_issubclass(type_, pydantic.ConstrainedStr): - return str - if lenient_issubclass(type_, pydantic.ConstrainedList): - return List[get_basic_type(type_.item_type)] # type: ignore +def get_basic_type(type_): + if isinstance(type_, type): + if issubclass(type_, pydantic.ConstrainedInt): + return int + if issubclass(type_, pydantic.ConstrainedStr): + return str if type_ in FIELDS_MAP: type_ = FIELDS_MAP.get(type_) @@ -93,43 +81,3 @@ def get_basic_type(type_) -> Type[Any]: return new_type_supertype(type_) return type_ - - -def replace_pydantic_types(type_: Any, is_input: bool): - if lenient_issubclass(type_, BaseModel): - attr = "_strawberry_input_type" if is_input else "_strawberry_type" - if hasattr(type_, attr): - return getattr(type_, attr) - else: - raise UnregisteredTypeException(type_) - return type_ - - -def replace_types_recursively(type_: Any, is_input: bool) -> Any: - """Runs the conversions recursively into the arguments of generic types if any""" - basic_type = get_basic_type(type_) - replaced_type = replace_pydantic_types(basic_type, is_input) - - origin = get_origin(type_) - if not origin or not hasattr(type_, "__args__"): - return replaced_type - - converted = tuple( - replace_types_recursively(t, is_input=is_input) for t in get_args(replaced_type) - ) - - if isinstance(replaced_type, TypingGenericAlias): - return TypingGenericAlias(origin, converted) - - replaced_type = replaced_type.copy_with(converted) - - if isinstance(replaced_type, TypeDefinition): - # TODO: Not sure if this is necessary. No coverage in tests - # TODO: Unnecessary with StrawberryObject - replaced_type = builtins.type( - replaced_type.name, - (), - {"_type_definition": replaced_type}, - ) - - return replaced_type diff --git a/strawberry/experimental/pydantic/object_type.py b/strawberry/experimental/pydantic/object_type.py index 8c06c34dc9..9e10f38256 100644 --- a/strawberry/experimental/pydantic/object_type.py +++ b/strawberry/experimental/pydantic/object_type.py @@ -1,8 +1,9 @@ from __future__ import annotations +import builtins import dataclasses -import sys import warnings +from functools import partial from typing import ( TYPE_CHECKING, Any, @@ -16,38 +17,113 @@ cast, ) -from graphql import GraphQLResolveInfo +from pydantic import BaseModel from pydantic.fields import ModelField +from typing_extensions import Literal + +from graphql import GraphQLResolveInfo -from strawberry.annotation import StrawberryAnnotation -from strawberry.auto import StrawberryAuto +import strawberry +from strawberry.arguments import UNSET from strawberry.experimental.pydantic.conversion import ( convert_pydantic_model_to_strawberry_class, convert_strawberry_class_to_pydantic_model, ) -from strawberry.experimental.pydantic.exceptions import MissingFieldsListError -from strawberry.experimental.pydantic.fields import replace_types_recursively +from strawberry.experimental.pydantic.fields import get_basic_type +from strawberry.experimental.pydantic.orm import ( + is_orm_field, + is_ormar_field, + is_ormar_model, + is_sqlmodel_field, + replace_ormar_types, +) from strawberry.experimental.pydantic.utils import ( DataclassCreationFields, ensure_all_auto_fields_in_pydantic, get_default_factory_for_field, + get_model_fields, get_private_fields, + sort_creation_fields, ) from strawberry.field import StrawberryField from strawberry.object_type import _process_type, _wrap_dataclass +from strawberry.schema_directive import StrawberrySchemaDirective from strawberry.types.type_resolver import _get_fields -from strawberry.utils.dataclasses import add_custom_init_fn +from strawberry.types.types import TypeDefinition +from .exceptions import MissingFieldsListError -def get_type_for_field(field: ModelField, is_input: bool): - outer_type = field.outer_type_ - replaced_type = replace_types_recursively(outer_type, is_input) - if not field.required: - return Optional[replaced_type] - else: +try: + from typing import ForwardRef # type: ignore +except ImportError: # pragma: no cover + # ForwardRef is private in python 3.6 and 3.7 + from typing import _ForwardRef as ForwardRef # type: ignore + +from .lazy_types import LazyForwardRefType, LazyModelType + + +def replace_pydantic_types( + type_: Any, is_input: bool, model: Type[BaseModel], name: str +): + if isinstance(type_, ForwardRef): + return LazyForwardRefType[model, name] # type: ignore + + if is_ormar_model(model): + return replace_ormar_types(type_, model, name) + + origin = getattr(type_, "__origin__", None) + if origin is Literal: + # Literal does not have types in its __args__ so we return early + return type_ + if hasattr(type_, "__args__"): + replaced_type = type_.copy_with( + tuple( + replace_pydantic_types(t, is_input, model, name) for t in type_.__args__ + ) + ) + + if isinstance(replaced_type, TypeDefinition): + # TODO: Not sure if this is necessary. No coverage in tests + # TODO: Unnecessary with StrawberryObject + + replaced_type = builtins.type( + replaced_type.name, + (), + {"_type_definition": replaced_type}, + ) + return replaced_type + if issubclass(type_, BaseModel): + attr = "_strawberry_input_type" if is_input else "_strawberry_type" + if hasattr(type_, attr): + return getattr(type_, attr) + else: + return LazyModelType[type_] # type: ignore + + return type_ + + +def get_type_for_field(field: ModelField, is_input: bool, model, name): + model.update_forward_refs() + + if is_ormar_field(field): + type_ = replace_ormar_types(field, model, name) + elif is_sqlmodel_field(field): + type_ = replace_pydantic_types( + model.__annotations__[name], is_input, model, name + ) + else: + type_ = field.outer_type_ + type_ = get_basic_type(type_) + type_ = replace_pydantic_types(type_, is_input, model, name) + + if not field.required: + type_ = Optional[type_] + + return type_ + def _build_dataclass_creation_fields( field: ModelField, @@ -58,10 +134,18 @@ def _build_dataclass_creation_fields( model: Type[BaseModel], name: str, ) -> DataclassCreationFields: - field_type = ( - get_type_for_field(field, is_input) - if field.name in auto_fields_set - else existing_fields[field.name].type + if is_orm_field(field): + name_ = name + graphql_name, description = None, None + else: + graphql_name = field.alias if field.has_alias and use_pydantic_alias else None + name_ = field.name + description = field.field_info.description + + type_annotation = ( + get_type_for_field(field, is_input, model, name) + if name_ in auto_fields_set + else existing_fields[name_].type ) if name_ in existing_fields and existing_fields[name_].base_resolver is not None: @@ -69,34 +153,19 @@ def _build_dataclass_creation_fields( strawberry_field = existing_fields[name_] else: # otherwise we build an appropriate strawberry field that resolves it - existing_field = existing_fields.get(field.name) - graphql_name = None - if existing_field and existing_field.graphql_name: - graphql_name = existing_field.graphql_name - elif field.has_alias and use_pydantic_alias: - graphql_name = field.alias - strawberry_field = StrawberryField( - python_name=field.name, + python_name=name_, graphql_name=graphql_name, # always unset because we use default_factory instead - default=dataclasses.MISSING, + default=UNSET, default_factory=get_default_factory_for_field(field), - type_annotation=StrawberryAnnotation.from_annotation(field_type), - description=field.field_info.description, - deprecation_reason=( - existing_field.deprecation_reason if existing_field else None - ), - permission_classes=( - existing_field.permission_classes if existing_field else [] - ), - directives=existing_field.directives if existing_field else (), - metadata=existing_field.metadata if existing_field else {}, + type_annotation=type_annotation, + description=description, ) return DataclassCreationFields( - name=field.name, - field_type=field_type, + name=name_, + type_annotation=type_annotation, field=strawberry_field, ) @@ -118,13 +187,13 @@ def type( is_input: bool = False, is_interface: bool = False, description: Optional[str] = None, - directives: Optional[Sequence[object]] = (), + directives: Optional[Sequence[StrawberrySchemaDirective]] = (), all_fields: bool = False, use_pydantic_alias: bool = True, ) -> Callable[..., Type[StrawberryTypeFromPydantic[PydanticModel]]]: def wrap(cls: Any) -> Type[StrawberryTypeFromPydantic[PydanticModel]]: - model_fields = model.__fields__ - original_fields_set = set(fields) if fields else set() + model_fields = get_model_fields(model) + original_fields_set = set(fields) if fields else set([]) if fields: warnings.warn( @@ -133,20 +202,15 @@ def wrap(cls: Any) -> Type[StrawberryTypeFromPydantic[PydanticModel]]: ) existing_fields = getattr(cls, "__annotations__", {}) - # these are the fields that matched a field name in the pydantic model # and should copy their alias from the pydantic model fields_set = original_fields_set.union( - {name for name, _ in existing_fields.items() if name in model_fields} + set(name for name, _ in existing_fields.items() if name in model_fields) ) # these are the fields that were marked with strawberry.auto and # should copy their type from the pydantic model auto_fields_set = original_fields_set.union( - { - name - for name, type_ in existing_fields.items() - if isinstance(type_, StrawberryAuto) - } + set(name for name, typ in existing_fields.items() if typ == strawberry.auto) ) if all_fields or exclude: @@ -195,15 +259,20 @@ def wrap(cls: Any) -> Type[StrawberryTypeFromPydantic[PydanticModel]]: if field_name in fields_set ] - all_model_fields = [ - DataclassCreationFields( - name=field.name, - field_type=field.type, - field=field, + all_model_fields.extend( + ( + DataclassCreationFields( + name=field.name, + type_annotation=field.type, + field=field, + ) + for field in extra_fields + private_fields + if field.name not in fields_set ) - for field in extra_fields + private_fields - if field.name not in fields_set - ] + all_model_fields + ) + + # Sort fields so that fields with missing defaults go first + sorted_fields = sort_creation_fields(all_model_fields) # Implicitly define `is_type_of` to support interfaces/unions that use # pydantic objects (not the corresponding strawberry type) @@ -229,29 +298,13 @@ def is_type_of(cls: Type, obj: Any, _info: GraphQLResolveInfo) -> bool: if has_custom_to_pydantic: namespace["to_pydantic"] = cls.to_pydantic - kwargs: Dict[str, object] = {} - - # Python 3.10.1 introduces the kw_only param to `make_dataclass`. - # If we're on an older version then generate our own custom init function - # Note: Python 3.10.0 added the `kw_only` param to dataclasses, it was - # just missed from the `make_dataclass` function: - # https://github.com/python/cpython/issues/89961 - if sys.version_info >= (3, 10, 1): - kwargs["kw_only"] = dataclasses.MISSING - else: - kwargs["init"] = False - cls = dataclasses.make_dataclass( cls.__name__, - [field.to_tuple() for field in all_model_fields], + [field.to_tuple() for field in sorted_fields], bases=cls.__bases__, namespace=namespace, - **kwargs, # type: ignore ) - if sys.version_info < (3, 10, 1): - add_custom_init_fn(cls) - _process_type( cls, name=name, @@ -265,23 +318,22 @@ def is_type_of(cls: Type, obj: Any, _info: GraphQLResolveInfo) -> bool: model._strawberry_input_type = cls # type: ignore else: model._strawberry_type = cls # type: ignore - cls._pydantic_type = model + cls._pydantic_type = model # type: ignore def from_pydantic_default( - instance: PydanticModel, extra: Optional[Dict[str, Any]] = None + instance: PydanticModel, extra: Dict[str, Any] = None ) -> StrawberryTypeFromPydantic[PydanticModel]: return convert_pydantic_model_to_strawberry_class( cls=cls, model_instance=instance, extra=extra ) - def to_pydantic_default(self, **kwargs) -> PydanticModel: + def to_pydantic_default(self) -> PydanticModel: instance_kwargs = { f.name: convert_strawberry_class_to_pydantic_model( getattr(self, f.name) ) for f in dataclasses.fields(self) } - instance_kwargs.update(kwargs) return model(**instance_kwargs) if not has_custom_from_pydantic: @@ -294,57 +346,6 @@ def to_pydantic_default(self, **kwargs) -> PydanticModel: return wrap -def input( - model: Type[PydanticModel], - *, - fields: Optional[List[str]] = None, - name: Optional[str] = None, - is_interface: bool = False, - description: Optional[str] = None, - directives: Optional[Sequence[object]] = (), - all_fields: bool = False, - use_pydantic_alias: bool = True, -) -> Callable[..., Type[StrawberryTypeFromPydantic[PydanticModel]]]: - """Convenience decorator for creating an input type from a Pydantic model. - Equal to partial(type, is_input=True) - See https://github.com/strawberry-graphql/strawberry/issues/1830 - """ - return type( - model=model, - fields=fields, - name=name, - is_input=True, - is_interface=is_interface, - description=description, - directives=directives, - all_fields=all_fields, - use_pydantic_alias=use_pydantic_alias, - ) - +input = partial(type, is_input=True) -def interface( - model: Type[PydanticModel], - *, - fields: Optional[List[str]] = None, - name: Optional[str] = None, - is_input: bool = False, - description: Optional[str] = None, - directives: Optional[Sequence[object]] = (), - all_fields: bool = False, - use_pydantic_alias: bool = True, -) -> Callable[..., Type[StrawberryTypeFromPydantic[PydanticModel]]]: - """Convenience decorator for creating an interface type from a Pydantic model. - Equal to partial(type, is_interface=True) - See https://github.com/strawberry-graphql/strawberry/issues/1830 - """ - return type( - model=model, - fields=fields, - name=name, - is_input=is_input, - is_interface=True, - description=description, - directives=directives, - all_fields=all_fields, - use_pydantic_alias=use_pydantic_alias, - ) +interface = partial(type, is_interface=True) diff --git a/strawberry/experimental/pydantic/utils.py b/strawberry/experimental/pydantic/utils.py index 8c2611e543..c7eccea02a 100644 --- a/strawberry/experimental/pydantic/utils.py +++ b/strawberry/experimental/pydantic/utils.py @@ -6,6 +6,7 @@ from pydantic.typing import NoArgAnyCallable from pydantic.utils import smart_deepcopy +from strawberry.arguments import UNSET, _Unset, is_unset # type: ignore from strawberry.experimental.pydantic.exceptions import ( AutoFieldsNotInBaseModelError, BothDefaultAndDefaultFactoryDefinedError, @@ -14,11 +15,11 @@ from strawberry.experimental.pydantic.orm import ( get_ormar_accessors, get_sqlmodel_relationships, + is_orm_field, is_ormar_model, is_sqlmodel_model, ) from strawberry.private import is_private -from strawberry.unset import UNSET from strawberry.utils.typing import ( get_list_annotation, get_optional_annotation, @@ -58,35 +59,48 @@ class DataclassCreationFields(NamedTuple): """Fields required for the fields parameter of make_dataclass""" name: str - field_type: Type + type_annotation: Type field: dataclasses.Field def to_tuple(self) -> Tuple[str, Type, dataclasses.Field]: # fields parameter wants (name, type, Field) - return self.name, self.field_type, self.field + return self.name, self.type_annotation, self.field -def get_default_factory_for_field( - field: ModelField, -) -> Union[NoArgAnyCallable, dataclasses._MISSING_TYPE]: +def sort_creation_fields( + fields: List[DataclassCreationFields], +) -> List[DataclassCreationFields]: + """ + Sort fields so that fields with missing defaults go first + because dataclasses require that fields with no defaults are defined + first + """ + + def has_default(model_field: DataclassCreationFields) -> bool: + """Check if field has defaults.""" + return (model_field.field.default is not dataclasses.MISSING) or ( + model_field.field.default_factory is not dataclasses.MISSING # type: ignore + ) + + return sorted(fields, key=has_default) + + +def get_default_factory_for_field(field: ModelField) -> Union[NoArgAnyCallable, _Unset]: """ Gets the default factory for a pydantic field. - Handles mutable defaults when making the dataclass by - using pydantic's smart_deepcopy + Handles mutable defaults when making the dataclass by using pydantic's smart_deepcopy Returns optionally a NoArgAnyCallable representing a default_factory parameter """ - # replace dataclasses.MISSING with our own UNSET to make comparisons easier - default_factory = ( - field.default_factory - if field.default_factory is not dataclasses.MISSING - else UNSET - ) - default = field.default if field.default is not dataclasses.MISSING else UNSET + if is_orm_field(field): + return UNSET + + default_factory = field.default_factory + default = field.default - has_factory = default_factory is not None and default_factory is not UNSET - has_default = default is not None and default is not UNSET + has_factory = default_factory is not None and not is_unset(default_factory) + has_default = default is not None and not is_unset(default) # defining both default and default_factory is not supported @@ -115,7 +129,7 @@ def get_default_factory_for_field( if not field.required: return lambda: None - return dataclasses.MISSING + return UNSET def ensure_all_auto_fields_in_pydantic( diff --git a/strawberry/ext/__init__.py b/strawberry/ext/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/strawberry/ext/dataclasses/LICENSE b/strawberry/ext/dataclasses/LICENSE deleted file mode 100644 index 473861da1b..0000000000 --- a/strawberry/ext/dataclasses/LICENSE +++ /dev/null @@ -1,279 +0,0 @@ -A. HISTORY OF THE SOFTWARE -========================== - -Python was created in the early 1990s by Guido van Rossum at Stichting -Mathematisch Centrum (CWI, see http://www.cwi.nl) in the Netherlands -as a successor of a language called ABC. Guido remains Python's -principal author, although it includes many contributions from others. - -In 1995, Guido continued his work on Python at the Corporation for -National Research Initiatives (CNRI, see http://www.cnri.reston.va.us) -in Reston, Virginia where he released several versions of the -software. - -In May 2000, Guido and the Python core development team moved to -BeOpen.com to form the BeOpen PythonLabs team. In October of the same -year, the PythonLabs team moved to Digital Creations, which became -Zope Corporation. In 2001, the Python Software Foundation (PSF, see -https://www.python.org/psf/) was formed, a non-profit organization -created specifically to own Python-related Intellectual Property. -Zope Corporation was a sponsoring member of the PSF. - -All Python releases are Open Source (see http://www.opensource.org for -the Open Source Definition). Historically, most, but not all, Python -releases have also been GPL-compatible; the table below summarizes -the various releases. - - Release Derived Year Owner GPL- - from compatible? (1) - - 0.9.0 thru 1.2 1991-1995 CWI yes - 1.3 thru 1.5.2 1.2 1995-1999 CNRI yes - 1.6 1.5.2 2000 CNRI no - 2.0 1.6 2000 BeOpen.com no - 1.6.1 1.6 2001 CNRI yes (2) - 2.1 2.0+1.6.1 2001 PSF no - 2.0.1 2.0+1.6.1 2001 PSF yes - 2.1.1 2.1+2.0.1 2001 PSF yes - 2.1.2 2.1.1 2002 PSF yes - 2.1.3 2.1.2 2002 PSF yes - 2.2 and above 2.1.1 2001-now PSF yes - -Footnotes: - -(1) GPL-compatible doesn't mean that we're distributing Python under - the GPL. All Python licenses, unlike the GPL, let you distribute - a modified version without making your changes open source. The - GPL-compatible licenses make it possible to combine Python with - other software that is released under the GPL; the others don't. - -(2) According to Richard Stallman, 1.6.1 is not GPL-compatible, - because its license has a choice of law clause. According to - CNRI, however, Stallman's lawyer has told CNRI's lawyer that 1.6.1 - is "not incompatible" with the GPL. - -Thanks to the many outside volunteers who have worked under Guido's -direction to make these releases possible. - - -B. TERMS AND CONDITIONS FOR ACCESSING OR OTHERWISE USING PYTHON -=============================================================== - -Python software and documentation are licensed under the -Python Software Foundation License Version 2. - -Starting with Python 3.8.6, examples, recipes, and other code in -the documentation are dual licensed under the PSF License Version 2 -and the Zero-Clause BSD license. - -Some software incorporated into Python is under different licenses. -The licenses are listed with code falling under that license. - - -PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2 --------------------------------------------- - -1. This LICENSE AGREEMENT is between the Python Software Foundation -("PSF"), and the Individual or Organization ("Licensee") accessing and -otherwise using this software ("Python") in source or binary form and -its associated documentation. - -2. Subject to the terms and conditions of this License Agreement, PSF hereby -grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce, -analyze, test, perform and/or display publicly, prepare derivative works, -distribute, and otherwise use Python alone or in any derivative version, -provided, however, that PSF's License Agreement and PSF's notice of copyright, -i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, -2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021 Python Software Foundation; -All Rights Reserved" are retained in Python alone or in any derivative version -prepared by Licensee. - -3. In the event Licensee prepares a derivative work that is based on -or incorporates Python or any part thereof, and wants to make -the derivative work available to others as provided herein, then -Licensee hereby agrees to include in any such work a brief summary of -the changes made to Python. - -4. PSF is making Python available to Licensee on an "AS IS" -basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR -IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND -DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS -FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT -INFRINGE ANY THIRD PARTY RIGHTS. - -5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON -FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS -A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON, -OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. - -6. This License Agreement will automatically terminate upon a material -breach of its terms and conditions. - -7. Nothing in this License Agreement shall be deemed to create any -relationship of agency, partnership, or joint venture between PSF and -Licensee. This License Agreement does not grant permission to use PSF -trademarks or trade name in a trademark sense to endorse or promote -products or services of Licensee, or any third party. - -8. By copying, installing or otherwise using Python, Licensee -agrees to be bound by the terms and conditions of this License -Agreement. - - -BEOPEN.COM LICENSE AGREEMENT FOR PYTHON 2.0 -------------------------------------------- - -BEOPEN PYTHON OPEN SOURCE LICENSE AGREEMENT VERSION 1 - -1. This LICENSE AGREEMENT is between BeOpen.com ("BeOpen"), having an -office at 160 Saratoga Avenue, Santa Clara, CA 95051, and the -Individual or Organization ("Licensee") accessing and otherwise using -this software in source or binary form and its associated -documentation ("the Software"). - -2. Subject to the terms and conditions of this BeOpen Python License -Agreement, BeOpen hereby grants Licensee a non-exclusive, -royalty-free, world-wide license to reproduce, analyze, test, perform -and/or display publicly, prepare derivative works, distribute, and -otherwise use the Software alone or in any derivative version, -provided, however, that the BeOpen Python License is retained in the -Software, alone or in any derivative version prepared by Licensee. - -3. BeOpen is making the Software available to Licensee on an "AS IS" -basis. BEOPEN MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR -IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, BEOPEN MAKES NO AND -DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS -FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF THE SOFTWARE WILL NOT -INFRINGE ANY THIRD PARTY RIGHTS. - -4. BEOPEN SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF THE -SOFTWARE FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS -AS A RESULT OF USING, MODIFYING OR DISTRIBUTING THE SOFTWARE, OR ANY -DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. - -5. This License Agreement will automatically terminate upon a material -breach of its terms and conditions. - -6. This License Agreement shall be governed by and interpreted in all -respects by the law of the State of California, excluding conflict of -law provisions. Nothing in this License Agreement shall be deemed to -create any relationship of agency, partnership, or joint venture -between BeOpen and Licensee. This License Agreement does not grant -permission to use BeOpen trademarks or trade names in a trademark -sense to endorse or promote products or services of Licensee, or any -third party. As an exception, the "BeOpen Python" logos available at -http://www.pythonlabs.com/logos.html may be used according to the -permissions granted on that web page. - -7. By copying, installing or otherwise using the software, Licensee -agrees to be bound by the terms and conditions of this License -Agreement. - - -CNRI LICENSE AGREEMENT FOR PYTHON 1.6.1 ---------------------------------------- - -1. This LICENSE AGREEMENT is between the Corporation for National -Research Initiatives, having an office at 1895 Preston White Drive, -Reston, VA 20191 ("CNRI"), and the Individual or Organization -("Licensee") accessing and otherwise using Python 1.6.1 software in -source or binary form and its associated documentation. - -2. Subject to the terms and conditions of this License Agreement, CNRI -hereby grants Licensee a nonexclusive, royalty-free, world-wide -license to reproduce, analyze, test, perform and/or display publicly, -prepare derivative works, distribute, and otherwise use Python 1.6.1 -alone or in any derivative version, provided, however, that CNRI's -License Agreement and CNRI's notice of copyright, i.e., "Copyright (c) -1995-2001 Corporation for National Research Initiatives; All Rights -Reserved" are retained in Python 1.6.1 alone or in any derivative -version prepared by Licensee. Alternately, in lieu of CNRI's License -Agreement, Licensee may substitute the following text (omitting the -quotes): "Python 1.6.1 is made available subject to the terms and -conditions in CNRI's License Agreement. This Agreement together with -Python 1.6.1 may be located on the Internet using the following -unique, persistent identifier (known as a handle): 1895.22/1013. This -Agreement may also be obtained from a proxy server on the Internet -using the following URL: http://hdl.handle.net/1895.22/1013". - -3. In the event Licensee prepares a derivative work that is based on -or incorporates Python 1.6.1 or any part thereof, and wants to make -the derivative work available to others as provided herein, then -Licensee hereby agrees to include in any such work a brief summary of -the changes made to Python 1.6.1. - -4. CNRI is making Python 1.6.1 available to Licensee on an "AS IS" -basis. CNRI MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR -IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, CNRI MAKES NO AND -DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS -FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON 1.6.1 WILL NOT -INFRINGE ANY THIRD PARTY RIGHTS. - -5. CNRI SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON -1.6.1 FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS -A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON 1.6.1, -OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. - -6. This License Agreement will automatically terminate upon a material -breach of its terms and conditions. - -7. This License Agreement shall be governed by the federal -intellectual property law of the United States, including without -limitation the federal copyright law, and, to the extent such -U.S. federal law does not apply, by the law of the Commonwealth of -Virginia, excluding Virginia's conflict of law provisions. -Notwithstanding the foregoing, with regard to derivative works based -on Python 1.6.1 that incorporate non-separable material that was -previously distributed under the GNU General Public License (GPL), the -law of the Commonwealth of Virginia shall govern this License -Agreement only as to issues arising under or with respect to -Paragraphs 4, 5, and 7 of this License Agreement. Nothing in this -License Agreement shall be deemed to create any relationship of -agency, partnership, or joint venture between CNRI and Licensee. This -License Agreement does not grant permission to use CNRI trademarks or -trade name in a trademark sense to endorse or promote products or -services of Licensee, or any third party. - -8. By clicking on the "ACCEPT" button where indicated, or by copying, -installing or otherwise using Python 1.6.1, Licensee agrees to be -bound by the terms and conditions of this License Agreement. - - ACCEPT - - -CWI LICENSE AGREEMENT FOR PYTHON 0.9.0 THROUGH 1.2 --------------------------------------------------- - -Copyright (c) 1991 - 1995, Stichting Mathematisch Centrum Amsterdam, -The Netherlands. All rights reserved. - -Permission to use, copy, modify, and distribute this software and its -documentation for any purpose and without fee is hereby granted, -provided that the above copyright notice appear in all copies and that -both that copyright notice and this permission notice appear in -supporting documentation, and that the name of Stichting Mathematisch -Centrum or CWI not be used in advertising or publicity pertaining to -distribution of the software without specific, written prior -permission. - -STICHTING MATHEMATISCH CENTRUM DISCLAIMS ALL WARRANTIES WITH REGARD TO -THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND -FITNESS, IN NO EVENT SHALL STICHTING MATHEMATISCH CENTRUM BE LIABLE -FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES -WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN -ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT -OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - -ZERO-CLAUSE BSD LICENSE FOR CODE IN THE PYTHON DOCUMENTATION ----------------------------------------------------------------------- - -Permission to use, copy, modify, and/or distribute this software for any -purpose with or without fee is hereby granted. - -THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH -REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY -AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, -INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM -LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR -OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR -PERFORMANCE OF THIS SOFTWARE. diff --git a/strawberry/ext/dataclasses/__init__.py b/strawberry/ext/dataclasses/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/strawberry/ext/dataclasses/dataclasses.py b/strawberry/ext/dataclasses/dataclasses.py deleted file mode 100644 index 7770737995..0000000000 --- a/strawberry/ext/dataclasses/dataclasses.py +++ /dev/null @@ -1,70 +0,0 @@ -# -# This code is licensed under the Python Software Foundation License Version 2 -# - -from dataclasses import ( # type: ignore - _FIELD_INITVAR, - _HAS_DEFAULT_FACTORY, - _POST_INIT_NAME, - MISSING, - _create_fn, - _field_init, - _init_param, -) - - -def dataclass_init_fn(fields, frozen, has_post_init, self_name, globals_): - """ - We create a custom __init__ function for the dataclasses that back - Strawberry object types to only accept keyword arguments. This allows us to - avoid the problem where a type cannot define a field with a default value - before a field that doesn't have a default value. - - An example of the problem: - https://stackoverflow.com/questions/51575931/class-inheritance-in-python-3-7-dataclasses - - Code is adapted from: - https://github.com/python/cpython/blob/v3.9.6/Lib/dataclasses.py#L489-L536 - - Note: in Python 3.10 and above we use the `kw_only` argument to achieve the - same result. - """ - # fields contains both real fields and InitVar pseudo-fields. - - locals_ = {f"_type_{f.name}": f.type for f in fields} - locals_.update( - { - "MISSING": MISSING, - "_HAS_DEFAULT_FACTORY": _HAS_DEFAULT_FACTORY, - } - ) - - body_lines = [] - for f in fields: - line = _field_init(f, frozen, locals_, self_name) - # line is None means that this field doesn't require - # initialization (it's a pseudo-field). Just skip it. - if line: - body_lines.append(line) - - # Does this class have a post-init function? - if has_post_init: - params_str = ",".join(f.name for f in fields if f._field_type is _FIELD_INITVAR) - body_lines.append(f"{self_name}.{_POST_INIT_NAME}({params_str})") - - # If no body lines, use 'pass'. - if not body_lines: - body_lines = ["pass"] - - _init_params = [_init_param(f) for f in fields if f.init] - if len(_init_params) > 0: - _init_params = ["*", *_init_params] - - return _create_fn( - "__init__", - [self_name, *_init_params], - body_lines, - locals=locals_, - globals=globals_, - return_type=None, - ) diff --git a/strawberry/ext/mypy_plugin.py b/strawberry/ext/mypy_plugin.py index 5003f8be0b..0249a8b97d 100644 --- a/strawberry/ext/mypy_plugin.py +++ b/strawberry/ext/mypy_plugin.py @@ -1,10 +1,8 @@ -import re -import warnings from decimal import Decimal from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Union, cast + from typing_extensions import Final -import mypy from mypy.nodes import ( ARG_OPT, ARG_POS, @@ -17,6 +15,7 @@ CallExpr, CastExpr, ClassDef, + Context, Expression, FuncDef, IndexExpr, @@ -60,21 +59,12 @@ from mypy.typevars import fill_typevars from mypy.util import get_unique_redefinition_name + # Backwards compatible with the removal of `TypeVarDef` in mypy 0.920. try: from mypy.types import TypeVarDef # type: ignore except ImportError: - TypeVarDef = TypeVarType - -# To be compatible with user who don't use pydantic -try: - from pydantic.mypy import METADATA_KEY as PYDANTIC_METADATA_KEY - from pydantic.mypy import PydanticModelField -except ImportError: - PYDANTIC_METADATA_KEY = "" - -VERSION_RE = re.compile(r"(^0|^(?:[1-9][0-9]*))\.(0|(?:[1-9][0-9]*))") -FALLBACK_VERSION = Decimal("0.800") + TypeVarDef = TypeVarType # type: ignore class MypyVersion: @@ -114,7 +104,7 @@ def strawberry_field_hook(ctx: FunctionContext) -> Type: def _get_named_type(name: str, api: SemanticAnalyzerPluginInterface): if "." in name: - return api.named_type_or_none(name) + return api.named_type_or_none(name) # type: ignore return api.named_type(name) @@ -265,56 +255,6 @@ def enum_hook(ctx: DynamicClassDefContext) -> None: ) -def scalar_hook(ctx: DynamicClassDefContext) -> None: - first_argument = ctx.call.args[0] - - if isinstance(first_argument, NameExpr): - if not first_argument.node: - ctx.api.defer() - - return - - if isinstance(first_argument.node, Var): - var_type = first_argument.node.type or AnyType( - TypeOfAny.implementation_artifact - ) - - type_alias = TypeAlias( - var_type, - fullname=ctx.api.qualified_name(ctx.name), - line=ctx.call.line, - column=ctx.call.column, - ) - - ctx.api.add_symbol_table_node( - ctx.name, SymbolTableNode(GDEF, type_alias, plugin_generated=False) - ) - return - - scalar_type: Optional[Type] - - # TODO: add proper support for NewType - - try: - scalar_type = _get_type_for_expr(first_argument, ctx.api) - except InvalidNodeTypeException: - scalar_type = None - - if not scalar_type: - scalar_type = AnyType(TypeOfAny.from_error) - - type_alias = TypeAlias( - scalar_type, - fullname=ctx.api.qualified_name(ctx.name), - line=ctx.call.line, - column=ctx.call.column, - ) - - ctx.api.add_symbol_table_node( - ctx.name, SymbolTableNode(GDEF, type_alias, plugin_generated=False) - ) - - def add_static_method_to_class( api: Union[SemanticAnalyzerPluginInterface, CheckerPluginInterface], cls: ClassDef, @@ -398,68 +338,16 @@ def strawberry_pydantic_class_callback(ctx: ClassDefContext) -> None: ] add_method(ctx, "__init__", init_args, NoneType()) - model_type = cast( - mypy.types.Instance, _get_type_for_expr(model_expression, ctx.api) - ) + model_type = _get_type_for_expr(model_expression, ctx.api) - # these are the fields that the user added to the strawberry type - new_strawberry_fields: Set[str] = set() - - # TODO: think about inheritance for strawberry? - for stmt in ctx.cls.defs.body: - if isinstance(stmt, AssignmentStmt): - lhs = cast(NameExpr, stmt.lvalues[0]) - new_strawberry_fields.add(lhs.name) - - pydantic_fields: Set["PydanticModelField"] = set() - try: - for _name, data in model_type.type.metadata[PYDANTIC_METADATA_KEY][ - "fields" - ].items(): - field = PydanticModelField.deserialize(ctx.cls.info, data) - pydantic_fields.add(field) - except KeyError: - # this will happen if the user didn't add the pydantic plugin - # AND is using the pydantic conversion decorator - ctx.api.fail( - "Pydantic plugin not installed," - " please add pydantic.mypy your mypy.ini plugins", - ctx.reason, - ) - - potentially_missing_fields: Set["PydanticModelField"] = { - f for f in pydantic_fields if f.name not in new_strawberry_fields - } - - """ - Need to check if all_fields=True from the pydantic decorator - There is no way to real check that Literal[True] was used - We just check if the strawberry type is missing all the fields - This means that the user is using all_fields=True - """ - is_all_fields: bool = len(potentially_missing_fields) == len(pydantic_fields) - missing_pydantic_fields: Set["PydanticModelField"] = ( - potentially_missing_fields if not is_all_fields else set() + # Add to_pydantic + add_method( + ctx, + "to_pydantic", + args=[], + return_type=model_type, ) - # Add the default to_pydantic if undefined by the user - if "to_pydantic" not in ctx.cls.info.names: - add_method( - ctx, - "to_pydantic", - args=[ - f.to_argument( - # TODO: use_alias should depend on config? - info=model_type.type, - typed=True, - force_optional=False, - use_alias=True, - ) - for f in missing_pydantic_fields - ], - return_type=model_type, - ) - # Add from_pydantic model_argument = Argument( variable=Var(name="instance", type=model_type), @@ -482,7 +370,6 @@ def is_dataclasses_field_or_strawberry_field(expr: Expression) -> bool: if isinstance(expr.callee, RefExpr) and expr.callee.fullname in ( "dataclasses.field", "strawberry.field.field", - "strawberry.mutation.mutation", "strawberry.federation.field", "strawberry.federation.field.field", ): @@ -491,17 +378,12 @@ def is_dataclasses_field_or_strawberry_field(expr: Expression) -> bool: if isinstance(expr.callee, MemberExpr) and isinstance( expr.callee.expr, NameExpr ): - return ( - expr.callee.name in {"field", "mutation"} - and expr.callee.expr.name == "strawberry" - ) + return expr.callee.name == "field" and expr.callee.expr.name == "strawberry" return False -def _collect_field_args( - ctx: ClassDefContext, expr: Expression -) -> Tuple[bool, Dict[str, Expression]]: +def _collect_field_args(expr: Expression) -> Tuple[bool, Dict[str, Expression]]: """Returns a tuple where the first value represents whether or not the expression is a call to dataclass.field and the second is a dictionary of the keyword arguments that field() was called with. @@ -510,15 +392,11 @@ def _collect_field_args( if is_dataclasses_field_or_strawberry_field(expr): expr = cast(CallExpr, expr) + # field() only takes keyword arguments. args = {} for name, arg in zip(expr.arg_names, expr.args): - if name is None: - ctx.api.fail( - '"field()" or "mutation()" only takes keyword arguments', expr - ) - return False, {} - + assert name is not None args[name] = arg return True, args @@ -722,7 +600,7 @@ def collect_attributes(self) -> Optional[List[DataclassAttribute]]: is_init_var = True node.type = node_type.args[0] - has_field_call, field_args = _collect_field_args(ctx, stmt.rvalue) + has_field_call, field_args = _collect_field_args(stmt.rvalue) is_in_init_param = field_args.get("init") if is_in_init_param is None: @@ -765,7 +643,7 @@ def collect_attributes(self) -> Optional[List[DataclassAttribute]]: if MypyVersion.VERSION >= Decimal("0.800"): params["info"] = cls.info if MypyVersion.VERSION >= Decimal("0.920"): - params["kw_only"] = True + params["kw_only"] = False attribute = DataclassAttribute(**params) # type: ignore attrs.append(attribute) @@ -803,6 +681,28 @@ def collect_attributes(self) -> Optional[List[DataclassAttribute]]: break all_attrs = super_attrs + all_attrs + # Ensure that arguments without a default don't follow + # arguments that have a default. + found_default = False + for attr in all_attrs: + # If we find any attribute that is_in_init but that + # doesn't have a default after one that does have one, + # then that's an error. + if found_default and attr.is_in_init and not attr.has_default: + # If the issue comes from merging different classes, report it + # at the class definition point. + context = ( + Context(line=attr.line, column=attr.column) + if attr in attrs + else ctx.cls + ) + ctx.api.fail( + "Attributes without a default cannot follow attributes with one", + context, + ) + + found_default = found_default or (attr.has_default and attr.is_in_init) + return all_attrs def _freeze(self, attributes: List[DataclassAttribute]) -> None: @@ -860,9 +760,6 @@ def get_dynamic_class_hook( if self._is_strawberry_enum(fullname): return enum_hook - if self._is_strawberry_scalar(fullname): - return scalar_hook - if self._is_strawberry_create_type(fullname): return create_type_hook @@ -901,7 +798,6 @@ def _is_strawberry_union(self, fullname: str) -> bool: def _is_strawberry_field(self, fullname: str) -> bool: if fullname in { "strawberry.field.field", - "strawberry.mutation.mutation", "strawberry.federation.field", }: return True @@ -910,7 +806,6 @@ def _is_strawberry_field(self, fullname: str) -> bool: fullname.endswith(decorator) for decorator in { "strawberry.field", - "strawberry.mutation", "strawberry.federation.field", } ) @@ -920,11 +815,6 @@ def _is_strawberry_enum(self, fullname: str) -> bool: "strawberry.enum" ) - def _is_strawberry_scalar(self, fullname: str) -> bool: - return fullname == "strawberry.custom_scalar.scalar" or fullname.endswith( - "strawberry.scalar" - ) - def _is_strawberry_lazy_type(self, fullname: str) -> bool: return fullname == "strawberry.lazy_type.LazyType" @@ -935,10 +825,6 @@ def _is_strawberry_decorator(self, fullname: str) -> bool: "strawberry.object_type.type", "strawberry.federation.type", "strawberry.federation.object_type.type", - "strawberry.federation.input", - "strawberry.federation.object_type.input", - "strawberry.federation.interface", - "strawberry.federation.object_type.interface", "strawberry.schema_directive.schema_directive", "strawberry.object_type.input", "strawberry.object_type.interface", @@ -978,7 +864,6 @@ def _is_strawberry_pydantic_decorator(self, fullname: str) -> bool: for strawberry_decorator in { "strawberry.experimental.pydantic.object_type.type", "strawberry.experimental.pydantic.object_type.input", - "strawberry.experimental.pydantic.object_type.interface", "strawberry.experimental.pydantic.error_type", } ): @@ -999,13 +884,7 @@ def _is_strawberry_pydantic_decorator(self, fullname: str) -> bool: def plugin(version: str): - match = VERSION_RE.match(version) - if match: - MypyVersion.VERSION = Decimal(".".join(match.groups())) - else: - MypyVersion.VERSION = FALLBACK_VERSION - warnings.warn( - f"Mypy version {version} could not be parsed. Reverting to v0.800" - ) + # Save the version to be used by the plugin. + MypyVersion.VERSION = Decimal(version) return StrawberryPlugin diff --git a/strawberry/extensions/__init__.py b/strawberry/extensions/__init__.py index d24626291d..dce36757df 100644 --- a/strawberry/extensions/__init__.py +++ b/strawberry/extensions/__init__.py @@ -1,11 +1,11 @@ from .add_validation_rules import AddValidationRules from .base_extension import Extension from .disable_validation import DisableValidation -from .mask_errors import MaskErrors from .parser_cache import ParserCache from .query_depth_limiter import QueryDepthLimiter from .validation_cache import ValidationCache + __all__ = [ "Extension", "AddValidationRules", @@ -13,5 +13,4 @@ "ParserCache", "QueryDepthLimiter", "ValidationCache", - "MaskErrors", ] diff --git a/strawberry/extensions/base_extension.py b/strawberry/extensions/base_extension.py index 9a50d9e0d2..62424824b5 100644 --- a/strawberry/extensions/base_extension.py +++ b/strawberry/extensions/base_extension.py @@ -1,8 +1,6 @@ from typing import Any, Dict -from graphql import GraphQLResolveInfo - -from strawberry.types import ExecutionContext +from strawberry.types import ExecutionContext, Info from strawberry.utils.await_maybe import AwaitableOrValue @@ -37,7 +35,7 @@ def on_executing_end(self) -> AwaitableOrValue[None]: """This method is called after the executing step""" def resolve( - self, _next, root, info: GraphQLResolveInfo, *args, **kwargs + self, _next, root, info: Info, *args, **kwargs ) -> AwaitableOrValue[object]: return _next(root, info, *args, **kwargs) diff --git a/strawberry/extensions/directives.py b/strawberry/extensions/directives.py index e7090e74dc..783fa1f386 100644 --- a/strawberry/extensions/directives.py +++ b/strawberry/extensions/directives.py @@ -1,13 +1,10 @@ -from typing import TYPE_CHECKING, Any, Dict, Tuple +from typing import TYPE_CHECKING, Any -from graphql import DirectiveNode, GraphQLResolveInfo - -from strawberry.directive import StrawberryDirective from strawberry.extensions import Extension -from strawberry.field import StrawberryField from strawberry.types import Info from strawberry.utils.await_maybe import AwaitableOrValue, await_maybe + if TYPE_CHECKING: from strawberry.schema.schema import Schema @@ -17,61 +14,58 @@ class DirectivesExtension(Extension): async def resolve( - self, _next, root, info: GraphQLResolveInfo, *args, **kwargs + self, _next, root, info: Info, *args, **kwargs ) -> AwaitableOrValue[Any]: - value = await await_maybe(_next(root, info, *args, **kwargs)) + result = await await_maybe(_next(root, info, *args, **kwargs)) for directive in info.field_nodes[0].directives: - if directive.name.value in SPECIFIED_DIRECTIVES: + directive_name = directive.name.value + + if directive_name in SPECIFIED_DIRECTIVES: continue - strawberry_directive, arguments = process_directive(directive, value, info) - value = await await_maybe(strawberry_directive.resolver(**arguments)) - return value + # TODO: support converting lists + arguments = { + argument.name.value: argument.value.value # type: ignore + for argument in directive.arguments + } + + schema: Schema = info.schema._strawberry_schema # type: ignore + strawberry_directive = schema.get_directive_by_name(directive_name) + assert ( + strawberry_directive is not None + ), f"Directive {directive_name} not found" + + result = await await_maybe( + strawberry_directive.resolver(result, **arguments) + ) + + return result class DirectivesExtensionSync(Extension): - def resolve( - self, _next, root, info: GraphQLResolveInfo, *args, **kwargs - ) -> AwaitableOrValue[Any]: - value = _next(root, info, *args, **kwargs) + # TODO: we might need the graphql info here + def resolve(self, _next, root, info, *args, **kwargs) -> AwaitableOrValue[Any]: + result = _next(root, info, *args, **kwargs) for directive in info.field_nodes[0].directives: - if directive.name.value in SPECIFIED_DIRECTIVES: + directive_name = directive.name.value + + if directive_name in SPECIFIED_DIRECTIVES: continue - strawberry_directive, arguments = process_directive(directive, value, info) - value = strawberry_directive.resolver(**arguments) - - return value - - -def process_directive( - directive: DirectiveNode, - value: Any, - info: GraphQLResolveInfo, -) -> Tuple[StrawberryDirective, Dict[str, Any]]: - """Get a `StrawberryDirective` from ``directive` and prepare its arguments.""" - directive_name = directive.name.value - schema: Schema = info.schema._strawberry_schema # type: ignore - - strawberry_directive = schema.get_directive_by_name(directive_name) - assert strawberry_directive is not None, f"Directive {directive_name} not found" - - # TODO: support converting lists - arguments = { - argument.name.value: argument.value.value # type: ignore - for argument in directive.arguments - } - resolver = strawberry_directive.resolver - - info_parameter = resolver.info_parameter - value_parameter = resolver.value_parameter - if info_parameter: - field: StrawberryField = schema.get_field_for_type( # type: ignore - field_name=info.field_name, - type_name=info.parent_type.name, - ) - arguments[info_parameter.name] = Info(_raw_info=info, _field=field) - if value_parameter: - arguments[value_parameter.name] = value - return strawberry_directive, arguments + + # TODO: support converting lists + arguments = { + argument.name.value: argument.value.value # type: ignore + for argument in directive.arguments + } + + schema: Schema = info.schema._strawberry_schema # type: ignore + strawberry_directive = schema.get_directive_by_name(directive_name) + assert ( + strawberry_directive is not None + ), f"Directive {directive_name} not found" + + result = strawberry_directive.resolver(result, **arguments) + + return result diff --git a/strawberry/extensions/mask_errors.py b/strawberry/extensions/mask_errors.py deleted file mode 100644 index 2786b06ac5..0000000000 --- a/strawberry/extensions/mask_errors.py +++ /dev/null @@ -1,45 +0,0 @@ -from typing import Callable - -from graphql.error import GraphQLError - -from strawberry.extensions import Extension - - -def default_should_mask_error(_) -> bool: - # Mask all errors - return True - - -class MaskErrors(Extension): - should_mask_error: Callable[[GraphQLError], bool] - error_message: str - - def __init__( - self, - should_mask_error: Callable[[GraphQLError], bool] = default_should_mask_error, - error_message: str = "Unexpected error.", - ): - self.should_mask_error = should_mask_error - self.error_message = error_message - - def anonymise_error(self, error: GraphQLError) -> GraphQLError: - return GraphQLError( - message=self.error_message, - nodes=error.nodes, - source=error.source, - positions=error.positions, - path=error.path, - original_error=None, - ) - - def on_request_end(self): - result = self.execution_context.result - if result and result.errors: - processed_errors = [] - for error in result.errors: - if self.should_mask_error(error): - processed_errors.append(self.anonymise_error(error)) - else: - processed_errors.append(error) - - result.errors = processed_errors diff --git a/strawberry/extensions/query_depth_limiter.py b/strawberry/extensions/query_depth_limiter.py index f2997487b0..0c54e02665 100644 --- a/strawberry/extensions/query_depth_limiter.py +++ b/strawberry/extensions/query_depth_limiter.py @@ -26,7 +26,7 @@ # SOFTWARE. import re -from typing import Callable, Dict, Iterable, List, Optional, Type, Union +from typing import Callable, Dict, List, Optional, Type, Union from graphql import GraphQLError from graphql.language import ( @@ -43,6 +43,7 @@ from strawberry.extensions import AddValidationRules from strawberry.extensions.utils import is_introspection_key + IgnoreType = Union[Callable[[str], bool], re.Pattern, str] @@ -118,7 +119,7 @@ def __init__(self, validation_context: ValidationContext): def get_fragments( - definitions: Iterable[DefinitionNode], + definitions: List[DefinitionNode], ) -> Dict[str, FragmentDefinitionNode]: fragments = {} for definition in definitions: @@ -131,7 +132,7 @@ def get_fragments( # This will actually get both queries and mutations. # We can basically treat those the same def get_queries_and_mutations( - definitions: Iterable[DefinitionNode], + definitions: List[DefinitionNode], ) -> Dict[str, OperationDefinitionNode]: operations = {} @@ -162,8 +163,7 @@ def determine_depth( return depth_so_far if isinstance(node, FieldNode): - # by default, ignore the introspection fields which begin - # with double underscores + # by default, ignore the introspection fields which begin with double underscores should_ignore = is_introspection_key(node.name.value) or is_ignored( node, ignore ) @@ -213,7 +213,7 @@ def determine_depth( ) ) else: - raise TypeError(f"Depth crawler cannot handle: {node.kind}") # pragma: no cover + raise Exception(f"Depth crawler cannot handle: {node.kind}") # pragma: no cover def is_ignored(node: FieldNode, ignore: Optional[List[IgnoreType]] = None) -> bool: @@ -232,6 +232,6 @@ def is_ignored(node: FieldNode, ignore: Optional[List[IgnoreType]] = None) -> bo if rule(field_name): return True else: - raise TypeError(f"Invalid ignore option: {rule}") + raise ValueError(f"Invalid ignore option: {rule}") return False diff --git a/strawberry/extensions/runner.py b/strawberry/extensions/runner.py index 01966c79cf..432560d317 100644 --- a/strawberry/extensions/runner.py +++ b/strawberry/extensions/runner.py @@ -70,7 +70,7 @@ async def get_extensions_results(self) -> Dict[str, Any]: for extension in self.extensions: results = await await_maybe(extension.get_results()) - data.update(results) + data.update(results) # type: ignore return data diff --git a/strawberry/extensions/tracing/__init__.py b/strawberry/extensions/tracing/__init__.py index e352472371..0d18e23c4c 100644 --- a/strawberry/extensions/tracing/__init__.py +++ b/strawberry/extensions/tracing/__init__.py @@ -1,32 +1,2 @@ -import importlib -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from .apollo import ApolloTracingExtension, ApolloTracingExtensionSync - from .datadog import DatadogTracingExtension, DatadogTracingExtensionSync - from .opentelemetry import ( - OpenTelemetryExtension, - OpenTelemetryExtensionSync, - ) - -__all__ = [ - "ApolloTracingExtension", - "ApolloTracingExtensionSync", - "DatadogTracingExtension", - "DatadogTracingExtensionSync", - "OpenTelemetryExtension", - "OpenTelemetryExtensionSync", -] - - -def __getattr__(name: str): - if name in {"DatadogTracingExtension", "DatadogTracingExtensionSync"}: - return getattr(importlib.import_module(".datadog", __name__), name) - - if name in {"ApolloTracingExtension", "ApolloTracingExtensionSync"}: - return getattr(importlib.import_module(".apollo", __name__), name) - - if name in {"OpenTelemetryExtension", "OpenTelemetryExtensionSync"}: - return getattr(importlib.import_module(".opentelemetry", __name__), name) - - raise AttributeError(f"module {__name__!r} has no attribute {name!r}") +from .apollo import ApolloTracingExtension, ApolloTracingExtensionSync # noqa +from .opentelemetry import OpenTelemetryExtension, OpenTelemetryExtensionSync # noqa diff --git a/strawberry/extensions/tracing/apollo.py b/strawberry/extensions/tracing/apollo.py index cecba2db59..d9091f6287 100644 --- a/strawberry/extensions/tracing/apollo.py +++ b/strawberry/extensions/tracing/apollo.py @@ -10,6 +10,7 @@ from .utils import should_skip_tracing + DATETIME_FORMAT = "%Y-%m-%dT%H:%M:%S.%fZ" diff --git a/strawberry/extensions/tracing/datadog.py b/strawberry/extensions/tracing/datadog.py deleted file mode 100644 index d87c6b9948..0000000000 --- a/strawberry/extensions/tracing/datadog.py +++ /dev/null @@ -1,114 +0,0 @@ -import hashlib -from inspect import isawaitable -from typing import Optional - -from ddtrace import tracer - -from strawberry.extensions import Extension -from strawberry.extensions.tracing.utils import should_skip_tracing -from strawberry.types.execution import ExecutionContext -from strawberry.utils.cached_property import cached_property - - -class DatadogTracingExtension(Extension): - def __init__( - self, - *, - execution_context: Optional[ExecutionContext] = None, - ): - if execution_context: - self.execution_context = execution_context - - @cached_property - def _resource_name(self): - assert self.execution_context.query - - query_hash = self.hash_query(self.execution_context.query) - - if self.execution_context.operation_name: - return f"{self.execution_context.operation_name}:{query_hash}" - - return query_hash - - def hash_query(self, query: str): - return hashlib.md5(query.encode("utf-8")).hexdigest() - - def on_request_start(self) -> None: - self._operation_name = self.execution_context.operation_name - span_name = ( - f"{self._operation_name}" if self._operation_name else "Anonymous Query" - ) - - self.request_span = tracer.trace( - span_name, - resource=self._resource_name, - span_type="graphql", - service="strawberry", - ) - self.request_span.set_tag("graphql.operation_name", self._operation_name) - - operation_type = "query" - - assert self.execution_context.query - - if self.execution_context.query.strip().startswith("mutation"): - operation_type = "mutation" - if self.execution_context.query.strip().startswith("subscription"): - operation_type = "subscription" - - self.request_span.set_tag("graphql.operation_type", operation_type) - - def on_request_end(self) -> None: - self.request_span.finish() - - def on_validation_start(self): - self.validation_span = tracer.trace("Validation", span_type="graphql") - - def on_validation_end(self): - self.validation_span.finish() - - def on_parsing_start(self): - self.parsing_span = tracer.trace("Parsing", span_type="graphql") - - def on_parsing_end(self): - self.parsing_span.finish() - - async def resolve(self, _next, root, info, *args, **kwargs): - if should_skip_tracing(_next, info): - result = _next(root, info, *args, **kwargs) - - if isawaitable(result): # pragma: no cover - result = await result - - return result - - field_path = f"{info.parent_type}.{info.field_name}" - - with tracer.trace(f"Resolving: {field_path}", span_type="graphql") as span: - span.set_tag("graphql.field_name", info.field_name) - span.set_tag("graphql.parent_type", info.parent_type.name) - span.set_tag("graphql.field_path", field_path) - span.set_tag("graphql.path", ".".join(map(str, info.path.as_list()))) - - result = _next(root, info, *args, **kwargs) - - if isawaitable(result): - result = await result - - return result - - -class DatadogTracingExtensionSync(DatadogTracingExtension): - def resolve(self, _next, root, info, *args, **kwargs): - if should_skip_tracing(_next, info): - return _next(root, info, *args, **kwargs) - - field_path = f"{info.parent_type}.{info.field_name}" - - with tracer.trace(f"Resolving: {field_path}", span_type="graphql") as span: - span.set_tag("graphql.field_name", info.field_name) - span.set_tag("graphql.parent_type", info.parent_type.name) - span.set_tag("graphql.field_path", field_path) - span.set_tag("graphql.path", ".".join(map(str, info.path.as_list()))) - - return _next(root, info, *args, **kwargs) diff --git a/strawberry/extensions/tracing/opentelemetry.py b/strawberry/extensions/tracing/opentelemetry.py index a5df44efa3..136fbe760c 100644 --- a/strawberry/extensions/tracing/opentelemetry.py +++ b/strawberry/extensions/tracing/opentelemetry.py @@ -3,16 +3,18 @@ from inspect import isawaitable from typing import Any, Callable, Dict, Optional -from graphql import GraphQLResolveInfo from opentelemetry import trace from opentelemetry.trace import Span, SpanKind, Tracer +from graphql import GraphQLResolveInfo + from strawberry.extensions import Extension from strawberry.extensions.utils import get_path_from_info from strawberry.types.execution import ExecutionContext from .utils import should_skip_tracing + DATETIME_FORMAT = "%Y-%m-%dT%H:%M:%S.%fZ" ArgFilter = Callable[[Dict[str, Any], GraphQLResolveInfo], Dict[str, Any]] @@ -26,7 +28,7 @@ class RequestStage(enum.Enum): class OpenTelemetryExtension(Extension): _arg_filter: Optional[ArgFilter] - _span_holder: Dict[RequestStage, Span] = dict() + _span_holder: Dict[str, Span] = dict() _tracer: Tracer def __init__( @@ -52,11 +54,9 @@ def on_request_start(self): span_name, kind=SpanKind.SERVER ) self._span_holder[RequestStage.REQUEST].set_attribute("component", "graphql") - - if self.execution_context.query: - self._span_holder[RequestStage.REQUEST].set_attribute( - "query", self.execution_context.query - ) + self._span_holder[RequestStage.REQUEST].set_attribute( + "query", self.execution_context.query + ) def on_request_end(self): # If the client doesn't provide an operation name then GraphQL will diff --git a/strawberry/extensions/utils.py b/strawberry/extensions/utils.py index 85b95de001..82618abaac 100644 --- a/strawberry/extensions/utils.py +++ b/strawberry/extensions/utils.py @@ -7,7 +7,7 @@ def is_introspection_key(key: Union[str, int]) -> bool: # from: https://spec.graphql.org/June2018/#sec-Schema # > All types and directives defined within a schema must not have a name which # > begins with "__" (two underscores), as this is used exclusively - # > by GraphQL`s introspection system. + # > by GraphQLโ€™s introspection system. return str(key).startswith("__") diff --git a/strawberry/fastapi/__init__.py b/strawberry/fastapi/__init__.py index 8c9ab27782..adee7160a1 100644 --- a/strawberry/fastapi/__init__.py +++ b/strawberry/fastapi/__init__.py @@ -1,4 +1,4 @@ -from strawberry.fastapi.context import BaseContext -from strawberry.fastapi.router import GraphQLRouter +from strawberry.fastapi.router import BaseContext, GraphQLRouter + __all__ = ["BaseContext", "GraphQLRouter"] diff --git a/strawberry/fastapi/context.py b/strawberry/fastapi/context.py deleted file mode 100644 index 3c940edcf6..0000000000 --- a/strawberry/fastapi/context.py +++ /dev/null @@ -1,20 +0,0 @@ -from typing import Any, Dict, Optional, Union - -from starlette.background import BackgroundTasks -from starlette.requests import Request -from starlette.responses import Response -from starlette.websockets import WebSocket - -CustomContext = Union["BaseContext", Dict[str, Any]] -MergedContext = Union[ - "BaseContext", Dict[str, Union[Any, BackgroundTasks, Request, Response, WebSocket]] -] - - -class BaseContext: - connection_params: Optional[Any] = None - - def __init__(self): - self.request: Optional[Union[Request, WebSocket]] = None - self.background_tasks: Optional[BackgroundTasks] = None - self.response: Optional[Response] = None diff --git a/strawberry/fastapi/handlers/__init__.py b/strawberry/fastapi/handlers/__init__.py index 20f336f5ff..c3178b9da3 100644 --- a/strawberry/fastapi/handlers/__init__.py +++ b/strawberry/fastapi/handlers/__init__.py @@ -3,4 +3,5 @@ ) from strawberry.fastapi.handlers.graphql_ws_handler import GraphQLWSHandler + __all__ = ["GraphQLTransportWSHandler", "GraphQLWSHandler"] diff --git a/strawberry/fastapi/handlers/graphql_transport_ws_handler.py b/strawberry/fastapi/handlers/graphql_transport_ws_handler.py index 62ae43fd6e..7802c9d2d4 100644 --- a/strawberry/fastapi/handlers/graphql_transport_ws_handler.py +++ b/strawberry/fastapi/handlers/graphql_transport_ws_handler.py @@ -3,15 +3,11 @@ from strawberry.asgi.handlers import ( GraphQLTransportWSHandler as BaseGraphQLTransportWSHandler, ) -from strawberry.fastapi.context import BaseContext class GraphQLTransportWSHandler(BaseGraphQLTransportWSHandler): async def get_context(self) -> Any: - context = await self._get_context() - if isinstance(context, BaseContext): - context.connection_params = self.connection_params - return context + return await self._get_context() async def get_root_value(self) -> Any: return await self._get_root_value() diff --git a/strawberry/fastapi/handlers/graphql_ws_handler.py b/strawberry/fastapi/handlers/graphql_ws_handler.py index 33bdb4d444..6fb866422e 100644 --- a/strawberry/fastapi/handlers/graphql_ws_handler.py +++ b/strawberry/fastapi/handlers/graphql_ws_handler.py @@ -1,15 +1,11 @@ from typing import Any from strawberry.asgi.handlers import GraphQLWSHandler as BaseGraphQLWSHandler -from strawberry.fastapi.context import BaseContext class GraphQLWSHandler(BaseGraphQLWSHandler): async def get_context(self) -> Any: - context = await self._get_context() - if isinstance(context, BaseContext): - context.connection_params = self.connection_params - return context + return await self._get_context() async def get_root_value(self) -> Any: return await self._get_root_value() diff --git a/strawberry/fastapi/router.py b/strawberry/fastapi/router.py index debd877c81..1267648c02 100644 --- a/strawberry/fastapi/router.py +++ b/strawberry/fastapi/router.py @@ -1,43 +1,38 @@ import json from datetime import timedelta from inspect import signature -from typing import ( - Any, - Awaitable, - Callable, - Dict, - Iterable, - Optional, - Sequence, - Union, - cast, -) +from typing import Any, Callable, Dict, Optional, Sequence, Union from starlette import status from starlette.background import BackgroundTasks -from starlette.requests import HTTPConnection, Request -from starlette.responses import HTMLResponse, PlainTextResponse, Response +from starlette.requests import Request +from starlette.responses import HTMLResponse, JSONResponse, PlainTextResponse, Response from starlette.types import ASGIApp from starlette.websockets import WebSocket from fastapi import APIRouter, Depends +from strawberry.asgi.utils import get_graphiql_html from strawberry.exceptions import InvalidCustomContext, MissingQueryError -from strawberry.fastapi.context import BaseContext, CustomContext, MergedContext from strawberry.fastapi.handlers import GraphQLTransportWSHandler, GraphQLWSHandler from strawberry.file_uploads.utils import replace_placeholders_with_files -from strawberry.http import ( - GraphQLHTTPResponse, - parse_query_params, - parse_request_data, - process_result, -) +from strawberry.http import GraphQLHTTPResponse, parse_request_data, process_result from strawberry.schema import BaseSchema -from strawberry.schema.exceptions import InvalidOperationTypeError from strawberry.subscriptions import GRAPHQL_TRANSPORT_WS_PROTOCOL, GRAPHQL_WS_PROTOCOL from strawberry.types import ExecutionResult -from strawberry.types.graphql import OperationType from strawberry.utils.debug import pretty_print_graphql_operation -from strawberry.utils.graphiql import get_graphiql_html + + +CustomContext = Union["BaseContext", Dict[str, Any]] +MergedContext = Union[ + "BaseContext", Dict[str, Union[Any, BackgroundTasks, Request, Response, WebSocket]] +] + + +class BaseContext: + def __init__(self): + self.request: Optional[Union[Request, WebSocket]] = None + self.background_tasks: Optional[BackgroundTasks] = None + self.response: Optional[Response] = None class GraphQLRouter(APIRouter): @@ -50,33 +45,31 @@ async def __get_root_value(): @staticmethod def __get_context_getter( - custom_getter: Callable[ - ..., Union[Optional[CustomContext], Awaitable[Optional[CustomContext]]] - ] - ) -> Callable[..., Awaitable[CustomContext]]: - async def dependency( - custom_context: Optional[CustomContext], + custom_getter: Callable[..., Optional[CustomContext]] + ) -> Callable[..., CustomContext]: + def dependency( + custom_getter: Optional[CustomContext], background_tasks: BackgroundTasks, - connection: HTTPConnection, - response: Response = None, # type: ignore + request: Request = None, + response: Response = None, + ws: WebSocket = None, ) -> MergedContext: - request = cast(Union[Request, WebSocket], connection) - if isinstance(custom_context, BaseContext): - custom_context.request = request - custom_context.background_tasks = background_tasks - custom_context.response = response - return custom_context default_context = { - "request": request, + "request": request or ws, "background_tasks": background_tasks, "response": response, } - if isinstance(custom_context, dict): + if isinstance(custom_getter, BaseContext): + custom_getter.request = request or ws + custom_getter.background_tasks = background_tasks + custom_getter.response = response + return custom_getter + elif isinstance(custom_getter, dict): return { **default_context, - **custom_context, + **custom_getter, } - elif custom_context is None: + elif custom_getter is None: return default_context else: raise InvalidCustomContext() @@ -88,9 +81,7 @@ async def dependency( sig = sig.replace( parameters=[ *list(sig.parameters.values())[1:], - sig.parameters["custom_context"].replace( - default=Depends(custom_getter) - ), + sig.parameters["custom_getter"].replace(default=Depends(custom_getter)), ], ) # there is an ongoing issue with types and .__signature__ applied to Callables: @@ -104,7 +95,6 @@ def __init__( schema: BaseSchema, path: str = "", graphiql: bool = True, - allow_queries_via_get: bool = True, keep_alive: bool = False, keep_alive_interval: float = 1, debug: bool = False, @@ -123,7 +113,6 @@ def __init__( ) self.schema = schema self.graphiql = graphiql - self.allow_queries_via_get = allow_queries_via_get self.keep_alive = keep_alive self.keep_alive_interval = keep_alive_interval self.debug = debug @@ -145,35 +134,13 @@ def __init__( }, }, ) - async def handle_http_get( - request: Request, - response: Response, - context=Depends(self.context_getter), - root_value=Depends(self.root_value_getter), - ) -> Response: - if request.query_params: - try: - query_data = parse_query_params(request.query_params._dict) - - except json.JSONDecodeError: - return PlainTextResponse( - "Unable to parse request body as JSON", - status_code=status.HTTP_400_BAD_REQUEST, - ) - - return await self.execute_request( - request=request, - response=response, - data=query_data, - context=context, - root_value=root_value, - ) - elif self.should_render_graphiql(request): - return self.get_graphiql_response() - return Response(status_code=status.HTTP_404_NOT_FOUND) + async def get_graphiql() -> Response: + if not self.graphiql: + return Response(status_code=status.HTTP_404_NOT_FOUND) + return self.get_graphiql_response() @self.post(path) - async def handle_http_post( + async def handle_http_query( request: Request, response: Response, context=Depends(self.context_getter), @@ -195,29 +162,11 @@ async def handle_http_post( return self._merge_responses(response, actual_response) elif content_type.startswith("multipart/form-data"): multipart_data = await request.form() - try: - operations_text = multipart_data.get("operations", "{}") - operations = json.loads(operations_text) # type: ignore - files_map = json.loads(multipart_data.get("map", "{}")) # type: ignore # noqa: E501 - except json.JSONDecodeError: - actual_response = PlainTextResponse( - "Unable to parse request body as JSON", - status_code=status.HTTP_400_BAD_REQUEST, - ) - - return self._merge_responses(response, actual_response) - - try: - data = replace_placeholders_with_files( - operations, files_map, multipart_data - ) - except KeyError: - actual_response = PlainTextResponse( - "File(s) missing in form data", - status_code=status.HTTP_400_BAD_REQUEST, - ) - - return self._merge_responses(response, actual_response) + operations = json.loads(multipart_data.get("operations", {})) + files_map = json.loads(multipart_data.get("map", {})) + data = replace_placeholders_with_files( + operations, files_map, multipart_data + ) else: actual_response = PlainTextResponse( "Unsupported Media Type", @@ -226,14 +175,32 @@ async def handle_http_post( return self._merge_responses(response, actual_response) - return await self.execute_request( - request=request, - response=response, - data=data, + try: + request_data = parse_request_data(data) + except MissingQueryError: + actual_response = PlainTextResponse( + "No GraphQL query found in the request", + status_code=status.HTTP_400_BAD_REQUEST, + ) + return self._merge_responses(response, actual_response) + + result = await self.execute( + request_data.query, + variables=request_data.variables, context=context, + operation_name=request_data.operation_name, root_value=root_value, ) + response_data = await self.process_result(request, result) + + actual_response = JSONResponse( + response_data, + status_code=status.HTTP_200_OK, + ) + + return self._merge_responses(response, actual_response) + @self.websocket(path) async def websocket_endpoint( websocket: WebSocket, @@ -275,18 +242,10 @@ def pick_preferred_protocol(self, ws: WebSocket) -> Optional[str]: intersection = set(protocols) & set(self.protocols) return min( intersection, - key=lambda i: protocols.index(i), + key=lambda i: protocols.index(i), # type: ignore default=None, ) - def should_render_graphiql(self, request: Request) -> bool: - if not self.graphiql: - return False - return any( - supported_header in request.headers.get("accept", "") - for supported_header in ("text/html", "*/*") - ) - def get_graphiql_response(self) -> HTMLResponse: html = get_graphiql_html() return HTMLResponse(html) @@ -300,15 +259,9 @@ def _merge_responses(response: Response, actual_response: Response) -> Response: return actual_response async def execute( - self, - query: Optional[str], - variables: Optional[Dict[str, Any]] = None, - context: Any = None, - operation_name: Optional[str] = None, - root_value: Any = None, - allowed_operation_types: Optional[Iterable[OperationType]] = None, + self, query, variables=None, context=None, operation_name=None, root_value=None ): - if self.debug and query: + if self.debug: pretty_print_graphql_operation(operation_name, query, variables) return await self.schema.execute( @@ -317,55 +270,9 @@ async def execute( variable_values=variables, operation_name=operation_name, context_value=context, - allowed_operation_types=allowed_operation_types, ) async def process_result( self, request: Request, result: ExecutionResult ) -> GraphQLHTTPResponse: return process_result(result) - - async def execute_request( - self, request: Request, response: Response, data: dict, context, root_value - ) -> Response: - request_data = parse_request_data(data) - - method = request.method - allowed_operation_types = OperationType.from_http(method) - - if not self.allow_queries_via_get and method == "GET": - allowed_operation_types = allowed_operation_types - {OperationType.QUERY} - - try: - result = await self.execute( - request_data.query, - variables=request_data.variables, - context=context, - operation_name=request_data.operation_name, - root_value=root_value, - allowed_operation_types=allowed_operation_types, - ) - except InvalidOperationTypeError as e: - return PlainTextResponse( - e.as_http_error_reason(method), - status_code=status.HTTP_400_BAD_REQUEST, - ) - except MissingQueryError: - missing_query_response = PlainTextResponse( - "No GraphQL query found in the request", - status_code=status.HTTP_400_BAD_REQUEST, - ) - return self._merge_responses(response, missing_query_response) - - response_data = await self.process_result(request, result) - - actual_response = Response( - self.encode_json(response_data), - media_type="application/json", - status_code=status.HTTP_200_OK, - ) - - return self._merge_responses(response, actual_response) - - def encode_json(self, response_data: GraphQLHTTPResponse) -> str: - return json.dumps(response_data) diff --git a/strawberry/federation/__init__.py b/strawberry/federation/__init__.py index fe15311995..5791a02ed1 100644 --- a/strawberry/federation/__init__.py +++ b/strawberry/federation/__init__.py @@ -1,22 +1,10 @@ -from .argument import argument -from .enum import enum, enum_value from .field import field -from .mutation import mutation -from .object_type import input, interface, type -from .scalar import scalar +from .object_type import type from .schema import Schema -from .union import union + __all__ = [ - "argument", - "enum", - "enum_value", "field", - "mutation", - "input", - "interface", "type", - "scalar", "Schema", - "union", ] diff --git a/strawberry/federation/argument.py b/strawberry/federation/argument.py deleted file mode 100644 index ff2f4fc3b4..0000000000 --- a/strawberry/federation/argument.py +++ /dev/null @@ -1,29 +0,0 @@ -from typing import Iterable, Optional - -from strawberry.arguments import StrawberryArgumentAnnotation - - -def argument( - description: Optional[str] = None, - name: Optional[str] = None, - deprecation_reason: Optional[str] = None, - directives: Iterable[object] = (), - inaccessible: bool = False, - tags: Optional[Iterable[str]] = (), -) -> StrawberryArgumentAnnotation: - from strawberry.federation.schema_directives import Inaccessible, Tag - - directives = list(directives) - - if inaccessible: - directives.append(Inaccessible()) - - if tags: - directives.extend(Tag(name=tag) for tag in tags) - - return StrawberryArgumentAnnotation( - description=description, - name=name, - deprecation_reason=deprecation_reason, - directives=directives, - ) diff --git a/strawberry/federation/enum.py b/strawberry/federation/enum.py deleted file mode 100644 index 0d7f51911f..0000000000 --- a/strawberry/federation/enum.py +++ /dev/null @@ -1,84 +0,0 @@ -from typing import Any, Callable, Iterable, Optional, Union, overload - -from strawberry.enum import EnumType, EnumValueDefinition, _process_enum -from strawberry.enum import enum_value as base_enum_value - - -def enum_value( - value: Any, - deprecation_reason: Optional[str] = None, - directives: Iterable[object] = (), - inaccessible: bool = False, - tags: Iterable[str] = (), -) -> EnumValueDefinition: - from strawberry.federation.schema_directives import Inaccessible, Tag - - directives = list(directives) - - if inaccessible: - directives.append(Inaccessible()) - - if tags: - directives.extend(Tag(name=tag) for tag in tags) - - return base_enum_value(value, deprecation_reason, directives) - - -@overload -def enum( - _cls: EnumType, - *, - name=None, - description=None, - directives: Iterable[object] = (), - inaccessible: bool = False, - tags: Optional[Iterable[str]] = (), -) -> EnumType: - ... - - -@overload -def enum( - _cls: None = None, - *, - name=None, - description=None, - directives: Iterable[object] = (), - inaccessible: bool = False, - tags: Optional[Iterable[str]] = (), -) -> Callable[[EnumType], EnumType]: - ... - - -def enum( - _cls: Optional[EnumType] = None, - *, - name=None, - description=None, - directives=(), - inaccessible=False, - tags=(), -) -> Union[EnumType, Callable[[EnumType], EnumType]]: - """Registers the enum in the GraphQL type system. - - If name is passed, the name of the GraphQL type will be - the value passed of name instead of the Enum class name. - """ - - from strawberry.federation.schema_directives import Inaccessible, Tag - - directives = list(directives) - - if inaccessible: - directives.append(Inaccessible()) - - if tags: - directives.extend(Tag(name=tag) for tag in tags) - - def wrap(cls: EnumType) -> EnumType: - return _process_enum(cls, name, description, directives=directives) - - if not _cls: - return wrap - - return wrap(_cls) # pragma: no cover diff --git a/strawberry/federation/field.py b/strawberry/federation/field.py index 0fd30c00b8..d1001ddd8a 100644 --- a/strawberry/federation/field.py +++ b/strawberry/federation/field.py @@ -1,8 +1,6 @@ -import dataclasses from typing import ( Any, Callable, - Iterable, List, Optional, Sequence, @@ -11,12 +9,16 @@ Union, overload, ) + from typing_extensions import Literal -from strawberry.field import _RESOLVER_TYPE, StrawberryField -from strawberry.field import field as base_field +from strawberry.arguments import UNSET +from strawberry.field import _RESOLVER_TYPE, StrawberryField, field as base_field from strawberry.permission import BasePermission -from strawberry.unset import UNSET +from strawberry.schema_directive import StrawberrySchemaDirective + +from .schema_directives import External, Provides, Requires + T = TypeVar("T") @@ -24,24 +26,19 @@ @overload def field( *, - resolver: _RESOLVER_TYPE[T], + resolver: Callable[[], T], name: Optional[str] = None, is_subscription: bool = False, description: Optional[str] = None, provides: Optional[List[str]] = None, requires: Optional[List[str]] = None, external: bool = False, - shareable: bool = False, - tags: Optional[Iterable[str]] = (), - override: Optional[str] = None, - inaccessible: bool = False, init: Literal[False] = False, permission_classes: Optional[List[Type[BasePermission]]] = None, deprecation_reason: Optional[str] = None, default: Any = UNSET, - default_factory: Union[Callable[..., object], object] = UNSET, - directives: Sequence[object] = (), - graphql_type: Optional[Any] = None, + default_factory: Union[Callable, object] = UNSET, + directives: Sequence[StrawberrySchemaDirective] = (), ) -> T: ... @@ -55,24 +52,19 @@ def field( provides: Optional[List[str]] = None, requires: Optional[List[str]] = None, external: bool = False, - shareable: bool = False, - tags: Optional[Iterable[str]] = (), - override: Optional[str] = None, - inaccessible: bool = False, init: Literal[True] = True, permission_classes: Optional[List[Type[BasePermission]]] = None, deprecation_reason: Optional[str] = None, default: Any = UNSET, - default_factory: Union[Callable[..., object], object] = UNSET, - directives: Sequence[object] = (), - graphql_type: Optional[Any] = None, + default_factory: Union[Callable, object] = UNSET, + directives: Sequence[StrawberrySchemaDirective] = (), ) -> Any: ... @overload def field( - resolver: _RESOLVER_TYPE[T], + resolver: _RESOLVER_TYPE, *, name: Optional[str] = None, is_subscription: bool = False, @@ -80,79 +72,47 @@ def field( provides: Optional[List[str]] = None, requires: Optional[List[str]] = None, external: bool = False, - shareable: bool = False, - tags: Optional[Iterable[str]] = (), - override: Optional[str] = None, - inaccessible: bool = False, permission_classes: Optional[List[Type[BasePermission]]] = None, deprecation_reason: Optional[str] = None, default: Any = UNSET, - default_factory: Union[Callable[..., object], object] = UNSET, - directives: Sequence[object] = (), - graphql_type: Optional[Any] = None, + default_factory: Union[Callable, object] = UNSET, + directives: Sequence[StrawberrySchemaDirective] = (), ) -> StrawberryField: ... def field( - resolver: Optional[_RESOLVER_TYPE[Any]] = None, + resolver=None, *, - name: Optional[str] = None, - is_subscription: bool = False, - description: Optional[str] = None, - provides: Optional[List[str]] = None, - requires: Optional[List[str]] = None, - external: bool = False, - shareable: bool = False, - tags: Optional[Iterable[str]] = (), - override: Optional[str] = None, - inaccessible: bool = 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, - directives: Sequence[object] = (), - graphql_type: Optional[Any] = None, + name=None, + is_subscription=False, + description=None, + provides=None, + requires=None, + external=False, + permission_classes=None, + deprecation_reason=None, + default=UNSET, + default_factory=UNSET, + directives: Sequence[StrawberrySchemaDirective] = (), # 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: Literal[True, False, None] = None, + init=None, ) -> Any: - from .schema_directives import ( - External, - Inaccessible, - Override, - Provides, - Requires, - Shareable, - Tag, - ) - directives = list(directives) if provides: - directives.append(Provides(fields=" ".join(provides))) + directives.append(Provides(" ".join(provides))) # type: ignore if requires: - directives.append(Requires(fields=" ".join(requires))) + directives.append(Requires(" ".join(requires))) # type: ignore if external: - directives.append(External()) - - if shareable: - directives.append(Shareable()) - - if tags: - directives.extend(Tag(name=tag) for tag in tags) - - if override: - directives.append(Override(override_from=override)) - - if inaccessible: - directives.append(Inaccessible()) + directives.append(External()) # type: ignore - return base_field( # type: ignore - resolver=resolver, # type: ignore + return base_field( + resolver=resolver, name=name, is_subscription=is_subscription, description=description, @@ -160,7 +120,6 @@ def field( deprecation_reason=deprecation_reason, default=default, default_factory=default_factory, - init=init, # type: ignore + init=init, directives=directives, - graphql_type=graphql_type, ) diff --git a/strawberry/federation/mutation.py b/strawberry/federation/mutation.py deleted file mode 100644 index d033d61c97..0000000000 --- a/strawberry/federation/mutation.py +++ /dev/null @@ -1,3 +0,0 @@ -from .field import field - -mutation = field diff --git a/strawberry/federation/object_type.py b/strawberry/federation/object_type.py index 5aaf6a8356..01668cd205 100644 --- a/strawberry/federation/object_type.py +++ b/strawberry/federation/object_type.py @@ -1,92 +1,26 @@ -from typing import ( - TYPE_CHECKING, - Callable, - Iterable, - Optional, - Sequence, - Type, - TypeVar, - Union, - overload, -) +from typing import Callable, List, TypeVar, overload -from strawberry.field import StrawberryField -from strawberry.field import field as base_field +from strawberry.federation.schema_directives import Key +from strawberry.field import StrawberryField, field as base_field from strawberry.object_type import type as base_type -from strawberry.unset import UNSET from strawberry.utils.typing import __dataclass_transform__ from .field import field -if TYPE_CHECKING: - from .schema_directives import Key - - -T = TypeVar("T", bound=Type) - - -def _impl_type( - cls: Optional[T], - *, - name: Optional[str] = None, - description: Optional[str] = None, - directives: Iterable[object] = (), - keys: Iterable[Union["Key", str]] = (), - extend: bool = False, - shareable: bool = False, - inaccessible: bool = UNSET, - tags: Iterable[str] = (), - is_input: bool = False, - is_interface: bool = False, -) -> T: - from strawberry.federation.schema_directives import ( - Inaccessible, - Key, - Shareable, - Tag, - ) - - directives = list(directives) - - directives.extend( - Key(fields=key, resolvable=UNSET) if isinstance(key, str) else key - for key in keys - ) - - if shareable: - directives.append(Shareable()) - - if inaccessible is not UNSET: - directives.append(Inaccessible()) - if tags: - directives.extend(Tag(name=tag) for tag in tags) - - return base_type( # type: ignore - cls, - name=name, - description=description, - directives=directives, - extend=extend, - is_input=is_input, - is_interface=is_interface, - ) +T = TypeVar("T") @overload @__dataclass_transform__( - order_default=True, - kw_only_default=True, - field_descriptors=(base_field, field, StrawberryField), + order_default=True, field_descriptors=(base_field, field, StrawberryField) ) def type( cls: T, *, - name: Optional[str] = None, - description: Optional[str] = None, - keys: Iterable[Union["Key", str]] = (), - inaccessible: bool = UNSET, - tags: Iterable[str] = (), + name: str = None, + description: str = None, + keys: List[str] = None, extend: bool = False, ) -> T: ... @@ -94,158 +28,32 @@ def type( @overload @__dataclass_transform__( - order_default=True, - kw_only_default=True, - field_descriptors=(base_field, field, StrawberryField), + order_default=True, field_descriptors=(base_field, field, StrawberryField) ) def type( *, - name: Optional[str] = None, - description: Optional[str] = None, - keys: Iterable[Union["Key", str]] = (), - inaccessible: bool = UNSET, - tags: Iterable[str] = (), + name: str = None, + description: str = None, + keys: List[str] = None, extend: bool = False, - shareable: bool = False, - directives: Iterable[object] = (), ) -> Callable[[T], T]: ... def type( - cls: Optional[T] = None, - *, - name: Optional[str] = None, - description: Optional[str] = None, - keys: Iterable[Union["Key", str]] = (), - inaccessible: bool = UNSET, - tags: Iterable[str] = (), - extend: bool = False, - shareable: bool = False, - directives: Iterable[object] = (), -): - return _impl_type( - cls, - name=name, - description=description, - directives=directives, - keys=keys, - extend=extend, - shareable=shareable, - inaccessible=inaccessible, - tags=tags, - ) - - -@overload -@__dataclass_transform__( - order_default=True, - kw_only_default=True, - field_descriptors=(base_field, field, StrawberryField), -) -def input( - cls: T, - *, - name: Optional[str] = None, - description: Optional[str] = None, - directives: Sequence[object] = (), - inaccessible: bool = UNSET, - tags: Iterable[str] = (), -) -> T: - ... - - -@overload -@__dataclass_transform__( - order_default=True, - kw_only_default=True, - field_descriptors=(base_field, field, StrawberryField), -) -def input( - *, - name: Optional[str] = None, - description: Optional[str] = None, - directives: Sequence[object] = (), - inaccessible: bool = UNSET, - tags: Iterable[str] = (), -) -> Callable[[T], T]: - ... - - -def input( - cls: Optional[T] = None, + cls=None, *, - name: Optional[str] = None, - description: Optional[str] = None, - directives: Sequence[object] = (), - inaccessible: bool = UNSET, - tags: Iterable[str] = (), + name=None, + description=None, + keys=None, + extend=False, ): - return _impl_type( - cls, - name=name, - description=description, - directives=directives, - inaccessible=inaccessible, - is_input=True, - tags=tags, - ) - - -@overload -@__dataclass_transform__( - order_default=True, - kw_only_default=True, - field_descriptors=(base_field, field, StrawberryField), -) -def interface( - cls: T, - *, - name: Optional[str] = None, - description: Optional[str] = None, - keys: Iterable[Union["Key", str]] = (), - inaccessible: bool = UNSET, - tags: Iterable[str] = (), - directives: Iterable[object] = (), -) -> T: - ... - - -@overload -@__dataclass_transform__( - order_default=True, - kw_only_default=True, - field_descriptors=(base_field, field, StrawberryField), -) -def interface( - *, - name: Optional[str] = None, - description: Optional[str] = None, - keys: Iterable[Union["Key", str]] = (), - inaccessible: bool = UNSET, - tags: Iterable[str] = (), - directives: Iterable[object] = (), -) -> Callable[[T], T]: - ... + directives = [Key(key) for key in keys or []] - -def interface( - cls: Optional[T] = None, - *, - name: Optional[str] = None, - description: Optional[str] = None, - keys: Iterable[Union["Key", str]] = (), - inaccessible: bool = UNSET, - tags: Iterable[str] = (), - directives: Iterable[object] = (), -): - return _impl_type( + return base_type( cls, name=name, description=description, - directives=directives, - keys=keys, - inaccessible=inaccessible, - is_interface=True, - tags=tags, + directives=directives, # type: ignore + extend=extend, ) diff --git a/strawberry/federation/scalar.py b/strawberry/federation/scalar.py deleted file mode 100644 index 41e985e8f2..0000000000 --- a/strawberry/federation/scalar.py +++ /dev/null @@ -1,116 +0,0 @@ -import sys -from typing import Any, Callable, Iterable, NewType, Optional, TypeVar, Union, overload - -from strawberry.custom_scalar import _process_scalar - -# in python 3.10+ NewType is a class -if sys.version_info >= (3, 10): - _T = TypeVar("_T", bound=Union[type, NewType]) -else: - _T = TypeVar("_T", bound=type) - - -def identity(x): # pragma: no cover - return x - - -@overload -def scalar( - *, - name: Optional[str] = None, - description: Optional[str] = None, - specified_by_url: Optional[str] = None, - serialize: Callable = identity, - parse_value: Optional[Callable] = None, - parse_literal: Optional[Callable] = None, - directives: Iterable[object] = (), - inaccessible: bool = False, - tags: Optional[Iterable[str]] = (), -) -> Callable[[_T], _T]: - ... - - -@overload -def scalar( - cls: _T, - *, - name: Optional[str] = None, - description: Optional[str] = None, - specified_by_url: Optional[str] = None, - serialize: Callable = identity, - parse_value: Optional[Callable] = None, - parse_literal: Optional[Callable] = None, - directives: Iterable[object] = (), - inaccessible: bool = False, - tags: Optional[Iterable[str]] = (), -) -> _T: - ... - - -def scalar( - cls=None, - *, - name: Optional[str] = None, - description: Optional[str] = None, - specified_by_url: Optional[str] = None, - serialize: Callable = identity, - parse_value: Optional[Callable] = None, - parse_literal: Optional[Callable] = None, - directives: Iterable[object] = (), - inaccessible: bool = False, - tags: Optional[Iterable[str]] = (), -) -> Any: - """Annotates a class or type as a GraphQL custom scalar. - - Example usages: - - >>> strawberry.federation.scalar( - >>> datetime.date, - >>> serialize=lambda value: value.isoformat(), - >>> parse_value=datetime.parse_date - >>> ) - - >>> Base64Encoded = strawberry.federation.scalar( - >>> NewType("Base64Encoded", bytes), - >>> serialize=base64.b64encode, - >>> parse_value=base64.b64decode - >>> ) - - >>> @strawberry.federation.scalar( - >>> serialize=lambda value: ",".join(value.items), - >>> parse_value=lambda value: CustomList(value.split(",")) - >>> ) - >>> class CustomList: - >>> def __init__(self, items): - >>> self.items = items - - """ - from strawberry.federation.schema_directives import Inaccessible, Tag - - if parse_value is None: - parse_value = cls - - directives = list(directives) - - if inaccessible: - directives.append(Inaccessible()) - - if tags: - directives.extend(Tag(name=tag) for tag in tags) - - def wrap(cls): - return _process_scalar( - cls, - name=name, - description=description, - specified_by_url=specified_by_url, - serialize=serialize, - parse_value=parse_value, - parse_literal=parse_literal, - directives=directives, - ) - - if cls is None: - return wrap - - return wrap(cls) diff --git a/strawberry/federation/schema.py b/strawberry/federation/schema.py index 0cc90a2289..780f4710d8 100644 --- a/strawberry/federation/schema.py +++ b/strawberry/federation/schema.py @@ -1,200 +1,53 @@ -from collections import defaultdict -from copy import copy -from functools import partial -from itertools import chain -from typing import Any, Dict, Iterable, List, Optional, Type, Union, cast +from typing import Any, Union, cast -from graphql import ExecutionContext as GraphQLExecutionContext from graphql import ( - GraphQLError, GraphQLField, - GraphQLInterfaceType, GraphQLList, GraphQLNonNull, GraphQLObjectType, GraphQLScalarType, + GraphQLString, GraphQLUnionType, ) from graphql.type.definition import GraphQLArgument -from strawberry.custom_scalar import ScalarDefinition, ScalarWrapper +from strawberry.custom_scalar import ScalarDefinition from strawberry.enum import EnumDefinition -from strawberry.extensions import Extension from strawberry.schema.types.concrete_type import TypeMap from strawberry.types.types import TypeDefinition from strawberry.union import StrawberryUnion -from strawberry.utils.cached_property import cached_property from strawberry.utils.inspect import get_func_args from ..printer import print_schema from ..schema import Schema as BaseSchema -from ..schema.config import StrawberryConfig +from .schema_directives import Key class Schema(BaseSchema): - def __init__( - self, - query: Optional[Type] = None, - mutation: Optional[Type] = None, - subscription: Optional[Type] = None, - # TODO: we should update directives' type in the main schema - directives: Iterable[Type] = (), - types: Iterable[Type] = (), - extensions: Iterable[Union[Type[Extension], Extension]] = (), - execution_context_class: Optional[Type[GraphQLExecutionContext]] = None, - config: Optional[StrawberryConfig] = None, - scalar_overrides: Optional[ - Dict[object, Union[Type, ScalarWrapper, ScalarDefinition]] - ] = None, - schema_directives: Iterable[object] = (), - enable_federation_2: bool = False, - ): - - query = self._get_federation_query_type(query) - - super().__init__( - query=query, - mutation=mutation, - subscription=subscription, - directives=directives, # type: ignore - types=types, - extensions=extensions, - execution_context_class=execution_context_class, - config=config, - scalar_overrides=scalar_overrides, - schema_directives=schema_directives, - ) + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) self._add_scalars() - self._add_entities_to_query() - - if enable_federation_2: - self._add_link_directives() - else: - self._remove_resolvable_field() - - def _get_federation_query_type(self, query: Optional[Type]) -> Type: - """Returns a new query type that includes the _service field. - - If the query type is provided, it will be used as the base for the new - query type. Otherwise, a new query type will be created. - - Federation needs the following two fields to be present in the query type: - - _service: This field is used by the gateway to query for the capabilities - of the federated service. - - _entities: This field is used by the gateway to query for the entities - that are part of the federated service. - - The _service field is added by default, but the _entities field is only - added if the schema contains an entity type. - """ - - # note we don't add the _entities field here, as we need to know if the - # schema contains an entity type first and we do that by leveraging - # the schema converter type map, so we don't have to do that twice - # TODO: ideally we should be able to do this without using the schema - # converter, but for now this is the easiest way to do it - # see `_add_entities_to_query` - - import strawberry - from strawberry.tools.create_type import create_type - from strawberry.tools.merge_types import merge_types - - @strawberry.type(name="_Service") - class Service: - sdl: str = strawberry.field( - resolver=lambda: print_schema(self), - ) - - @strawberry.field(name="_service") - def service() -> Service: - return Service() - - fields = [service] - - FederationQuery = create_type(name="Query", fields=fields) - - if query is None: - return FederationQuery - - query_type = merge_types( - "Query", - ( - FederationQuery, - query, - ), - ) - - # TODO: this should be probably done in merge_types - if query._type_definition.extend: - query_type._type_definition.extend = True # type: ignore - - return query_type - - def _add_entities_to_query(self): - entity_type = _get_entity_type(self.schema_converter.type_map) - - if not entity_type: - return - - self._schema.type_map[entity_type.name] = entity_type - fields = {"_entities": self._get_entities_field(entity_type)} - - # Copy the query type, update it to use the modified fields - query_type = cast(GraphQLObjectType, self._schema.query_type) - fields.update(query_type.fields) - - query_type = copy(query_type) - query_type._fields = fields - - self._schema.query_type = query_type - self._schema.type_map[query_type.name] = query_type + self._create_service_field() + self._extend_query_type() def entities_resolver(self, root, info, representations): results = [] for representation in representations: type_name = representation.pop("__typename") - type_ = self.schema_converter.type_map[type_name] - - definition = cast(TypeDefinition, type_.definition) - - if hasattr(definition.origin, "resolve_reference"): - - resolve_reference = definition.origin.resolve_reference - - func_args = get_func_args(resolve_reference) - kwargs = representation - - # TODO: use the same logic we use for other resolvers - if "info" in func_args: - kwargs["info"] = info - - get_result = partial(resolve_reference, **kwargs) - else: - from strawberry.arguments import convert_argument + type = self.schema_converter.type_map[type_name] - strawberry_schema = info.schema.extensions["strawberry-definition"] - config = strawberry_schema.config - scalar_registry = strawberry_schema.schema_converter.scalar_registry + definition = cast(TypeDefinition, type.definition) + resolve_reference = definition.origin.resolve_reference - get_result = partial( - convert_argument, - representation, - type_=definition.origin, - scalar_registry=scalar_registry, - config=config, - ) + func_args = get_func_args(resolve_reference) + kwargs = representation - try: - result = get_result() - except Exception as e: - result = GraphQLError( - f"Unable to resolve reference for {definition.origin}", - original_error=e, - ) + if "info" in func_args: + kwargs["info"] = info - results.append(result) + results.append(resolve_reference(**kwargs)) return results @@ -203,58 +56,27 @@ def _add_scalars(self): self._schema.type_map["_Any"] = self.Any - def _remove_resolvable_field(self) -> None: - # this might be removed when we remove support for federation 1 - # or when we improve how we print the directives - from ..unset import UNSET - from .schema_directives import Key - - for directive in self.schema_directives_in_use: - if isinstance(directive, Key): - directive.resolvable = UNSET - - @cached_property - def schema_directives_in_use(self) -> List[object]: - all_graphql_types = self._schema.type_map.values() - - directives = [] - - for type_ in all_graphql_types: - strawberry_definition = type_.extensions.get("strawberry-definition") - - if not strawberry_definition: - continue - - directives.extend(strawberry_definition.directives) - - fields = getattr(strawberry_definition, "fields", []) - values = getattr(strawberry_definition, "values", []) - - for field in chain(fields, values): - directives.extend(field.directives) + def _extend_query_type(self): + fields = {"_service": self._service_field} - return directives + entity_type = _get_entity_type(self.schema_converter.type_map) - def _add_link_directives(self): - from .schema_directives import FederationDirective, Link + if entity_type: + self._schema.type_map[entity_type.name] = entity_type - directive_by_url = defaultdict(set) + fields["_entities"] = self._get_entities_field(entity_type) - for directive in self.schema_directives_in_use: - if isinstance(directive, FederationDirective): - directive_by_url[directive.imported_from.url].add( - f"@{directive.imported_from.name}" - ) + query_type = cast(GraphQLObjectType, self._schema.query_type) + fields.update(query_type.fields) - link_directives = tuple( - Link( - url=url, - import_=list(sorted(directives)), - ) - for url, directives in directive_by_url.items() + self._schema.query_type = GraphQLObjectType( + name=query_type.name, + description=query_type.description, + fields=fields, ) - self.schema_directives = tuple(self.schema_directives) + link_directives + self._schema.type_map["_Service"] = self._service_type + self._schema.type_map[self._schema.query_type.name] = self._schema.query_type def _get_entities_field(self, entity_type: GraphQLUnionType) -> GraphQLField: return GraphQLField( @@ -267,6 +89,16 @@ def _get_entities_field(self, entity_type: GraphQLUnionType) -> GraphQLField: resolve=self.entities_resolver, ) + def _create_service_field(self): + self._service_type = GraphQLObjectType( + name="_Service", fields={"sdl": GraphQLField(GraphQLNonNull(GraphQLString))} + ) + + self._service_field = GraphQLField( + GraphQLNonNull(self._service_type), + resolve=lambda _, info: {"sdl": print_schema(self)}, + ) + def _get_entity_type(type_map: TypeMap): # https://www.apollographql.com/docs/apollo-server/federation/federation-spec/#resolve-requests-for-entities @@ -278,8 +110,6 @@ def _get_entity_type(type_map: TypeMap): type.implementation for type in type_map.values() if _has_federation_keys(type.definition) - # TODO: check this - and not isinstance(type.implementation, GraphQLInterfaceType) ] # If no types are annotated with the key directive, then the _Entity @@ -290,7 +120,7 @@ def _get_entity_type(type_map: TypeMap): entity_type = GraphQLUnionType("_Entity", federation_key_types) # type: ignore def _resolve_type(self, value, _type): - return self._type_definition.name + return type_map[self._type_definition.name].implementation entity_type.resolve_type = _resolve_type @@ -298,9 +128,7 @@ def _resolve_type(self, value, _type): def _is_key(directive: Any) -> bool: - from .schema_directives import Key - - return isinstance(directive, Key) + return directive.wrap is Key.wrap # type: ignore def _has_federation_keys( diff --git a/strawberry/federation/schema_directives.py b/strawberry/federation/schema_directives.py index 7b295f6fdf..3ad6dc277f 100644 --- a/strawberry/federation/schema_directives.py +++ b/strawberry/federation/schema_directives.py @@ -1,150 +1,25 @@ -from dataclasses import dataclass -from typing import ClassVar, List, Optional - -from strawberry import directive_field +from strawberry.custom_scalar import scalar from strawberry.schema_directive import Location, schema_directive -from strawberry.unset import UNSET - -from .types import FieldSet, LinkImport, LinkPurpose - - -@dataclass -class ImportedFrom: - name: str - url: str = "https://specs.apollo.dev/federation/v2.0" - - -class FederationDirective: - imported_from: ClassVar[ImportedFrom] - - -@schema_directive( - locations=[Location.FIELD_DEFINITION], name="external", print_definition=False -) -class External(FederationDirective): - imported_from: ClassVar[ImportedFrom] = ImportedFrom( - name="external", url="https://specs.apollo.dev/federation/v2.0" - ) - - -@schema_directive( - locations=[Location.FIELD_DEFINITION], name="requires", print_definition=False -) -class Requires(FederationDirective): - fields: FieldSet - imported_from: ClassVar[ImportedFrom] = ImportedFrom( - name="requires", url="https://specs.apollo.dev/federation/v2.0" - ) - - -@schema_directive( - locations=[Location.FIELD_DEFINITION], name="provides", print_definition=False -) -class Provides(FederationDirective): - fields: FieldSet - imported_from: ClassVar[ImportedFrom] = ImportedFrom( - name="provides", url="https://specs.apollo.dev/federation/v2.0" - ) - - -@schema_directive( - locations=[Location.OBJECT, Location.INTERFACE], - name="key", - repeatable=True, - print_definition=False, -) -class Key(FederationDirective): - fields: FieldSet - resolvable: Optional[bool] = True - imported_from: ClassVar[ImportedFrom] = ImportedFrom( - name="key", url="https://specs.apollo.dev/federation/v2.0" - ) - -@schema_directive( - locations=[Location.FIELD_DEFINITION, Location.OBJECT], - name="shareable", - print_definition=False, -) -class Shareable(FederationDirective): - imported_from: ClassVar[ImportedFrom] = ImportedFrom( - name="shareable", url="https://specs.apollo.dev/federation/v2.0" - ) +FieldSet = scalar(str, name="_FieldSet") -@schema_directive( - locations=[Location.SCHEMA], name="link", repeatable=True, print_definition=False -) -class Link: - url: Optional[str] - as_: Optional[str] = directive_field(name="as") - for_: Optional[LinkPurpose] = directive_field(name="for") - import_: Optional[List[Optional[LinkImport]]] = directive_field(name="import") - def __init__( - self, - url: Optional[str] = UNSET, - as_: Optional[str] = UNSET, - for_: Optional[LinkPurpose] = UNSET, - import_: Optional[List[Optional[LinkImport]]] = UNSET, - ): - self.url = url - self.as_ = as_ - self.for_ = for_ - self.import_ = import_ +@schema_directive(locations=[Location.FIELD_DEFINITION], name="external") +class External: + ... -@schema_directive( - locations=[ - Location.FIELD_DEFINITION, - Location.INTERFACE, - Location.OBJECT, - Location.UNION, - Location.ARGUMENT_DEFINITION, - Location.SCALAR, - Location.ENUM, - Location.ENUM_VALUE, - Location.INPUT_OBJECT, - Location.INPUT_FIELD_DEFINITION, - ], - name="tag", - repeatable=True, - print_definition=False, -) -class Tag(FederationDirective): - name: str - imported_from: ClassVar[ImportedFrom] = ImportedFrom( - name="tag", url="https://specs.apollo.dev/federation/v2.0" - ) +@schema_directive(locations=[Location.FIELD_DEFINITION], name="requires") +class Requires: + fields: FieldSet # type: ignore -@schema_directive( - locations=[Location.FIELD_DEFINITION], name="override", print_definition=False -) -class Override(FederationDirective): - override_from: str = directive_field(name="from") - imported_from: ClassVar[ImportedFrom] = ImportedFrom( - name="override", url="https://specs.apollo.dev/federation/v2.0" - ) +@schema_directive(locations=[Location.FIELD_DEFINITION], name="provides") +class Provides: + fields: FieldSet # type: ignore -@schema_directive( - locations=[ - Location.FIELD_DEFINITION, - Location.OBJECT, - Location.INTERFACE, - Location.UNION, - Location.ARGUMENT_DEFINITION, - Location.SCALAR, - Location.ENUM, - Location.ENUM_VALUE, - Location.INPUT_OBJECT, - Location.INPUT_FIELD_DEFINITION, - ], - name="inaccessible", - print_definition=False, -) -class Inaccessible(FederationDirective): - imported_from: ClassVar[ImportedFrom] = ImportedFrom( - name="inaccessible", url="https://specs.apollo.dev/federation/v2.0" - ) +@schema_directive(locations=[Location.OBJECT, Location.INTERFACE], name="key") +class Key: + fields: FieldSet # type: ignore diff --git a/strawberry/federation/types.py b/strawberry/federation/types.py deleted file mode 100644 index 4a6cb19a9b..0000000000 --- a/strawberry/federation/types.py +++ /dev/null @@ -1,14 +0,0 @@ -from enum import Enum - -from strawberry.custom_scalar import scalar -from strawberry.enum import enum - -FieldSet = scalar(str, name="_FieldSet") - -LinkImport = scalar(object, name="link__Import") - - -@enum(name="link__Purpose") -class LinkPurpose(Enum): - SECURITY = "SECURITY" - EXECUTION = "EXECUTION" diff --git a/strawberry/federation/union.py b/strawberry/federation/union.py deleted file mode 100644 index 8746c8d4b2..0000000000 --- a/strawberry/federation/union.py +++ /dev/null @@ -1,43 +0,0 @@ -from typing import Iterable, Optional, Tuple, Type, TypeVar, Union - -from strawberry.union import union as base_union - -Types = TypeVar("Types", bound=Type) - - -def union( - name: str, - types: Tuple[Types, ...], - *, - description: Optional[str] = None, - directives: Iterable[object] = (), - inaccessible: bool = False, - tags: Optional[Iterable[str]] = (), -) -> Union[Types]: - """Creates a new named Union type. - - Example usages: - - >>> @strawberry.type - ... class A: ... - >>> @strawberry.type - ... class B: ... - >>> strawberry.federation.union("Name", (A, Optional[B])) - """ - - from strawberry.federation.schema_directives import Inaccessible, Tag - - directives = list(directives) - - if inaccessible: - directives.append(Inaccessible()) - - if tags: - directives.extend(Tag(name=tag) for tag in tags) - - return base_union( - name, - types, - description=description, - directives=directives, - ) diff --git a/strawberry/field.py b/strawberry/field.py index b6593c0f2a..027d7e0412 100644 --- a/strawberry/field.py +++ b/strawberry/field.py @@ -17,51 +17,31 @@ Union, overload, ) + +from backports.cached_property import cached_property from typing_extensions import Literal from strawberry.annotation import StrawberryAnnotation -from strawberry.arguments import StrawberryArgument -from strawberry.exceptions import InvalidArgumentTypeError, InvalidDefaultFactoryError +from strawberry.arguments import UNSET, StrawberryArgument +from strawberry.exceptions import InvalidDefaultFactoryError, InvalidFieldArgument +from strawberry.schema_directive import StrawberrySchemaDirective from strawberry.type import StrawberryType from strawberry.types.info import Info from strawberry.union import StrawberryUnion -from strawberry.utils.cached_property import cached_property from .permission import BasePermission from .types.fields.resolver import StrawberryResolver + if TYPE_CHECKING: from .object_type import TypeDefinition -T = TypeVar("T") - - -_RESOLVER_TYPE = Union[ - StrawberryResolver[T], - Callable[..., T], - "staticmethod[T]", - "classmethod[T]", -] - -UNRESOLVED = object() - - -def _is_generic(resolver_type: Union[StrawberryType, type]) -> bool: - """Returns True if `resolver_type` is generic else False""" - if isinstance(resolver_type, StrawberryType): - return resolver_type.is_generic - - # solves the Generic subclass case - if hasattr(resolver_type, "_type_definition"): - return resolver_type._type_definition.is_generic - - return False +_RESOLVER_TYPE = Union[StrawberryResolver, Callable, staticmethod, classmethod] class StrawberryField(dataclasses.Field): - type_annotation: Optional[StrawberryAnnotation] - default_resolver: Callable[[Any, str], object] = getattr + python_name: str def __init__( self, @@ -73,11 +53,10 @@ def __init__( description: Optional[str] = None, base_resolver: Optional[StrawberryResolver] = None, permission_classes: List[Type[BasePermission]] = (), # type: ignore - default: object = dataclasses.MISSING, - default_factory: Union[Callable[[], Any], object] = dataclasses.MISSING, - metadata: Optional[Mapping[Any, Any]] = None, + default: object = UNSET, + default_factory: Union[Callable[[], Any], object] = UNSET, deprecation_reason: Optional[str] = None, - directives: Sequence[object] = (), + directives: Sequence[StrawberrySchemaDirective] = (), ): # basic fields are fields with no provided resolver is_basic_field = not base_resolver @@ -86,16 +65,22 @@ def __init__( # kw_only was added to python 3.10 and it is required if sys.version_info >= (3, 10): - kwargs["kw_only"] = dataclasses.MISSING - - super().__init__( - default=default, - default_factory=default_factory, # type: ignore + kwargs["kw_only"] = False + + super().__init__( # type: ignore + default=(default if default is not UNSET else dataclasses.MISSING), + default_factory=( + # mypy is not able to understand that default factory + # is a callable so we do a type ignore + default_factory # type: ignore + if default_factory is not UNSET + else dataclasses.MISSING + ), init=is_basic_field, repr=is_basic_field, compare=is_basic_field, hash=None, - metadata=metadata or {}, + metadata={}, **kwargs, ) @@ -141,46 +126,23 @@ def __call__(self, resolver: _RESOLVER_TYPE) -> "StrawberryField": if isinstance(argument.type_annotation.annotation, str): continue elif isinstance(argument.type, StrawberryUnion): - raise InvalidArgumentTypeError( - resolver, - argument, + raise InvalidFieldArgument( + resolver.name, + argument.python_name, + "Union", ) elif getattr(argument.type, "_type_definition", False): if argument.type._type_definition.is_interface: # type: ignore - raise InvalidArgumentTypeError( - resolver, - argument, + raise InvalidFieldArgument( + resolver.name, + argument.python_name, + "Interface", ) self.base_resolver = resolver return self - def get_result( - self, source: Any, info: Optional[Info], args: List[Any], kwargs: Dict[str, Any] - ) -> Union[Awaitable[Any], Any]: - """ - Calls the resolver defined for the StrawberryField. - If the field doesn't have a resolver defined we default - to using the default resolver specified in StrawberryConfig. - """ - - if self.base_resolver: - return self.base_resolver(*args, **kwargs) - - return self.default_resolver(source, self.python_name) - - @property - def is_basic_field(self) -> bool: - """ - Flag indicating if this is a "basic" field that has no resolver or - permission classes, i.e. it just returns the relevant attribute from - the source object. If it is a basic field we can avoid constructing - an `Info` object and running any permission checks in the resolver - which improves performance. - """ - return not self.base_resolver and not self.permission_classes - @property def arguments(self) -> List[StrawberryArgument]: if not self.base_resolver: @@ -200,7 +162,10 @@ def _python_name(self) -> Optional[str]: def _set_python_name(self, name: str) -> None: self.name = name - python_name: str = property(_python_name, _set_python_name) # type: ignore[assignment] # noqa: E501 + # using the function syntax for property here in order to make it easier + # to ignore this mypy error: + # https://github.com/python/mypy/issues/4125 + python_name = property(_python_name, _set_python_name) # type: ignore @property def base_resolver(self) -> Optional[StrawberryResolver]: @@ -227,47 +192,31 @@ def base_resolver(self, resolver: StrawberryResolver) -> None: _ = resolver.arguments @property # type: ignore - def type(self) -> Union[StrawberryType, type, Literal[UNRESOLVED]]: # type: ignore + def type(self) -> Union[StrawberryType, type]: # type: ignore # We are catching NameError because dataclasses tries to fetch the type # of the field from the class before the class is fully defined. # This triggers a NameError error when using forward references because # our `type` property tries to find the field type from the global namespace # but it is not yet defined. try: - # Prioritise the field type over the resolver return type - if self.type_annotation is not None: - return self.type_annotation.resolve() - if self.base_resolver is not None: # Handle unannotated functions (such as lambdas) if self.base_resolver.type is not None: + return self.base_resolver.type - # Generics will raise MissingTypesForGenericError later - # on if we let it be returned. So use `type_annotation` instead - # which is the same behaviour as having no type information. - if not _is_generic(self.base_resolver.type): - return self.base_resolver.type + assert self.type_annotation is not None - # If we get this far it means that we don't have a field type and - # the resolver doesn't have a return type so all we can do is return - # UNRESOLVED here. - # This case will raise a MissingReturnAnnotationError exception in the - # _check_field_annotations function: - # https://github.com/strawberry-graphql/strawberry/blob/846f060a63cb568b3cdc0deb26c308a8d0718190/strawberry/object_type.py#L76-L80 - return UNRESOLVED + if not isinstance(self.type_annotation, StrawberryAnnotation): + # TODO: This is because of dataclasses + return self.type_annotation + return self.type_annotation.resolve() except NameError: - return UNRESOLVED + return None # type: ignore @type.setter def type(self, type_: Any) -> None: - # Note: we aren't setting a namespace here for the annotation. That - # happens in the `_get_fields` function in `types/type_resolver` so - # that we have access to the correct namespace for the object type - # the field is attached to. - self.type_annotation = StrawberryAnnotation.from_annotation( - type_, namespace=None - ) + self.type_annotation = type_ # TODO: add this to arguments (and/or move it to StrawberryType) @property @@ -286,17 +235,19 @@ def type_params(self) -> List[TypeVar]: def copy_with( self, type_var_map: Mapping[TypeVar, Union[StrawberryType, builtins.type]] ) -> "StrawberryField": - new_type: Union[StrawberryType, type] = self.type + new_type: Union[StrawberryType, type] # TODO: Remove with creation of StrawberryObject. Will act same as other # StrawberryTypes if hasattr(self.type, "_type_definition"): - type_definition: TypeDefinition = self.type._type_definition + type_definition: TypeDefinition = self.type._type_definition # type: ignore if type_definition.is_generic: type_ = type_definition new_type = type_.copy_with(type_var_map) - elif isinstance(self.type, StrawberryType): + else: + assert isinstance(self.type, StrawberryType) + new_type = self.type.copy_with(type_var_map) new_resolver = ( @@ -318,10 +269,23 @@ def copy_with( permission_classes=self.permission_classes, default=self.default_value, # ignored because of https://github.com/python/mypy/issues/6910 - default_factory=self.default_factory, + default_factory=self.default_factory, # type: ignore[misc] deprecation_reason=self.deprecation_reason, ) + def get_result( + self, source: Any, info: Info, args: List[Any], kwargs: Dict[str, Any] + ) -> Union[Awaitable[Any], Any]: + """ + Calls the resolver defined for the StrawberryField. If the field doesn't have a + resolver defined we default to using getattr on `source`. + """ + + if self.base_resolver: + return self.base_resolver(*args, **kwargs) + + return getattr(source, self.python_name) + @property def _has_async_permission_classes(self) -> bool: for permission_class in self.permission_classes: @@ -338,21 +302,22 @@ def is_async(self) -> bool: return self._has_async_permission_classes or self._has_async_base_resolver +T = TypeVar("T") + + @overload def field( *, - resolver: _RESOLVER_TYPE[T], + resolver: Callable[[], T], 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, + default: Any = UNSET, + default_factory: Union[Callable, object] = UNSET, + directives: Optional[Sequence[StrawberrySchemaDirective]] = (), ) -> T: ... @@ -366,50 +331,44 @@ def field( 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, + default: Any = UNSET, + default_factory: Union[Callable, object] = UNSET, + directives: Optional[Sequence[StrawberrySchemaDirective]] = (), ) -> Any: ... @overload def field( - resolver: _RESOLVER_TYPE[T], + resolver: _RESOLVER_TYPE, *, 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, + default: Any = UNSET, + default_factory: Union[Callable, object] = UNSET, + directives: Optional[Sequence[StrawberrySchemaDirective]] = (), ) -> StrawberryField: ... def field( - resolver: Optional[_RESOLVER_TYPE[Any]] = None, + resolver=None, *, - 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, + name=None, + is_subscription=False, + description=None, + permission_classes=None, + deprecation_reason=None, + default=UNSET, + default_factory=UNSET, + directives=(), # 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: Literal[True, False, None] = None, + init=None, ) -> Any: """Annotates a method or property as a GraphQL field. @@ -426,20 +385,17 @@ def field( it can be used both as decorator and as a normal function. """ - type_annotation = StrawberryAnnotation.from_annotation(graphql_type) - field_ = StrawberryField( python_name=None, graphql_name=name, - type_annotation=type_annotation, + type_annotation=None, 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 (), + directives=directives, ) if resolver: diff --git a/strawberry/file_uploads/__init__.py b/strawberry/file_uploads/__init__.py index 608dff7741..264ddc8d6e 100644 --- a/strawberry/file_uploads/__init__.py +++ b/strawberry/file_uploads/__init__.py @@ -1,3 +1,4 @@ from .scalars import Upload + __all__ = ["Upload"] diff --git a/strawberry/file_uploads/scalars.py b/strawberry/file_uploads/scalars.py index 4dcaccf19a..8a741fd256 100644 --- a/strawberry/file_uploads/scalars.py +++ b/strawberry/file_uploads/scalars.py @@ -2,4 +2,5 @@ from ..custom_scalar import scalar + Upload = scalar(NewType("Upload", bytes), parse_value=lambda x: x) diff --git a/strawberry/file_uploads/utils.py b/strawberry/file_uploads/utils.py index f745b82abf..9f9fe1b13e 100644 --- a/strawberry/file_uploads/utils.py +++ b/strawberry/file_uploads/utils.py @@ -7,7 +7,6 @@ def replace_placeholders_with_files( files_map: Mapping[str, List[str]], files: Mapping[str, Any], ) -> Dict[str, Any]: - # TODO: test this with missing variables in operations_with_placeholders operations = copy.deepcopy(operations_with_placeholders) for multipart_form_field_name, operations_paths in files_map.items(): diff --git a/strawberry/flask/graphiql.py b/strawberry/flask/graphiql.py index b52252bf73..3b681b1cef 100644 --- a/strawberry/flask/graphiql.py +++ b/strawberry/flask/graphiql.py @@ -1,10 +1,13 @@ -from typing import Any +from os.path import abspath, dirname, join -def should_render_graphiql(graphiql: bool, request: Any) -> bool: - if not graphiql: - return False - return any( - supported_header in request.environ.get("HTTP_ACCEPT", "") - for supported_header in ("text/html", "*/*") - ) +def render_graphiql_page(): + dir_path = abspath(join(dirname(__file__), "..")) + graphiql_html_file = f"{dir_path}/static/graphiql.html" + + html_string = None + + with open(graphiql_html_file, "r") as f: + html_string = f.read() + + return html_string.replace("{{ SUBSCRIPTION_ENABLED }}", "false") diff --git a/strawberry/flask/views.py b/strawberry/flask/views.py index cfd78fc430..cd997ff994 100644 --- a/strawberry/flask/views.py +++ b/strawberry/flask/views.py @@ -1,221 +1,75 @@ import json -from typing import Dict -from flask import Response, render_template_string, request -from flask.typing import ResponseReturnValue +from flask import Response, abort, render_template_string, request from flask.views import View from strawberry.exceptions import MissingQueryError from strawberry.file_uploads.utils import replace_placeholders_with_files -from strawberry.flask.graphiql import should_render_graphiql -from strawberry.http import ( - GraphQLHTTPResponse, - parse_query_params, - parse_request_data, - process_result, -) -from strawberry.schema.base import BaseSchema -from strawberry.schema.exceptions import InvalidOperationTypeError +from strawberry.http import GraphQLHTTPResponse, parse_request_data, process_result from strawberry.types import ExecutionResult -from strawberry.types.graphql import OperationType -from strawberry.utils.graphiql import get_graphiql_html +from ..schema import BaseSchema +from .graphiql import render_graphiql_page -class BaseGraphQLView(View): + +class GraphQLView(View): methods = ["GET", "POST"] def __init__( self, schema: BaseSchema, graphiql: bool = True, - allow_queries_via_get: bool = True, ): - self.schema = schema self.graphiql = graphiql - self.allow_queries_via_get = allow_queries_via_get - - def render_template(self, template: str) -> str: - return render_template_string(template) - - def encode_json(self, response_data: GraphQLHTTPResponse) -> str: - return json.dumps(response_data) - + self.schema = schema -class GraphQLView(BaseGraphQLView): - def get_root_value(self) -> object: + def get_root_value(self): return None - def get_context(self, response: Response) -> Dict[str, object]: - return {"request": request, "response": response} + def get_context(self): + return {"request": request} + + def render_template(self, template=None): + return render_template_string(template) def process_result(self, result: ExecutionResult) -> GraphQLHTTPResponse: return process_result(result) - def dispatch_request(self) -> ResponseReturnValue: - method = request.method - content_type = request.content_type or "" - - if request.method not in {"POST", "GET"}: - return Response( - "Unsupported method, must be of request type POST or GET", 405 - ) - - if "application/json" in content_type: - try: - data = json.loads(request.data) - except json.JSONDecodeError: - return Response( - status=400, response="Unable to parse request body as JSON" - ) - elif content_type.startswith("multipart/form-data"): - - try: - operations = json.loads(request.form.get("operations", "{}")) - files_map = json.loads(request.form.get("map", "{}")) - except json.JSONDecodeError: - return Response( - status=400, response="Unable to parse request body as JSON" - ) - - try: - data = replace_placeholders_with_files( - operations, files_map, request.files - ) - except KeyError: - return Response(status=400, response="File(s) missing in form data") - elif method == "GET" and request.args: - try: - data = parse_query_params(request.args.to_dict()) - except json.JSONDecodeError: - return Response( - status=400, response="Unable to parse request body as JSON" - ) - elif method == "GET" and should_render_graphiql(self.graphiql, request): - template = get_graphiql_html(False) + def dispatch_request(self): + if "text/html" in request.environ.get("HTTP_ACCEPT", ""): + if not self.graphiql: + abort(404) + template = render_graphiql_page() return self.render_template(template=template) - elif method == "GET": - return Response(status=404) - else: - return Response("Unsupported Media Type", 415) - - request_data = parse_request_data(data) - response = Response(status=200, content_type="application/json") - context = self.get_context(response) + if request.content_type.startswith("multipart/form-data"): + operations = json.loads(request.form.get("operations", "{}")) + files_map = json.loads(request.form.get("map", "{}")) - allowed_operation_types = OperationType.from_http(method) + data = replace_placeholders_with_files(operations, files_map, request.files) - if not self.allow_queries_via_get and method == "GET": - allowed_operation_types = allowed_operation_types - {OperationType.QUERY} + else: + data = request.json try: - result = self.schema.execute_sync( - request_data.query, - variable_values=request_data.variables, - context_value=context, - operation_name=request_data.operation_name, - root_value=self.get_root_value(), - allowed_operation_types=allowed_operation_types, - ) - except InvalidOperationTypeError as e: - return Response(e.as_http_error_reason(method), 400) + request_data = parse_request_data(data) except MissingQueryError: - return Response("No GraphQL query found in the request", 400) - - response_data = self.process_result(result) - response.set_data(self.encode_json(response_data)) - - return response - - -class AsyncGraphQLView(BaseGraphQLView): - methods = ["GET", "POST"] - - async def get_root_value(self) -> object: - return None + return Response("No valid query was provided for the request", 400) - async def get_context(self, response: Response) -> Dict[str, object]: - return {"request": request, "response": response} - - async def process_result(self, result: ExecutionResult) -> GraphQLHTTPResponse: - return process_result(result) - - async def dispatch_request(self) -> ResponseReturnValue: # type: ignore[override] - method = request.method - content_type = request.content_type or "" - - if request.method not in {"POST", "GET"}: - return Response( - "Unsupported method, must be of request type POST or GET", 405 - ) - - if "application/json" in content_type: - try: - data = json.loads(request.data) - except json.JSONDecodeError: - return Response( - status=400, response="Unable to parse request body as JSON" - ) - elif content_type.startswith("multipart/form-data"): - - try: - operations = json.loads(request.form.get("operations", "{}")) - files_map = json.loads(request.form.get("map", "{}")) - except json.JSONDecodeError: - return Response( - status=400, response="Unable to parse request body as JSON" - ) - - try: - data = replace_placeholders_with_files( - operations, files_map, request.files - ) - except KeyError: - return Response(status=400, response="File(s) missing in form data") - elif method == "GET" and request.args: - try: - data = parse_query_params(request.args.to_dict()) - except json.JSONDecodeError: - return Response( - status=400, response="Unable to parse request body as JSON" - ) - - elif method == "GET" and should_render_graphiql(self.graphiql, request): - template = get_graphiql_html(False) - - return self.render_template(template=template) - elif method == "GET": - return Response(status=404) - else: - return Response("Unsupported Media Type", 415) + context = self.get_context() - request_data = parse_request_data(data) + result = self.schema.execute_sync( + request_data.query, + variable_values=request_data.variables, + context_value=context, + operation_name=request_data.operation_name, + root_value=self.get_root_value(), + ) - response = Response(status=200, content_type="application/json") - context = await self.get_context(response) - - allowed_operation_types = OperationType.from_http(method) - - if not self.allow_queries_via_get and method == "GET": - allowed_operation_types = allowed_operation_types - {OperationType.QUERY} - - root_value = await self.get_root_value() - - try: - result = await self.schema.execute( - request_data.query, - variable_values=request_data.variables, - context_value=context, - operation_name=request_data.operation_name, - root_value=root_value, - allowed_operation_types=allowed_operation_types, - ) - except InvalidOperationTypeError as e: - return Response(e.as_http_error_reason(method), 400) - except MissingQueryError: - return Response("No GraphQL query found in the request", 400) - - response_data = await self.process_result(result) - response.set_data(self.encode_json(response_data)) + response_data = self.process_result(result) - return response + return Response( + json.dumps(response_data), + status=200, + content_type="application/json", + ) diff --git a/strawberry/http.py b/strawberry/http.py new file mode 100644 index 0000000000..47b02c66c8 --- /dev/null +++ b/strawberry/http.py @@ -0,0 +1,46 @@ +from dataclasses import dataclass +from typing import Any, Dict, List, Optional + +from typing_extensions import TypedDict + +from graphql.error import format_error as format_graphql_error + +from strawberry.exceptions import MissingQueryError +from strawberry.types import ExecutionResult + + +class GraphQLHTTPResponse(TypedDict, total=False): + data: Optional[Dict[str, Any]] + errors: Optional[List[Any]] + extensions: Optional[Dict[str, Any]] + + +def process_result(result: ExecutionResult) -> GraphQLHTTPResponse: + data: GraphQLHTTPResponse = {"data": result.data} + + if result.errors: + data["errors"] = [format_graphql_error(err) for err in result.errors] + if result.extensions: + data["extensions"] = result.extensions + + return data + + +@dataclass +class GraphQLRequestData: + query: str + variables: Optional[Dict[str, Any]] + operation_name: Optional[str] + + +def parse_request_data(data: Dict) -> GraphQLRequestData: + if "query" not in data: + raise MissingQueryError() + + result = GraphQLRequestData( + query=data["query"], + variables=data.get("variables"), + operation_name=data.get("operationName"), + ) + + return result diff --git a/strawberry/http/__init__.py b/strawberry/http/__init__.py deleted file mode 100644 index a52f0ae008..0000000000 --- a/strawberry/http/__init__.py +++ /dev/null @@ -1,49 +0,0 @@ -import json -from dataclasses import dataclass -from typing import Any, Dict, List, Mapping, Optional -from typing_extensions import TypedDict - -from graphql.error.graphql_error import format_error as format_graphql_error - -from strawberry.types import ExecutionResult - - -class GraphQLHTTPResponse(TypedDict, total=False): - data: Optional[Dict[str, object]] - errors: Optional[List[object]] - extensions: Optional[Dict[str, object]] - - -def process_result(result: ExecutionResult) -> GraphQLHTTPResponse: - data: GraphQLHTTPResponse = {"data": result.data} - - if result.errors: - data["errors"] = [format_graphql_error(err) for err in result.errors] - if result.extensions: - data["extensions"] = result.extensions - - return data - - -@dataclass -class GraphQLRequestData: - # query is optional here as it can be added by an extensions - # (for example an extension for persisted queries) - query: Optional[str] - variables: Optional[Dict[str, Any]] - operation_name: Optional[str] - - -def parse_query_params(params: Dict[str, str]) -> Dict[str, Any]: - if "variables" in params: - params["variables"] = json.loads(params["variables"]) - - return params - - -def parse_request_data(data: Mapping[str, Any]) -> GraphQLRequestData: - return GraphQLRequestData( - query=data.get("query"), - variables=data.get("variables"), - operation_name=data.get("operationName"), - ) diff --git a/strawberry/http/temporal_response.py b/strawberry/http/temporal_response.py deleted file mode 100644 index 85c14a4f66..0000000000 --- a/strawberry/http/temporal_response.py +++ /dev/null @@ -1,6 +0,0 @@ -from dataclasses import dataclass - - -@dataclass -class TemporalResponse: - status_code: int = 200 diff --git a/strawberry/lazy_type.py b/strawberry/lazy_type.py index af00513118..78c001b107 100644 --- a/strawberry/lazy_type.py +++ b/strawberry/lazy_type.py @@ -1,10 +1,8 @@ import importlib import inspect -import sys -import warnings from dataclasses import dataclass -from pathlib import Path -from typing import ForwardRef, Generic, Optional, Type, TypeVar, cast +from typing import Generic, Optional, Type, TypeVar + TypeName = TypeVar("TypeName") Module = TypeVar("Module") @@ -14,46 +12,22 @@ class LazyType(Generic[TypeName, Module]): type_name: str module: str - package: Optional[str] = None + package: Optional[str] def __class_getitem__(cls, params): - warnings.warn( - ( - "LazyType is deprecated, use " - "Annotated[YourType, strawberry.lazy(path)] instead" - ), - DeprecationWarning, - stacklevel=2, - ) - type_name, module = params package = None if module.startswith("."): current_frame = inspect.currentframe() - assert current_frame is not None - assert current_frame.f_back is not None - package = current_frame.f_back.f_globals["__package__"] + package = current_frame.f_back.f_globals["__package__"] # type: ignore return cls(type_name, module, package) def resolve_type(self) -> Type: module = importlib.import_module(self.module, self.package) - main_module = sys.modules.get("__main__", None) - if main_module: - # If lazy type points to the main module, use it instead of the imported - # module. Otherwise duplication checks during schema-conversion might fail. - # Refer to: https://github.com/strawberry-graphql/strawberry/issues/2397 - if main_module.__spec__ and main_module.__spec__.name == self.module: - module = main_module - elif hasattr(main_module, "__file__") and hasattr(module, "__file__"): - if ( - main_module.__file__ - and module.__file__ - and Path(main_module.__file__).samefile(module.__file__) - ): - module = main_module + return module.__dict__[self.type_name] # this empty call method allows LazyTypes to be used in generic types @@ -61,22 +35,3 @@ def resolve_type(self) -> Type: def __call__(self): # pragma: no cover return None - - -class StrawberryLazyReference: - def __init__(self, module: str) -> None: - self.module = module - self.package = None - - if module.startswith("."): - frame = inspect.stack()[2][0] - # TODO: raise a nice error if frame is None - assert frame is not None - self.package = cast(str, frame.f_globals["__package__"]) - - def resolve_forward_ref(self, forward_ref: ForwardRef) -> LazyType: - return LazyType(forward_ref.__forward_arg__, self.module, self.package) - - -def lazy(module_path: str) -> StrawberryLazyReference: - return StrawberryLazyReference(module_path) diff --git a/strawberry/mutation.py b/strawberry/mutation.py index 29466bc82e..ff86762feb 100644 --- a/strawberry/mutation.py +++ b/strawberry/mutation.py @@ -2,7 +2,8 @@ from .field import field -# Mutations and subscriptions are field, we might want to separate -# things in the long run for example to provide better errors + +# Mutations and subscriptions are field, we might want to separate things in the long run +# for example to provide better errors mutation = field subscription = partial(field, is_subscription=True) diff --git a/strawberry/object_type.py b/strawberry/object_type.py index 4d69b6101e..c808c71386 100644 --- a/strawberry/object_type.py +++ b/strawberry/object_type.py @@ -1,19 +1,9 @@ import dataclasses import inspect -import sys import types -from typing import ( - Callable, - Dict, - List, - Optional, - Sequence, - Type, - TypeVar, - Union, - cast, - overload, -) +from typing import Callable, List, Optional, Sequence, Type, TypeVar, cast, overload + +from strawberry.schema_directive import StrawberrySchemaDirective from .exceptions import ( MissingFieldAnnotationError, @@ -23,12 +13,9 @@ from .field import StrawberryField, field from .types.type_resolver import _get_fields from .types.types import TypeDefinition -from .utils.dataclasses import add_custom_init_fn from .utils.str_converters import to_camel_case from .utils.typing import __dataclass_transform__ -T = TypeVar("T", bound=Type) - def _get_interfaces(cls: Type) -> List[TypeDefinition]: interfaces = [] @@ -68,26 +55,18 @@ def _check_field_annotations(cls: Type): # to make sure dataclasses.dataclass is ready for it if isinstance(field_, StrawberryField): - # If the field has a type override then use that instead of using - # the class annotations or resolver annotation - if field_.type_annotation is not None: - cls_annotations[field_name] = field_.type_annotation.annotation - continue - # Make sure the cls has an annotation if field_name not in cls_annotations: # If the field uses the default resolver, the field _must_ be # annotated if not field_.base_resolver: - raise MissingFieldAnnotationError(field_name, cls) + raise MissingFieldAnnotationError(field_name) # The resolver _must_ have a return type annotation # TODO: Maybe check this immediately when adding resolver to # field if field_.base_resolver.type_annotation is None: - raise MissingReturnAnnotationError( - field_name, resolver=field_.base_resolver - ) + raise MissingReturnAnnotationError(field_name) cls_annotations[field_name] = field_.base_resolver.type_annotation @@ -101,7 +80,7 @@ def _check_field_annotations(cls: Type): # dataclasses.field if field_name not in cls_annotations: # Field object exists but did not get an annotation - raise MissingFieldAnnotationError(field_name, cls) + raise MissingFieldAnnotationError(field_name) def _wrap_dataclass(cls: Type): @@ -111,21 +90,7 @@ def _wrap_dataclass(cls: Type): # Ensure all Fields have been properly type-annotated _check_field_annotations(cls) - dclass_kwargs: Dict[str, bool] = {} - - # Python 3.10 introduces the kw_only param. If we're on an older version - # then generate our own custom init function - if sys.version_info >= (3, 10): - dclass_kwargs["kw_only"] = True - else: - dclass_kwargs["init"] = False - - dclass = dataclasses.dataclass(cls, **dclass_kwargs) - - if sys.version_info < (3, 10): - add_custom_init_fn(dclass) - - return dclass + return dataclasses.dataclass(cls) def _process_type( @@ -135,7 +100,7 @@ def _process_type( is_input: bool = False, is_interface: bool = False, description: Optional[str] = None, - directives: Optional[Sequence[object]] = (), + directives: Optional[Sequence[StrawberrySchemaDirective]] = (), extend: bool = False, ): name = name or to_camel_case(cls.__name__) @@ -181,49 +146,48 @@ def _process_type( return cls +T = TypeVar("T") + + @overload -@__dataclass_transform__( - order_default=True, kw_only_default=True, field_descriptors=(field, StrawberryField) -) +@__dataclass_transform__(order_default=True, field_descriptors=(field, StrawberryField)) def type( cls: T, *, - name: Optional[str] = None, + name: str = None, is_input: bool = False, is_interface: bool = False, - description: Optional[str] = None, - directives: Optional[Sequence[object]] = (), + description: str = None, + directives: Optional[Sequence[StrawberrySchemaDirective]] = (), extend: bool = False, ) -> T: ... @overload -@__dataclass_transform__( - order_default=True, kw_only_default=True, field_descriptors=(field, StrawberryField) -) +@__dataclass_transform__(order_default=True, field_descriptors=(field, StrawberryField)) def type( *, - name: Optional[str] = None, + name: str = None, is_input: bool = False, is_interface: bool = False, - description: Optional[str] = None, - directives: Optional[Sequence[object]] = (), + description: str = None, + directives: Optional[Sequence[StrawberrySchemaDirective]] = (), extend: bool = False, ) -> Callable[[T], T]: ... def type( - cls: Optional[T] = None, + cls=None, *, - name: Optional[str] = None, - is_input: bool = False, - is_interface: bool = False, - description: Optional[str] = None, - directives: Optional[Sequence[object]] = (), - extend: bool = False, -) -> Union[T, Callable[[T], T]]: + name=None, + is_input=False, + is_interface=False, + description=None, + directives=(), + extend=False, +): """Annotates a class as a GraphQL type. Example usage: @@ -261,38 +225,34 @@ def wrap(cls): @overload -@__dataclass_transform__( - order_default=True, kw_only_default=True, field_descriptors=(field, StrawberryField) -) +@__dataclass_transform__(order_default=True, field_descriptors=(field, StrawberryField)) def input( cls: T, *, - name: Optional[str] = None, - description: Optional[str] = None, - directives: Optional[Sequence[object]] = (), + name: str = None, + description: str = None, + directives: Optional[Sequence[StrawberrySchemaDirective]] = (), ) -> T: ... @overload -@__dataclass_transform__( - order_default=True, kw_only_default=True, field_descriptors=(field, StrawberryField) -) +@__dataclass_transform__(order_default=True, field_descriptors=(field, StrawberryField)) def input( *, - name: Optional[str] = None, - description: Optional[str] = None, - directives: Optional[Sequence[object]] = (), + name: str = None, + description: str = None, + directives: Optional[Sequence[StrawberrySchemaDirective]] = (), ) -> Callable[[T], T]: ... def input( - cls: Optional[T] = None, + cls=None, *, - name: Optional[str] = None, - description: Optional[str] = None, - directives: Optional[Sequence[object]] = (), + name=None, + description=None, + directives=(), ): """Annotates a class as a GraphQL Input type. Example usage: @@ -301,51 +261,18 @@ def input( >>> field_abc: str = "ABC" """ - return type( # type: ignore # not sure why mypy complains here - cls, - name=name, - description=description, - directives=directives, - is_input=True, + return type( + cls, name=name, description=description, directives=directives, is_input=True ) -@overload -@__dataclass_transform__( - order_default=True, kw_only_default=True, field_descriptors=(field, StrawberryField) -) -def interface( - cls: T, - *, - name: Optional[str] = None, - description: Optional[str] = None, - directives: Optional[Sequence[object]] = (), -) -> T: - ... - - -@overload -@__dataclass_transform__( - order_default=True, kw_only_default=True, field_descriptors=(field, StrawberryField) -) +@__dataclass_transform__(order_default=True, field_descriptors=(field, StrawberryField)) def interface( + cls: Type = None, *, - name: Optional[str] = None, - description: Optional[str] = None, - directives: Optional[Sequence[object]] = (), -) -> Callable[[T], T]: - ... - - -@__dataclass_transform__( - order_default=True, kw_only_default=True, field_descriptors=(field, StrawberryField) -) -def interface( - cls: Optional[T] = None, - *, - name: Optional[str] = None, - description: Optional[str] = None, - directives: Optional[Sequence[object]] = (), + name: str = None, + description: str = None, + directives: Optional[Sequence[StrawberrySchemaDirective]] = (), ): """Annotates a class as a GraphQL Interface. Example usage: @@ -354,7 +281,7 @@ def interface( >>> field_abc: str """ - return type( # type: ignore # not sure why mypy complains here + return type( cls, name=name, description=description, @@ -363,25 +290,9 @@ def interface( ) -def asdict(obj: object) -> Dict[str, object]: - """Convert a strawberry object into a dictionary. - This wraps the dataclasses.asdict function to strawberry. - - Example usage: - >>> @strawberry.type - >>> class User: - >>> name: str - >>> age: int - >>> # should be {"name": "Lorem", "age": 25} - >>> user_dict = strawberry.asdict(User(name="Lorem", age=25)) - """ - return dataclasses.asdict(obj) - - __all__ = [ "TypeDefinition", "input", "interface", "type", - "asdict", ] diff --git a/strawberry/printer.py b/strawberry/printer.py new file mode 100644 index 0000000000..7692022dd3 --- /dev/null +++ b/strawberry/printer.py @@ -0,0 +1,145 @@ +from itertools import chain +from typing import Dict, Optional, cast + +from graphql.type import is_object_type, is_specified_directive +from graphql.utilities.print_schema import ( + is_defined_type, + print_args, + print_block, + print_deprecated, + print_description, + print_directive, + print_implemented_interfaces, + print_schema_definition, + print_type as original_print_type, +) + +from strawberry.field import StrawberryField +from strawberry.schema_directive import Location, StrawberrySchemaDirective +from strawberry.types.types import TypeDefinition + +from .schema import BaseSchema + + +def print_schema_directive_params(params: Dict) -> str: + if not params: + return "" + + return "(" + ", ".join(f'{name}: "{value}"' for name, value in params.items()) + ")" + + +def print_schema_directive( + directive: StrawberrySchemaDirective, schema: BaseSchema +) -> str: + params = directive.instance.__dict__ if directive.instance else {} + + directive_name = schema.config.name_converter.from_directive(directive) + + return f" @{directive_name}{print_schema_directive_params(params)}" + + +def print_field_directives(field: Optional[StrawberryField], schema: BaseSchema) -> str: + if not field: + return "" + + directives = ( + directive + for directive in field.directives + if any( + location in [Location.FIELD_DEFINITION, Location.INPUT_FIELD_DEFINITION] + for location in directive.locations + ) + ) + + return "".join( + (print_schema_directive(directive, schema=schema) for directive in directives) + ) + + +def print_fields(type_, schema: BaseSchema) -> str: + strawberry_type = cast(TypeDefinition, schema.get_type_by_name(type_.name)) + + fields = [] + + for i, (name, field) in enumerate(type_.fields.items()): + python_name = field.extensions and field.extensions.get("python_name") + + strawberry_field = ( + strawberry_type.get_field(python_name) + if strawberry_type and python_name + else None + ) + + fields.append( + print_description(field, " ", not i) + + f" {name}" + + print_args(field.args, " ") + + f": {field.type}" + + print_field_directives(strawberry_field, schema=schema) + + print_deprecated(field.deprecation_reason) + ) + + return print_block(fields) + + +def print_extends(type_, schema: BaseSchema): + strawberry_type = cast(TypeDefinition, schema.get_type_by_name(type_.name)) + + if strawberry_type and strawberry_type.extend: + return "extend " + + return "" + + +def print_type_directives(type_, schema: BaseSchema) -> str: + strawberry_type = cast(TypeDefinition, schema.get_type_by_name(type_.name)) + + if not strawberry_type: + return "" + + directives = ( + directive + for directive in strawberry_type.directives or [] + if any(location in [Location.OBJECT] for location in directive.locations) + ) + + return "".join( + (print_schema_directive(directive, schema=schema) for directive in directives) + ) + + +def _print_object(type_, schema: BaseSchema) -> str: + return ( + print_description(type_) + + print_extends(type_, schema) + + f"type {type_.name}" + + print_implemented_interfaces(type_) + + print_type_directives(type_, schema) + + print_fields(type_, schema) + ) + + +def _print_type(field, schema: BaseSchema) -> str: + if is_object_type(field): + return _print_object(field, schema) + + return original_print_type(field) + + +def print_schema(schema: BaseSchema) -> str: + graphql_core_schema = schema._schema # type: ignore + + directives = filter( + lambda n: not is_specified_directive(n), graphql_core_schema.directives + ) + type_map = graphql_core_schema.type_map + + types = filter(is_defined_type, map(type_map.get, sorted(type_map))) # type: ignore + + return "\n\n".join( + chain( + filter(None, [print_schema_definition(graphql_core_schema)]), + (print_directive(directive) for directive in directives), + (_print_type(type_, schema) for type_ in types), # type: ignore + ) + ) diff --git a/strawberry/printer/__init__.py b/strawberry/printer/__init__.py deleted file mode 100644 index abb7d308d2..0000000000 --- a/strawberry/printer/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .printer import print_schema - -__all__ = ["print_schema"] diff --git a/strawberry/printer/ast_from_value.py b/strawberry/printer/ast_from_value.py deleted file mode 100644 index 2d79a1c29f..0000000000 --- a/strawberry/printer/ast_from_value.py +++ /dev/null @@ -1,144 +0,0 @@ -import dataclasses -import re -from math import isfinite -from typing import Any, Mapping, Optional, cast - -from graphql.language import ( - BooleanValueNode, - EnumValueNode, - FloatValueNode, - IntValueNode, - ListValueNode, - NameNode, - NullValueNode, - ObjectFieldNode, - ObjectValueNode, - StringValueNode, - ValueNode, -) -from graphql.pyutils import Undefined, inspect, is_iterable -from graphql.type import ( - GraphQLID, - GraphQLInputObjectType, - GraphQLInputType, - GraphQLList, - GraphQLNonNull, - is_enum_type, - is_input_object_type, - is_leaf_type, - is_list_type, - is_non_null_type, -) - -__all__ = ["ast_from_value"] - -_re_integer_string = re.compile("^-?(?:0|[1-9][0-9]*)$") - - -def ast_from_leaf_type( - serialized: object, type_: Optional[GraphQLInputType] -) -> ValueNode: - # Others serialize based on their corresponding Python scalar types. - if isinstance(serialized, bool): - return BooleanValueNode(value=serialized) - - # Python ints and floats correspond nicely to Int and Float values. - if isinstance(serialized, int): - return IntValueNode(value=str(serialized)) - if isinstance(serialized, float) and isfinite(serialized): - value = str(serialized) - if value.endswith(".0"): - value = value[:-2] - return FloatValueNode(value=value) - - if isinstance(serialized, str): - # Enum types use Enum literals. - if type_ and is_enum_type(type_): - return EnumValueNode(value=serialized) - - # ID types can use Int literals. - if type_ is GraphQLID and _re_integer_string.match(serialized): - return IntValueNode(value=serialized) - - return StringValueNode(value=serialized) - - if isinstance(serialized, dict): - return ObjectValueNode( - fields=[ - ObjectFieldNode( - name=NameNode(value=key), - value=ast_from_leaf_type(value, None), - ) - for key, value in serialized.items() - ] - ) - - raise TypeError( - f"Cannot convert value to AST: {inspect(serialized)}." - ) # pragma: no cover - - -def ast_from_value(value: Any, type_: GraphQLInputType) -> Optional[ValueNode]: - # custom ast_from_value that allows to also serialize custom scalar that aren't - # basic types, namely JSON scalar types - - if is_non_null_type(type_): - type_ = cast(GraphQLNonNull, type_) - ast_value = ast_from_value(value, type_.of_type) - if isinstance(ast_value, NullValueNode): - return None - return ast_value - - # only explicit None, not Undefined or NaN - if value is None: - return NullValueNode() - - # undefined - if value is Undefined: - return None - - # Convert Python list to GraphQL list. If the GraphQLType is a list, but the value - # is not a list, convert the value using the list's item type. - if is_list_type(type_): - type_ = cast(GraphQLList, type_) - item_type = type_.of_type - if is_iterable(value): - maybe_value_nodes = (ast_from_value(item, item_type) for item in value) - value_nodes = tuple(node for node in maybe_value_nodes if node) - return ListValueNode(values=value_nodes) - return ast_from_value(value, item_type) - - # Populate the fields of the input object by creating ASTs from each value in the - # Python dict according to the fields in the input type. - if is_input_object_type(type_): - # TODO: is this the right place? - if hasattr(value, "_type_definition"): - value = dataclasses.asdict(value) - - if value is None or not isinstance(value, Mapping): - return None - - type_ = cast(GraphQLInputObjectType, type_) - field_items = ( - (field_name, ast_from_value(value[field_name], field.type)) - for field_name, field in type_.fields.items() - if field_name in value - ) - field_nodes = tuple( - ObjectFieldNode(name=NameNode(value=field_name), value=field_value) - for field_name, field_value in field_items - if field_value - ) - return ObjectValueNode(fields=field_nodes) - - if is_leaf_type(type_): - # Since value is an internally represented value, it must be serialized to an - # externally represented value before converting into an AST. - serialized = type_.serialize(value) # type: ignore - if serialized is None or serialized is Undefined: - return None # pragma: no cover - - return ast_from_leaf_type(serialized, type_) - - # Not reachable. All possible input types have been considered. - raise TypeError(f"Unexpected input type: {inspect(type_)}.") # pragma: no cover diff --git a/strawberry/printer/printer.py b/strawberry/printer/printer.py deleted file mode 100644 index 50a550869a..0000000000 --- a/strawberry/printer/printer.py +++ /dev/null @@ -1,578 +0,0 @@ -from __future__ import annotations - -import dataclasses -from itertools import chain -from typing import ( - TYPE_CHECKING, - Any, - Dict, - List, - Optional, - Set, - Tuple, - TypeVar, - Union, - cast, - overload, -) - -from graphql import ( - GraphQLArgument, - GraphQLEnumType, - GraphQLEnumValue, - GraphQLScalarType, - GraphQLUnionType, - is_union_type, -) -from graphql.language.printer import print_ast -from graphql.type import ( - is_enum_type, - is_input_type, - is_interface_type, - is_object_type, - is_scalar_type, - is_specified_directive, -) -from graphql.type.directives import GraphQLDirective -from graphql.utilities.print_schema import ( - is_defined_type, - print_block, - print_deprecated, - print_description, - print_implemented_interfaces, - print_specified_by_url, -) -from graphql.utilities.print_schema import print_type as original_print_type - -from strawberry.custom_scalar import ScalarWrapper -from strawberry.enum import EnumDefinition -from strawberry.field import StrawberryField -from strawberry.schema.schema_converter import GraphQLCoreConverter -from strawberry.schema_directive import Location, StrawberrySchemaDirective -from strawberry.type import StrawberryContainer -from strawberry.unset import UNSET - -from .ast_from_value import ast_from_value - -if TYPE_CHECKING: - from strawberry.schema import BaseSchema - - -_T = TypeVar("_T") - - -@dataclasses.dataclass -class PrintExtras: - directives: Set[str] = dataclasses.field(default_factory=set) - types: Set[type] = dataclasses.field(default_factory=set) - - -@overload -def _serialize_dataclasses(value: Dict[_T, object]) -> Dict[_T, object]: - ... - - -@overload -def _serialize_dataclasses(value: Union[List[object], Tuple[object]]) -> List[object]: - ... - - -@overload -def _serialize_dataclasses(value: object) -> object: - ... - - -def _serialize_dataclasses(value): - if dataclasses.is_dataclass(value): - return dataclasses.asdict(value) - if isinstance(value, (list, tuple)): - return [_serialize_dataclasses(v) for v in value] - if isinstance(value, dict): - return {k: _serialize_dataclasses(v) for k, v in value.items()} - - return value - - -def print_schema_directive_params( - directive: GraphQLDirective, values: Dict[str, Any] -) -> str: - params = [] - for name, arg in directive.args.items(): - value = values.get(name, arg.default_value) - if value is UNSET: - value = None - else: - ast = ast_from_value(_serialize_dataclasses(value), arg.type) - value = ast and f"{name}: {print_ast(ast)}" - - if value: - params.append(value) - - if not params: - return "" - - return "(" + ", ".join(params) + ")" - - -def print_schema_directive( - directive: Any, schema: BaseSchema, *, extras: PrintExtras -) -> str: - strawberry_directive = cast( - StrawberrySchemaDirective, directive.__class__.__strawberry_directive__ - ) - schema_converter = schema.schema_converter - gql_directive = schema_converter.from_schema_directive(directive.__class__) - params = print_schema_directive_params( - gql_directive, - { - schema.config.name_converter.get_graphql_name(f): getattr( - directive, f.python_name or f.name, UNSET - ) - for f in strawberry_directive.fields - }, - ) - - printed_directive = print_directive(gql_directive, schema=schema) - - if printed_directive is not None: - extras.directives.add(printed_directive) - - for field in strawberry_directive.fields: - f_type = field.type - - while isinstance(f_type, StrawberryContainer): - f_type = f_type.of_type - - if hasattr(f_type, "_type_definition"): - extras.types.add(cast(type, f_type)) - - if hasattr(f_type, "_scalar_definition"): - extras.types.add(cast(type, f_type)) - - if isinstance(f_type, EnumDefinition): - extras.types.add(cast(type, f_type)) - - return f" @{gql_directive.name}{params}" - - -def print_field_directives( - field: Optional[StrawberryField], schema: BaseSchema, *, extras: PrintExtras -) -> str: - if not field: - return "" - - directives = ( - directive - for directive in field.directives - if any( - location in [Location.FIELD_DEFINITION, Location.INPUT_FIELD_DEFINITION] - for location in directive.__strawberry_directive__.locations # type: ignore - ) - ) - - return "".join( - print_schema_directive(directive, schema=schema, extras=extras) - for directive in directives - ) - - -def print_argument_directives( - argument: GraphQLArgument, *, schema: BaseSchema, extras: PrintExtras -) -> str: - strawberry_type = argument.extensions.get("strawberry-definition") - directives = strawberry_type.directives if strawberry_type else [] - - return "".join( - print_schema_directive(directive, schema=schema, extras=extras) - for directive in directives - ) - - -def print_args( - args: Dict[str, GraphQLArgument], - indentation: str = "", - *, - schema: BaseSchema, - extras: PrintExtras, -) -> str: - if not args: - return "" - - # If every arg does not have a description, print them on one line. - if not any(arg.description for arg in args.values()): - return ( - "(" - + ", ".join( - ( - f"{print_input_value(name, arg)}" - f"{print_argument_directives(arg, schema=schema, extras=extras)}" - ) - for name, arg in args.items() - ) - + ")" - ) - - return ( - "(\n" - + "\n".join( - print_description(arg, f" {indentation}", not i) - + f" {indentation}" - + print_input_value(name, arg) - + print_argument_directives(arg, schema=schema, extras=extras) - for i, (name, arg) in enumerate(args.items()) - ) - + f"\n{indentation})" - ) - - -def print_fields(type_, schema: BaseSchema, *, extras: PrintExtras) -> str: - fields = [] - - for i, (name, field) in enumerate(type_.fields.items()): - strawberry_field = field.extensions and field.extensions.get( - GraphQLCoreConverter.DEFINITION_BACKREF - ) - - args = ( - print_args(field.args, " ", schema=schema, extras=extras) - if hasattr(field, "args") - else "" - ) - - fields.append( - print_description(field, " ", not i) - + f" {name}" - + args - + f": {field.type}" - + print_field_directives(strawberry_field, schema=schema, extras=extras) - + print_deprecated(field.deprecation_reason) - ) - - return print_block(fields) - - -def print_scalar( - type_: GraphQLScalarType, *, schema: BaseSchema, extras: PrintExtras -) -> str: - # TODO: refactor this - strawberry_type = type_.extensions.get("strawberry-definition") - directives = strawberry_type.directives if strawberry_type else [] - - printed_directives = "".join( - print_schema_directive(directive, schema=schema, extras=extras) - for directive in directives - ) - - return ( - print_description(type_) - + f"scalar {type_.name}" - + print_specified_by_url(type_) - + printed_directives - ).strip() - - -def print_enum_value( - name: str, - value: GraphQLEnumValue, - first_in_block, - *, - schema: BaseSchema, - extras: PrintExtras, -) -> str: - strawberry_type = value.extensions.get("strawberry-definition") - directives = strawberry_type.directives if strawberry_type else [] - - printed_directives = "".join( - print_schema_directive(directive, schema=schema, extras=extras) - for directive in directives - ) - - return ( - print_description(value, " ", first_in_block) - + f" {name}" - + print_deprecated(value.deprecation_reason) - + printed_directives - ) - - -def print_enum( - type_: GraphQLEnumType, *, schema: BaseSchema, extras: PrintExtras -) -> str: - strawberry_type = type_.extensions.get("strawberry-definition") - directives = strawberry_type.directives if strawberry_type else [] - - printed_directives = "".join( - print_schema_directive(directive, schema=schema, extras=extras) - for directive in directives - ) - - values = [ - print_enum_value(name, value, not i, schema=schema, extras=extras) - for i, (name, value) in enumerate(type_.values.items()) - ] - return ( - print_description(type_) - + f"enum {type_.name}" - + printed_directives - + print_block(values) - ) - - -def print_extends(type_, schema: BaseSchema): - strawberry_type = type_.extensions and type_.extensions.get( - GraphQLCoreConverter.DEFINITION_BACKREF - ) - - if strawberry_type and strawberry_type.extend: - return "extend " - - return "" - - -def print_type_directives(type_, schema: BaseSchema, *, extras: PrintExtras) -> str: - strawberry_type = type_.extensions and type_.extensions.get( - GraphQLCoreConverter.DEFINITION_BACKREF - ) - - if not strawberry_type: - return "" - - allowed_locations = ( - [Location.INPUT_OBJECT] if strawberry_type.is_input else [Location.OBJECT] - ) - - directives = ( - directive - for directive in strawberry_type.directives or [] - if any( - location in allowed_locations - for location in directive.__strawberry_directive__.locations - ) - ) - - return "".join( - print_schema_directive(directive, schema=schema, extras=extras) - for directive in directives - ) - - -def _print_object(type_, schema: BaseSchema, *, extras: PrintExtras) -> str: - return ( - print_description(type_) - + print_extends(type_, schema) - + f"type {type_.name}" - + print_implemented_interfaces(type_) - + print_type_directives(type_, schema, extras=extras) - + print_fields(type_, schema, extras=extras) - ) - - -def _print_interface(type_, schema: BaseSchema, *, extras: PrintExtras) -> str: - return ( - print_description(type_) - + print_extends(type_, schema) - + f"interface {type_.name}" - + print_implemented_interfaces(type_) - + print_type_directives(type_, schema, extras=extras) - + print_fields(type_, schema, extras=extras) - ) - - -def print_input_value(name: str, arg: GraphQLArgument) -> str: - default_ast = ast_from_value(arg.default_value, arg.type) - arg_decl = f"{name}: {arg.type}" - if default_ast: - arg_decl += f" = {print_ast(default_ast)}" - return arg_decl + print_deprecated(arg.deprecation_reason) - - -def _print_input_object(type_, schema: BaseSchema, *, extras: PrintExtras) -> str: - fields = [] - for i, (name, field) in enumerate(type_.fields.items()): - strawberry_field = field.extensions and field.extensions.get( - GraphQLCoreConverter.DEFINITION_BACKREF - ) - - fields.append( - print_description(field, " ", not i) - + " " - + print_input_value(name, field) - + print_field_directives(strawberry_field, schema=schema, extras=extras) - ) - - return ( - print_description(type_) - + f"input {type_.name}" - + print_type_directives(type_, schema, extras=extras) - + print_block(fields) - ) - - -def print_union( - type_: GraphQLUnionType, *, schema: BaseSchema, extras: PrintExtras -) -> str: - strawberry_type = type_.extensions.get("strawberry-definition") - directives = strawberry_type.directives if strawberry_type else [] - - printed_directives = "".join( - print_schema_directive(directive, schema=schema, extras=extras) - for directive in directives - ) - - types = type_.types - possible_types = " = " + " | ".join(t.name for t in types) if types else "" - return ( - print_description(type_) - + f"union {type_.name}{printed_directives}" - + possible_types - ) - - -def _print_type(type_, schema: BaseSchema, *, extras: PrintExtras) -> str: - # prevents us from trying to print a scalar as an input type - if is_scalar_type(type_): - return print_scalar(type_, schema=schema, extras=extras) - - if is_enum_type(type_): - return print_enum(type_, schema=schema, extras=extras) - - if is_object_type(type_): - return _print_object(type_, schema, extras=extras) - - if is_input_type(type_): - return _print_input_object(type_, schema, extras=extras) - - if is_interface_type(type_): - return _print_interface(type_, schema, extras=extras) - - if is_union_type(type_): - return print_union(type_, schema=schema, extras=extras) - - return original_print_type(type_) - - -def print_schema_directives(schema: BaseSchema, *, extras: PrintExtras) -> str: - directives = ( - directive - for directive in schema.schema_directives - if any( - location in [Location.SCHEMA] - for location in directive.__strawberry_directive__.locations # type: ignore - ) - ) - - return "".join( - print_schema_directive(directive, schema=schema, extras=extras) - for directive in directives - ) - - -def _all_root_names_are_common_names(schema: BaseSchema) -> bool: - query = schema.query._type_definition - mutation = schema.mutation._type_definition if schema.mutation else None - subscription = schema.subscription._type_definition if schema.subscription else None - - return ( - query.name == "Query" - and (mutation is None or mutation.name == "Mutation") - and (subscription is None or subscription.name == "Subscription") - ) - - -def print_schema_definition( - schema: BaseSchema, *, extras: PrintExtras -) -> Optional[str]: - # TODO: add support for description - - if _all_root_names_are_common_names(schema) and not schema.schema_directives: - return None - - query_type = schema.query._type_definition - operation_types = [f" query: {query_type.name}"] - - if schema.mutation: - mutation_type = schema.mutation._type_definition - operation_types.append(f" mutation: {mutation_type.name}") - - if schema.subscription: - subscription_type = schema.subscription._type_definition - operation_types.append(f" subscription: {subscription_type.name}") - - directives = print_schema_directives(schema, extras=extras) - - return f"schema{directives} {{\n" + "\n".join(operation_types) + "\n}" - - -def print_directive( - directive: GraphQLDirective, *, schema: BaseSchema -) -> Optional[str]: - strawberry_directive = directive.extensions["strawberry-definition"] - - if ( - isinstance(strawberry_directive, StrawberrySchemaDirective) - and not strawberry_directive.print_definition - ): - return None - - return ( - print_description(directive) - + f"directive @{directive.name}" - # TODO: add support for directives on arguments directives - + print_args(directive.args, schema=schema, extras=PrintExtras()) - + (" repeatable" if directive.is_repeatable else "") - + " on " - + " | ".join(location.name for location in directive.locations) - ) - - -def is_builtin_directive(directive: GraphQLDirective) -> bool: - # this allows to force print the builtin directives if there's a - # directive that was implemented using the schema_directive - - if is_specified_directive(directive): - strawberry_definition = directive.extensions.get("strawberry-definition") - - return strawberry_definition is None - - return False - - -def print_schema(schema: BaseSchema) -> str: - graphql_core_schema = schema._schema # type: ignore - extras = PrintExtras() - - directives = filter( - lambda n: not is_builtin_directive(n), graphql_core_schema.directives - ) - type_map = graphql_core_schema.type_map - types = filter(is_defined_type, map(type_map.get, sorted(type_map))) - - types_printed = [_print_type(type_, schema, extras=extras) for type_ in types] - schema_definition = print_schema_definition(schema, extras=extras) - - directives = filter( - None, [print_directive(directive, schema=schema) for directive in directives] - ) - - def _name_getter(type_: Any): - if hasattr(type_, "name"): - return type_.name - if isinstance(type_, ScalarWrapper): - return type_._scalar_definition.name - return type_.__name__ - - return "\n\n".join( - chain( - sorted(extras.directives), - filter(None, [schema_definition]), - directives, - types_printed, - ( - _print_type( - schema.schema_converter.from_type(type_), schema, extras=extras - ) - # Make sure extra types are ordered for predictive printing - for type_ in sorted(extras.types, key=_name_getter) - ), - ) - ) diff --git a/strawberry/private.py b/strawberry/private.py index 50dd06f282..e0d0bc972b 100644 --- a/strawberry/private.py +++ b/strawberry/private.py @@ -1,4 +1,5 @@ from typing import TypeVar + from typing_extensions import Annotated, get_args, get_origin @@ -9,7 +10,7 @@ class StrawberryPrivate: T = TypeVar("T") Private = Annotated[T, StrawberryPrivate()] -Private.__doc__ = """Represents a field that won't be exposed in the GraphQL schema +Private.__doc__ = """Represent a private field that won't be converted into a GraphQL field Example: diff --git a/strawberry/sanic/context.py b/strawberry/sanic/context.py index 2ff283532e..db98f946c9 100644 --- a/strawberry/sanic/context.py +++ b/strawberry/sanic/context.py @@ -1,23 +1,11 @@ -import warnings -from typing_extensions import TypedDict +from dataclasses import dataclass from sanic.request import Request -from strawberry.http.temporal_response import TemporalResponse -class StrawberrySanicContext(TypedDict): +@dataclass +class StrawberrySanicContext: request: Request - response: TemporalResponse - # see https://github.com/python/mypy/issues/13066 for the type ignore - def __getattr__(self, key: str) -> object: # type: ignore - # a warning will be raised because this is not supported anymore - # but we need to keep it for backwards compatibility - - warnings.warn( - "Accessing context attributes via the dot notation is deprecated, " - "please use context.get('key') or context['key'] instead", - DeprecationWarning, - ) - - return super().__getitem__(key) + def __getitem__(self, key): + return super().__getattribute__(key) diff --git a/strawberry/sanic/graphiql.py b/strawberry/sanic/graphiql.py index ac79b64d61..b4d9f522c2 100644 --- a/strawberry/sanic/graphiql.py +++ b/strawberry/sanic/graphiql.py @@ -1,10 +1,13 @@ -from sanic.request import Request +from os.path import abspath, dirname, join -def should_render_graphiql(graphiql: bool, request: Request) -> bool: - if not graphiql: - return False - return any( - supported_header in request.headers.get("accept", "") - for supported_header in ("text/html", "*/*") - ) +def render_graphiql_page() -> str: + dir_path = abspath(join(dirname(__file__), "..")) + graphiql_html_file = f"{dir_path}/static/graphiql.html" + + html_string = None + + with open(graphiql_html_file, "r") as f: + html_string = f.read() + + return html_string.replace("{{ SUBSCRIPTION_ENABLED }}", "false") diff --git a/strawberry/sanic/views.py b/strawberry/sanic/views.py index fa73822c71..6d83ae0a55 100644 --- a/strawberry/sanic/views.py +++ b/strawberry/sanic/views.py @@ -1,9 +1,7 @@ import json -import warnings -from typing import Any, Dict, Optional, Type, Union -from typing_extensions import Literal +from typing import Any -from sanic.exceptions import NotFound, SanicException, ServerError +from sanic.exceptions import SanicException, ServerError from sanic.request import Request from sanic.response import HTTPResponse, html from sanic.views import HTTPMethodView @@ -12,20 +10,15 @@ from strawberry.http import ( GraphQLHTTPResponse, GraphQLRequestData, - parse_query_params, parse_request_data, process_result, ) -from strawberry.http.temporal_response import TemporalResponse -from strawberry.sanic.graphiql import should_render_graphiql -from strawberry.sanic.utils import convert_request_to_files_dict -from strawberry.schema import BaseSchema -from strawberry.schema.exceptions import InvalidOperationTypeError from strawberry.types import ExecutionResult -from strawberry.types.graphql import OperationType -from strawberry.utils.graphiql import get_graphiql_html +from ..schema import BaseSchema from .context import StrawberrySanicContext +from .graphiql import render_graphiql_page +from .utils import convert_request_to_files_dict class GraphQLView(HTTPMethodView): @@ -35,7 +28,6 @@ class GraphQLView(HTTPMethodView): Args: schema: strawberry.Schema graphiql: bool, default is True - allow_queries_via_get: bool, default is True Returns: None @@ -47,162 +39,62 @@ class GraphQLView(HTTPMethodView): ) """ - def __init__( - self, - schema: BaseSchema, - graphiql: bool = True, - allow_queries_via_get: bool = True, - json_encoder: Optional[Type[json.JSONEncoder]] = None, - json_dumps_params: Optional[Dict[str, Any]] = None, - ): - self.schema = schema + def __init__(self, schema: BaseSchema, graphiql: bool = True): self.graphiql = graphiql - self.allow_queries_via_get = allow_queries_via_get - self.json_encoder = json_encoder - self.json_dumps_params = json_dumps_params - - if self.json_encoder is not None: - warnings.warn( - "json_encoder is deprecated, override encode_json instead", - DeprecationWarning, - ) - - if self.json_dumps_params is not None: - warnings.warn( - "json_dumps_params is deprecated, override encode_json instead", - DeprecationWarning, - ) - - self.json_encoder = json.JSONEncoder + self.schema = schema def get_root_value(self): return None - async def get_context( - self, request: Request, response: TemporalResponse - ) -> StrawberrySanicContext: - return {"request": request, "response": response} + async def get_context(self, request: Request) -> Any: + return StrawberrySanicContext(request) - def render_template(self, template: str) -> HTTPResponse: + def render_template(self, template=None): return html(template) - async def process_result( - self, request: Request, result: ExecutionResult - ) -> GraphQLHTTPResponse: + def process_result(self, result: ExecutionResult) -> GraphQLHTTPResponse: return process_result(result) async def get(self, request: Request) -> HTTPResponse: - if request.args: - # Sanic request.args uses urllib.parse.parse_qs - # returns a dictionary where the keys are the unique variable names - # and the values are a list of values for each variable name - # Enforcing using the first value - query_data = { - variable_name: value[0] for variable_name, value in request.args.items() - } - try: - data = parse_query_params(query_data) - except json.JSONDecodeError: - raise ServerError( - "Unable to parse request body as JSON", status_code=400 - ) - - request_data = parse_request_data(data) - - return await self.execute_request( - request=request, request_data=request_data, method="GET" - ) - - elif should_render_graphiql(self.graphiql, request): - template = get_graphiql_html(False) - return self.render_template(template=template) - - raise NotFound() - - async def get_response( - self, response_data: GraphQLHTTPResponse, context: StrawberrySanicContext - ) -> HTTPResponse: - status_code = 200 - - if "response" in context and context["response"]: - status_code = context["response"].status_code - - data = self.encode_json(response_data) - - return HTTPResponse( - data, - status=status_code, - content_type="application/json", - ) - - def encode_json(self, response_data: GraphQLHTTPResponse) -> str: - if self.json_dumps_params: - assert self.json_encoder + if not self.graphiql: + raise SanicException(status_code=404) - return json.dumps( - response_data, cls=self.json_encoder, **self.json_dumps_params - ) - - if self.json_encoder: - return json.dumps(response_data, cls=self.json_encoder) - - return json.dumps(response_data) + template = render_graphiql_page() + return self.render_template(template=template) async def post(self, request: Request) -> HTTPResponse: request_data = self.get_request_data(request) - - return await self.execute_request( - request=request, request_data=request_data, method="POST" - ) - - async def execute_request( - self, - request: Request, - request_data: GraphQLRequestData, - method: Union[Literal["GET"], Literal["POST"]], - ) -> HTTPResponse: - context = await self.get_context(request, TemporalResponse()) + context = await self.get_context(request) root_value = self.get_root_value() - allowed_operation_types = OperationType.from_http(method) - - if not self.allow_queries_via_get and method == "GET": - allowed_operation_types = allowed_operation_types - {OperationType.QUERY} - - try: - result = await self.schema.execute( - query=request_data.query, - variable_values=request_data.variables, - context_value=context, - root_value=root_value, - operation_name=request_data.operation_name, - allowed_operation_types=allowed_operation_types, - ) - except InvalidOperationTypeError as e: - raise ServerError( - e.as_http_error_reason(method=method), status_code=400 - ) from e - except MissingQueryError: - raise ServerError("No GraphQL query found in the request", status_code=400) - - response_data = await self.process_result(request, result) + result = await self.schema.execute( + query=request_data.query, + variable_values=request_data.variables, + context_value=context, + root_value=root_value, + operation_name=request_data.operation_name, + ) + response_data = self.process_result(result) - return await self.get_response(response_data, context) + return HTTPResponse( + json.dumps(response_data), status=200, content_type="application/json" + ) def get_request_data(self, request: Request) -> GraphQLRequestData: try: - data = self.parse_request(request) + data = self.parse_body(request) except json.JSONDecodeError: raise ServerError("Unable to parse request body as JSON", status_code=400) - return parse_request_data(data) + try: + request_data = parse_request_data(data) + except MissingQueryError: + raise ServerError("No GraphQL query found in the request", status_code=400) - def parse_request(self, request: Request) -> Dict[str, Any]: - content_type = request.content_type or "" + return request_data - if "application/json" in content_type: - return json.loads(request.body) - elif content_type.startswith("multipart/form-data"): + def parse_body(self, request: Request) -> dict: + if request.content_type.startswith("multipart/form-data"): files = convert_request_to_files_dict(request) operations = json.loads(request.form.get("operations", "{}")) files_map = json.loads(request.form.get("map", "{}")) @@ -212,5 +104,4 @@ def parse_request(self, request: Request) -> Dict[str, Any]: raise SanicException( status_code=400, message="File(s) missing in form data" ) - - raise ServerError("Unsupported Media Type", status_code=415) + return request.json diff --git a/strawberry/scalars.py b/strawberry/scalars.py index a32b935f96..564b605b59 100644 --- a/strawberry/scalars.py +++ b/strawberry/scalars.py @@ -1,51 +1,9 @@ -import base64 from typing import Any, Dict, NewType, Union -from .custom_scalar import ScalarDefinition, ScalarWrapper, scalar +from .custom_scalar import ScalarDefinition, ScalarWrapper -ID = NewType("ID", str) - -JSON = scalar( - NewType("JSON", object), # mypy doesn't like `NewType("name", Any)` - description=( - "The `JSON` scalar type represents JSON values as specified by " - "[ECMA-404]" - "(http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf)." - ), - specified_by_url=( - "http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf" - ), - serialize=lambda v: v, - parse_value=lambda v: v, -) - -Base16 = scalar( - NewType("Base16", bytes), - description="Represents binary data as Base16-encoded (hexadecimal) strings.", - specified_by_url="https://datatracker.ietf.org/doc/html/rfc4648.html#section-8", - serialize=lambda v: base64.b16encode(v).decode("utf-8"), - parse_value=lambda v: base64.b16decode(v.encode("utf-8"), casefold=True), -) -Base32 = scalar( - NewType("Base32", bytes), - description=( - "Represents binary data as Base32-encoded strings, using the standard alphabet." - ), - specified_by_url=("https://datatracker.ietf.org/doc/html/rfc4648.html#section-6"), - serialize=lambda v: base64.b32encode(v).decode("utf-8"), - parse_value=lambda v: base64.b32decode(v.encode("utf-8"), casefold=True), -) - -Base64 = scalar( - NewType("Base64", bytes), - description=( - "Represents binary data as Base64-encoded strings, using the standard alphabet." - ), - specified_by_url="https://datatracker.ietf.org/doc/html/rfc4648.html#section-4", - serialize=lambda v: base64.b64encode(v).decode("utf-8"), - parse_value=lambda v: base64.b64decode(v.encode("utf-8")), -) +ID = NewType("ID", str) def is_scalar( diff --git a/strawberry/schema/__init__.py b/strawberry/schema/__init__.py index 5cf633ac21..be80b4207c 100644 --- a/strawberry/schema/__init__.py +++ b/strawberry/schema/__init__.py @@ -1,4 +1,5 @@ -from .base import BaseSchema -from .schema import Schema +from .base import BaseSchema as BaseSchema +from .schema import Schema as Schema + __all__ = ["BaseSchema", "Schema"] diff --git a/strawberry/schema/base.py b/strawberry/schema/base.py index 493e2d6e80..638764794a 100644 --- a/strawberry/schema/base.py +++ b/strawberry/schema/base.py @@ -1,6 +1,7 @@ from abc import abstractmethod from functools import lru_cache -from typing import Any, Dict, Iterable, List, Optional, Type, Union +from typing import Any, Dict, List, Optional, Union + from typing_extensions import Protocol from graphql import GraphQLError @@ -8,9 +9,7 @@ from strawberry.custom_scalar import ScalarDefinition from strawberry.directive import StrawberryDirective from strawberry.enum import EnumDefinition -from strawberry.schema.schema_converter import GraphQLCoreConverter from strawberry.types import ExecutionContext, ExecutionResult -from strawberry.types.graphql import OperationType from strawberry.types.types import TypeDefinition from strawberry.union import StrawberryUnion from strawberry.utils.logging import StrawberryLogger @@ -20,33 +19,26 @@ class BaseSchema(Protocol): config: StrawberryConfig - schema_converter: GraphQLCoreConverter - query: Type - mutation: Optional[Type] - subscription: Optional[Type] - schema_directives: Iterable[object] @abstractmethod async def execute( self, - query: Optional[str], + query: str, variable_values: Optional[Dict[str, Any]] = None, context_value: Optional[Any] = None, root_value: Optional[Any] = None, operation_name: Optional[str] = None, - allowed_operation_types: Optional[Iterable[OperationType]] = None, ) -> ExecutionResult: raise NotImplementedError @abstractmethod def execute_sync( self, - query: Optional[str], + query: str, variable_values: Optional[Dict[str, Any]] = None, context_value: Optional[Any] = None, root_value: Optional[Any] = None, operation_name: Optional[str] = None, - allowed_operation_types: Optional[Iterable[OperationType]] = None, ) -> ExecutionResult: raise NotImplementedError diff --git a/strawberry/schema/compat.py b/strawberry/schema/compat.py index bf21be79d4..a72c8fbfe3 100644 --- a/strawberry/schema/compat.py +++ b/strawberry/schema/compat.py @@ -7,6 +7,7 @@ from strawberry.type import StrawberryType from strawberry.types.types import TypeDefinition + # TypeGuard is only available in typing_extensions => 3.10, we don't want # to force updates to the typing_extensions package so we only use it when # TYPE_CHECKING is enabled. @@ -35,25 +36,24 @@ def is_scalar( type_: Union[StrawberryType, type], scalar_registry: Dict[object, Union[ScalarWrapper, ScalarDefinition]], ) -> TypeGuard[type]: + # isinstance(type_, StrawberryScalar) # noqa: E800 return is_strawberry_scalar(type_, scalar_registry) def is_object_type(type_: Union[StrawberryType, type]) -> TypeGuard[type]: + # isinstance(type_, StrawberryObjectType) # noqa: E800 return hasattr(type_, "_type_definition") def is_enum(type_: Union[StrawberryType, type]) -> TypeGuard[type]: + # isinstance(type_, StrawberryEnumType) # noqa: E800 return hasattr(type_, "_enum_definition") -def is_schema_directive(type_: Union[StrawberryType, type]) -> TypeGuard[type]: - return hasattr(type_, "__strawberry_directive__") - - def is_generic(type_: Union[StrawberryType, type]) -> bool: if hasattr(type_, "_type_definition"): - type_definition: TypeDefinition = type_._type_definition + type_definition: TypeDefinition = type_._type_definition # type: ignore return type_definition.is_generic if isinstance(type_, StrawberryType): diff --git a/strawberry/schema/config.py b/strawberry/schema/config.py index c23b557e58..43b3d87c10 100644 --- a/strawberry/schema/config.py +++ b/strawberry/schema/config.py @@ -1,16 +1,14 @@ from __future__ import annotations from dataclasses import InitVar, dataclass, field -from typing import Any, Callable from .name_converter import NameConverter @dataclass class StrawberryConfig: - auto_camel_case: InitVar[bool] = None # pyright: reportGeneralTypeIssues=false + auto_camel_case: InitVar[bool] = None # type: ignore name_converter: NameConverter = field(default_factory=NameConverter) - default_resolver: Callable[[Any, str], object] = getattr def __post_init__( self, diff --git a/strawberry/schema/exceptions.py b/strawberry/schema/exceptions.py deleted file mode 100644 index 85f21df459..0000000000 --- a/strawberry/schema/exceptions.py +++ /dev/null @@ -1,15 +0,0 @@ -from strawberry.types.graphql import OperationType - - -class InvalidOperationTypeError(Exception): - def __init__(self, operation_type: OperationType): - self.operation_type = operation_type - - def as_http_error_reason(self, method: str) -> str: - operation_type = { - OperationType.QUERY: "queries", - OperationType.MUTATION: "mutations", - OperationType.SUBSCRIPTION: "subscriptions", - }[self.operation_type] - - return f"{operation_type} are not allowed when using {method}" diff --git a/strawberry/schema/execute.py b/strawberry/schema/execute.py index f27dfcc87f..f87ce88536 100644 --- a/strawberry/schema/execute.py +++ b/strawberry/schema/execute.py @@ -1,32 +1,21 @@ from asyncio import ensure_future from inspect import isawaitable -from typing import ( - Awaitable, - Callable, - Iterable, - List, - Optional, - Sequence, - Tuple, - Type, - Union, - cast, +from typing import Awaitable, List, Optional, Sequence, Tuple, Type, Union, cast + +from graphql import ( + ExecutionContext as GraphQLExecutionContext, + ExecutionResult as GraphQLExecutionResult, + GraphQLError, + GraphQLSchema, + execute as original_execute, + parse, ) - -from graphql import ExecutionContext as GraphQLExecutionContext -from graphql import ExecutionResult as GraphQLExecutionResult -from graphql import GraphQLError, GraphQLSchema, parse -from graphql import execute as original_execute from graphql.language import DocumentNode from graphql.validation import ASTValidationRule, validate -from strawberry.exceptions import MissingQueryError from strawberry.extensions import Extension from strawberry.extensions.runner import ExtensionsRunner from strawberry.types import ExecutionContext, ExecutionResult -from strawberry.types.graphql import OperationType - -from .exceptions import InvalidOperationTypeError def parse_document(query: str) -> DocumentNode: @@ -59,12 +48,10 @@ def _run_validation(execution_context: ExecutionContext) -> None: async def execute( schema: GraphQLSchema, - *, - allowed_operation_types: Iterable[OperationType], + query: str, extensions: Sequence[Union[Type[Extension], Extension]], execution_context: ExecutionContext, execution_context_class: Optional[Type[GraphQLExecutionContext]] = None, - process_errors: Callable[[List[GraphQLError], Optional[ExecutionContext]], None], ) -> ExecutionResult: extensions_runner = ExtensionsRunner( execution_context=execution_context, @@ -74,19 +61,13 @@ async def execute( async with extensions_runner.request(): # Note: In graphql-core the schema would be validated here but in # Strawberry we are validating it at initialisation time instead - if not execution_context.query: - raise MissingQueryError() async with extensions_runner.parsing(): try: if not execution_context.graphql_document: - execution_context.graphql_document = parse_document( - execution_context.query - ) - + execution_context.graphql_document = parse_document(query) except GraphQLError as error: execution_context.errors = [error] - process_errors([error], execution_context) return ExecutionResult( data=None, errors=[error], @@ -97,21 +78,15 @@ async def execute( error = GraphQLError(str(error), original_error=error) execution_context.errors = [error] - process_errors([error], execution_context) - return ExecutionResult( data=None, errors=[error], extensions=await extensions_runner.get_extensions_results(), ) - if execution_context.operation_type not in allowed_operation_types: - raise InvalidOperationTypeError(execution_context.operation_type) - async with extensions_runner.validation(): _run_validation(execution_context) if execution_context.errors: - process_errors(execution_context.errors, execution_context) return ExecutionResult(data=None, errors=execution_context.errors) async with extensions_runner.executing(): @@ -137,12 +112,6 @@ async def execute( if result.errors: execution_context.errors = result.errors - # Run the `Schema.process_errors` function here before - # extensions have a chance to modify them (see the MaskErrors - # extension). That way we can log the original errors but - # only return a sanitised version to the client. - process_errors(result.errors, execution_context) - return ExecutionResult( data=execution_context.result.data, errors=execution_context.result.errors, @@ -152,12 +121,10 @@ async def execute( def execute_sync( schema: GraphQLSchema, - *, - allowed_operation_types: Iterable[OperationType], + query: str, extensions: Sequence[Union[Type[Extension], Extension]], execution_context: ExecutionContext, execution_context_class: Optional[Type[GraphQLExecutionContext]] = None, - process_errors: Callable[[List[GraphQLError], Optional[ExecutionContext]], None], ) -> ExecutionResult: extensions_runner = ExtensionsRunner( execution_context=execution_context, @@ -167,19 +134,13 @@ def execute_sync( with extensions_runner.request(): # Note: In graphql-core the schema would be validated here but in # Strawberry we are validating it at initialisation time instead - if not execution_context.query: - raise MissingQueryError() with extensions_runner.parsing(): try: if not execution_context.graphql_document: - execution_context.graphql_document = parse_document( - execution_context.query - ) - + execution_context.graphql_document = parse_document(query) except GraphQLError as error: execution_context.errors = [error] - process_errors([error], execution_context) return ExecutionResult( data=None, errors=[error], @@ -190,20 +151,15 @@ def execute_sync( error = GraphQLError(str(error), original_error=error) execution_context.errors = [error] - process_errors([error], execution_context) return ExecutionResult( data=None, errors=[error], extensions=extensions_runner.get_extensions_results_sync(), ) - if execution_context.operation_type not in allowed_operation_types: - raise InvalidOperationTypeError(execution_context.operation_type) - with extensions_runner.validation(): _run_validation(execution_context) if execution_context.errors: - process_errors(execution_context.errors, execution_context) return ExecutionResult(data=None, errors=execution_context.errors) with extensions_runner.executing(): @@ -233,12 +189,6 @@ def execute_sync( if result.errors: execution_context.errors = result.errors - # Run the `Schema.process_errors` function here before - # extensions have a chance to modify them (see the MaskErrors - # extension). That way we can log the original errors but - # only return a sanitised version to the client. - process_errors(result.errors, execution_context) - return ExecutionResult( data=execution_context.result.data, errors=execution_context.result.errors, diff --git a/strawberry/schema/name_converter.py b/strawberry/schema/name_converter.py index 8af7f81ce5..23e57a8c52 100644 --- a/strawberry/schema/name_converter.py +++ b/strawberry/schema/name_converter.py @@ -1,9 +1,9 @@ from __future__ import annotations -from typing import TYPE_CHECKING, List, Optional, Union, cast +from typing import TYPE_CHECKING, List, Optional, Union + from typing_extensions import Protocol -from strawberry.annotation import StrawberryAnnotation from strawberry.custom_scalar import ScalarDefinition from strawberry.directive import StrawberryDirective from strawberry.enum import EnumDefinition @@ -14,6 +14,7 @@ from strawberry.union import StrawberryUnion from strawberry.utils.str_converters import capitalize_first, to_camel_case + if TYPE_CHECKING: from strawberry.arguments import StrawberryArgument from strawberry.field import StrawberryField @@ -93,11 +94,8 @@ def from_union(self, union: StrawberryUnion) -> str: name = "" for type_ in union.types: - if isinstance(type_, LazyType): - type_ = cast(StrawberryType, type_.resolve_type()) - assert hasattr(type_, "_type_definition") - name += self.from_type(type_._type_definition) + name += self.from_type(type_._type_definition) # type: ignore return name @@ -117,9 +115,6 @@ def from_generic( def get_from_type(self, type_: Union[StrawberryType, type]) -> str: from strawberry.union import StrawberryUnion - # TODO: maybe we should move parse_annotated somewhere else? - type_ = StrawberryAnnotation.parse_annotated(type_) # type: ignore - if isinstance(type_, LazyType): name = type_.type_name elif isinstance(type_, EnumDefinition): @@ -134,11 +129,11 @@ def get_from_type(self, type_: Union[StrawberryType, type]) -> str: elif isinstance(type_, StrawberryOptional): name = self.get_from_type(type_.of_type) + "Optional" elif hasattr(type_, "_scalar_definition"): - strawberry_type = type_._scalar_definition + strawberry_type = type_._scalar_definition # type: ignore name = strawberry_type.name elif hasattr(type_, "_type_definition"): - strawberry_type = type_._type_definition + strawberry_type = type_._type_definition # type: ignore if strawberry_type.is_generic: types = type_.__args__ # type: ignore diff --git a/strawberry/schema/schema.py b/strawberry/schema/schema.py index 3d056875b1..b059a0ad19 100644 --- a/strawberry/schema/schema.py +++ b/strawberry/schema/schema.py @@ -1,10 +1,8 @@ from functools import lru_cache -from typing import Any, Dict, Iterable, List, Optional, Type, Union, cast +from typing import Any, Dict, Optional, Sequence, Type, Union -from graphql import ExecutionContext as GraphQLExecutionContext from graphql import ( - GraphQLNamedType, - GraphQLNonNull, + ExecutionContext as GraphQLExecutionContext, GraphQLSchema, get_introspection_query, parse, @@ -13,7 +11,6 @@ from graphql.subscription import subscribe from graphql.type.directives import specified_directives -from strawberry.annotation import StrawberryAnnotation from strawberry.custom_scalar import ScalarDefinition, ScalarWrapper from strawberry.directive import StrawberryDirective from strawberry.enum import EnumDefinition @@ -22,65 +19,46 @@ DirectivesExtension, DirectivesExtensionSync, ) -from strawberry.field import StrawberryField from strawberry.schema.schema_converter import GraphQLCoreConverter from strawberry.schema.types.scalar import DEFAULT_SCALAR_REGISTRY from strawberry.types import ExecutionContext, ExecutionResult -from strawberry.types.graphql import OperationType from strawberry.types.types import TypeDefinition from strawberry.union import StrawberryUnion from ..printer import print_schema -from . import compat from .base import BaseSchema from .config import StrawberryConfig from .execute import execute, execute_sync -DEFAULT_ALLOWED_OPERATION_TYPES = { - OperationType.QUERY, - OperationType.MUTATION, - OperationType.SUBSCRIPTION, -} - class Schema(BaseSchema): def __init__( self, - # TODO: can we make sure we only allow to pass - # something that has been decorated? + # TODO: can we make sure we only allow to pass something that has been decorated? query: Type, mutation: Optional[Type] = None, subscription: Optional[Type] = None, - directives: Iterable[StrawberryDirective] = (), + directives: Sequence[StrawberryDirective] = (), types=(), - extensions: Iterable[Union[Type[Extension], Extension]] = (), + extensions: Sequence[Union[Type[Extension], Extension]] = (), execution_context_class: Optional[Type[GraphQLExecutionContext]] = None, config: Optional[StrawberryConfig] = None, scalar_overrides: Optional[ - Dict[object, Union[Type, ScalarWrapper, ScalarDefinition]] + Dict[object, Union[ScalarWrapper, ScalarDefinition]] ] = None, - schema_directives: Iterable[object] = (), ): - self.query = query - self.mutation = mutation - self.subscription = subscription - self.extensions = extensions self.execution_context_class = execution_context_class self.config = config or StrawberryConfig() - SCALAR_OVERRIDES_DICT_TYPE = Dict[ - object, Union[ScalarWrapper, ScalarDefinition] - ] - - scalar_registry: SCALAR_OVERRIDES_DICT_TYPE = {**DEFAULT_SCALAR_REGISTRY} + scalar_registry: Dict[object, Union[ScalarWrapper, ScalarDefinition]] = { + **DEFAULT_SCALAR_REGISTRY + } if scalar_overrides: - # TODO: check that the overrides are valid - scalar_registry.update(cast(SCALAR_OVERRIDES_DICT_TYPE, scalar_overrides)) + scalar_registry.update(scalar_overrides) self.schema_converter = GraphQLCoreConverter(self.config, scalar_registry) self.directives = directives - self.schema_directives = schema_directives query_type = self.schema_converter.from_object(query._type_definition) mutation_type = ( @@ -100,44 +78,16 @@ def __init__( graphql_types = [] for type_ in types: - if compat.is_schema_directive(type_): - graphql_directives.append( - self.schema_converter.from_schema_directive(type_) - ) - else: - if hasattr(type_, "_type_definition"): - if type_._type_definition.is_generic: - type_ = StrawberryAnnotation(type_).resolve() - graphql_type = self.schema_converter.from_maybe_optional(type_) - if isinstance(graphql_type, GraphQLNonNull): - graphql_type = graphql_type.of_type - if not isinstance(graphql_type, GraphQLNamedType): - raise TypeError(f"{graphql_type} is not a named GraphQL Type") - graphql_types.append(graphql_type) - - try: - self._schema = GraphQLSchema( - query=query_type, - mutation=mutation_type, - subscription=subscription_type if subscription else None, - directives=specified_directives + tuple(graphql_directives), - types=graphql_types, - extensions={ - GraphQLCoreConverter.DEFINITION_BACKREF: self, - }, - ) - - except TypeError as error: - # GraphQL core throws a TypeError if there's any exception raised - # during the schema creation, so we check if the cause was a - # StrawberryError and raise it instead if that's the case. - - from strawberry.exceptions import StrawberryException - - if isinstance(error.__cause__, StrawberryException): - raise error.__cause__ from None - - raise + graphql_type = self.schema_converter.from_object(type_._type_definition) + graphql_types.append(graphql_type) + + self._schema = GraphQLSchema( + query=query_type, + mutation=mutation_type, + subscription=subscription_type if subscription else None, + directives=specified_directives + graphql_directives, + types=graphql_types, + ) # attach our schema to the GraphQL schema instance self._schema._strawberry_schema = self # type: ignore @@ -149,47 +99,18 @@ def __init__( formatted_errors = "\n\n".join(f"โŒ {error.message}" for error in errors) raise ValueError(f"Invalid Schema. Errors:\n\n{formatted_errors}") - def get_extensions( - self, sync: bool = False - ) -> List[Union[Type[Extension], Extension]]: - extensions = list(self.extensions) - - if self.directives: - extensions.append(DirectivesExtensionSync if sync else DirectivesExtension) + self.query = self.schema_converter.type_map[query_type.name] - return extensions - - @lru_cache() def get_type_by_name( self, name: str ) -> Optional[ Union[TypeDefinition, ScalarDefinition, EnumDefinition, StrawberryUnion] ]: - # TODO: respect auto_camel_case if name in self.schema_converter.type_map: return self.schema_converter.type_map[name].definition return None - def get_field_for_type( - self, field_name: str, type_name: str - ) -> Optional[StrawberryField]: - type_ = self.get_type_by_name(type_name) - - if not type_: - return None # pragma: no cover - - assert isinstance(type_, TypeDefinition) - - return next( - ( - field - for field in type_.fields - if self.config.name_converter.get_graphql_name(field) == field_name - ), - None, - ) - @lru_cache() def get_directive_by_name(self, graphql_name: str) -> Optional[StrawberryDirective]: return next( @@ -203,16 +124,12 @@ def get_directive_by_name(self, graphql_name: str) -> Optional[StrawberryDirecti async def execute( self, - query: Optional[str], + query: str, variable_values: Optional[Dict[str, Any]] = None, context_value: Optional[Any] = None, root_value: Optional[Any] = None, operation_name: Optional[str] = None, - allowed_operation_types: Optional[Iterable[OperationType]] = None, ) -> ExecutionResult: - if allowed_operation_types is None: - allowed_operation_types = DEFAULT_ALLOWED_OPERATION_TYPES - # Create execution context execution_context = ExecutionContext( query=query, @@ -225,27 +142,25 @@ async def execute( result = await execute( self._schema, - extensions=self.get_extensions(), + query, + extensions=list(self.extensions) + [DirectivesExtension], execution_context_class=self.execution_context_class, execution_context=execution_context, - allowed_operation_types=allowed_operation_types, - process_errors=self.process_errors, ) + if result.errors: + self.process_errors(result.errors, execution_context=execution_context) + return result def execute_sync( self, - query: Optional[str], + query: str, variable_values: Optional[Dict[str, Any]] = None, context_value: Optional[Any] = None, root_value: Optional[Any] = None, operation_name: Optional[str] = None, - allowed_operation_types: Optional[Iterable[OperationType]] = None, ) -> ExecutionResult: - if allowed_operation_types is None: - allowed_operation_types = DEFAULT_ALLOWED_OPERATION_TYPES - execution_context = ExecutionContext( query=query, schema=self, @@ -257,18 +172,19 @@ def execute_sync( result = execute_sync( self._schema, - extensions=self.get_extensions(sync=True), + query, + extensions=list(self.extensions) + [DirectivesExtensionSync], execution_context_class=self.execution_context_class, execution_context=execution_context, - allowed_operation_types=allowed_operation_types, - process_errors=self.process_errors, ) + if result.errors: + self.process_errors(result.errors, execution_context=execution_context) + return result async def subscribe( self, - # TODO: make this optional when we support extensions query: str, variable_values: Optional[Dict[str, Any]] = None, context_value: Optional[Any] = None, diff --git a/strawberry/schema/schema_converter.py b/strawberry/schema/schema_converter.py index 5d01a5305c..308380e622 100644 --- a/strawberry/schema/schema_converter.py +++ b/strawberry/schema/schema_converter.py @@ -1,19 +1,7 @@ from __future__ import annotations -import dataclasses -import sys -from typing import ( - Any, - Callable, - Dict, - List, - Optional, - Tuple, - Type, - TypeVar, - Union, - cast, -) +from enum import Enum +from typing import Any, Callable, Dict, List, Optional, Tuple, Type, Union, cast from graphql import ( GraphQLArgument, @@ -34,33 +22,24 @@ GraphQLScalarType, GraphQLUnionType, Undefined, - ValueNode, ) -from graphql.language.directive_locations import DirectiveLocation -from strawberry.annotation import StrawberryAnnotation -from strawberry.arguments import StrawberryArgument, convert_arguments +from strawberry.arguments import UNSET, StrawberryArgument, convert_arguments, is_unset from strawberry.custom_scalar import ScalarDefinition, ScalarWrapper from strawberry.directive import StrawberryDirective from strawberry.enum import EnumDefinition, EnumValue from strawberry.exceptions import ( - DuplicatedTypeName, - InvalidTypeInputForUnion, MissingTypesForGenericError, ScalarAlreadyRegisteredError, - UnresolvedFieldTypeError, ) -from strawberry.field import UNRESOLVED, StrawberryField +from strawberry.field import StrawberryField from strawberry.lazy_type import LazyType -from strawberry.private import is_private from strawberry.schema.config import StrawberryConfig from strawberry.schema.types.scalar import _make_scalar_type -from strawberry.schema_directive import StrawberrySchemaDirective from strawberry.type import StrawberryList, StrawberryOptional, StrawberryType from strawberry.types.info import Info from strawberry.types.types import TypeDefinition from strawberry.union import StrawberryUnion -from strawberry.unset import UNSET from strawberry.utils.await_maybe import await_maybe from . import compat @@ -72,30 +51,15 @@ # subclass the GraphQLEnumType class to enable returning Enum members from # resolvers. class CustomGraphQLEnumType(GraphQLEnumType): - def __init__(self, enum: EnumDefinition, *args, **kwargs): - super().__init__(*args, **kwargs) - self.wrapped_cls = enum.wrapped_cls - def serialize(self, output_value: Any) -> str: - if isinstance(output_value, self.wrapped_cls): + if isinstance(output_value, Enum): return output_value.name return super().serialize(output_value) - def parse_value(self, input_value: str) -> Any: - return self.wrapped_cls(super().parse_value(input_value)) - - def parse_literal( - self, value_node: ValueNode, _variables: Optional[Dict[str, Any]] = None - ) -> Any: - return self.wrapped_cls(super().parse_literal(value_node, _variables)) - class GraphQLCoreConverter: # TODO: Make abstract - # Extension key used to link a GraphQLType back into the Strawberry definition - DEFINITION_BACKREF = "strawberry-definition" - def __init__( self, config: StrawberryConfig, @@ -114,9 +78,6 @@ def from_argument(self, argument: StrawberryArgument) -> GraphQLArgument: default_value=default_value, description=argument.description, deprecation_reason=argument.deprecation_reason, - extensions={ - GraphQLCoreConverter.DEFINITION_BACKREF: argument, - }, ) def from_enum(self, enum: EnumDefinition) -> CustomGraphQLEnumType: @@ -125,21 +86,15 @@ def from_enum(self, enum: EnumDefinition) -> CustomGraphQLEnumType: assert enum_name is not None # Don't reevaluate known types - cached_type = self.type_map.get(enum_name, None) - if cached_type: - self.validate_same_type_definition(enum_name, enum, cached_type) - graphql_enum = cached_type.implementation + if enum_name in self.type_map: + graphql_enum = self.type_map[enum_name].implementation assert isinstance(graphql_enum, CustomGraphQLEnumType) # For mypy return graphql_enum graphql_enum = CustomGraphQLEnumType( - enum=enum, name=enum_name, values={item.name: self.from_enum_value(item) for item in enum.values}, description=enum.description, - extensions={ - GraphQLCoreConverter.DEFINITION_BACKREF: enum, - }, ) self.type_map[enum_name] = ConcreteType( @@ -149,14 +104,7 @@ def from_enum(self, enum: EnumDefinition) -> CustomGraphQLEnumType: return graphql_enum def from_enum_value(self, enum_value: EnumValue) -> GraphQLEnumValue: - return GraphQLEnumValue( - enum_value.value, - deprecation_reason=enum_value.deprecation_reason, - description=enum_value.description, - extensions={ - GraphQLCoreConverter.DEFINITION_BACKREF: enum_value, - }, - ) + return GraphQLEnumValue(enum_value.value) def from_directive(self, directive: StrawberryDirective) -> GraphQLDirective: graphql_arguments = {} @@ -172,47 +120,6 @@ def from_directive(self, directive: StrawberryDirective) -> GraphQLDirective: locations=directive.locations, args=graphql_arguments, description=directive.description, - extensions={ - GraphQLCoreConverter.DEFINITION_BACKREF: directive, - }, - ) - - def from_schema_directive(self, cls: Type) -> GraphQLDirective: - strawberry_directive = cast( - StrawberrySchemaDirective, cls.__strawberry_directive__ - ) - module = sys.modules[cls.__module__] - - args: Dict[str, GraphQLArgument] = {} - for field in strawberry_directive.fields: - default = field.default - if default == dataclasses.MISSING: - default = UNSET - - name = self.config.name_converter.get_graphql_name(field) - args[name] = self.from_argument( - StrawberryArgument( - python_name=field.python_name or field.name, - graphql_name=None, - type_annotation=StrawberryAnnotation( - annotation=field.type, - namespace=module.__dict__, - ), - default=default, - ) - ) - - return GraphQLDirective( - name=self.config.name_converter.from_directive(strawberry_directive), - locations=[ - DirectiveLocation(loc.value) for loc in strawberry_directive.locations - ], - args=args, - is_repeatable=strawberry_directive.repeatable, - description=strawberry_directive.description, - extensions={ - GraphQLCoreConverter.DEFINITION_BACKREF: strawberry_directive, - }, ) def from_field(self, field: StrawberryField) -> GraphQLField: @@ -237,16 +144,14 @@ def from_field(self, field: StrawberryField) -> GraphQLField: subscribe=subscribe, description=field.description, deprecation_reason=field.deprecation_reason, - extensions={ - GraphQLCoreConverter.DEFINITION_BACKREF: field, - }, + extensions={"python_name": field.python_name}, ) def from_input_field(self, field: StrawberryField) -> GraphQLInputField: field_type = cast(GraphQLInputType, self.from_maybe_optional(field.type)) default_value: object - if field.default_value is UNSET or field.default_value is dataclasses.MISSING: + if is_unset(field.default_value): default_value = Undefined else: default_value = field.default_value @@ -256,57 +161,6 @@ def from_input_field(self, field: StrawberryField) -> GraphQLInputField: default_value=default_value, description=field.description, deprecation_reason=field.deprecation_reason, - extensions={ - GraphQLCoreConverter.DEFINITION_BACKREF: field, - }, - ) - - FieldType = TypeVar("FieldType", GraphQLField, GraphQLInputField) - - @staticmethod - def _get_thunk_mapping( - type_definition: TypeDefinition, - name_converter: Callable[[StrawberryField], str], - field_converter: Callable[[StrawberryField], FieldType], - ) -> Dict[str, FieldType]: - """Create a GraphQL core `ThunkMapping` mapping of field names to field types. - - This method filters out remaining `strawberry.Private` annotated fields that - could not be filtered during the initialization of a `TypeDefinition` due to - postponed type-hint evaluation (PEP-563). Performing this filtering now (at - schema conversion time) ensures that all types to be included in the schema - should have already been resolved. - - Raises: - TypeError: If the type of a field in ``fields`` is `UNRESOLVED` - """ - thunk_mapping = {} - - for field in type_definition.fields: - if field.type is UNRESOLVED: - raise UnresolvedFieldTypeError(type_definition, field) - - if not is_private(field.type): - thunk_mapping[name_converter(field)] = field_converter(field) - - return thunk_mapping - - def get_graphql_fields( - self, type_definition: TypeDefinition - ) -> Dict[str, GraphQLField]: - return self._get_thunk_mapping( - type_definition=type_definition, - name_converter=self.config.name_converter.from_field, - field_converter=self.from_field, - ) - - def get_graphql_input_fields( - self, type_definition: TypeDefinition - ) -> Dict[str, GraphQLInputField]: - return self._get_thunk_mapping( - type_definition=type_definition, - name_converter=self.config.name_converter.from_field, - field_converter=self.from_input_field, ) def from_input_object(self, object_type: type) -> GraphQLInputObjectType: @@ -315,20 +169,24 @@ def from_input_object(self, object_type: type) -> GraphQLInputObjectType: type_name = self.config.name_converter.from_type(type_definition) # Don't reevaluate known types - cached_type = self.type_map.get(type_name, None) - if cached_type: - self.validate_same_type_definition(type_name, type_definition, cached_type) + if type_name in self.type_map: graphql_object_type = self.type_map[type_name].implementation assert isinstance(graphql_object_type, GraphQLInputObjectType) # For mypy return graphql_object_type + def get_graphql_fields() -> Dict[str, GraphQLInputField]: + graphql_fields = {} + for field in type_definition.fields: + field_name = self.config.name_converter.from_field(field) + + graphql_fields[field_name] = self.from_input_field(field) + + return graphql_fields + graphql_object_type = GraphQLInputObjectType( name=type_name, - fields=lambda: self.get_graphql_input_fields(type_definition), + fields=get_graphql_fields, description=type_definition.description, - extensions={ - GraphQLCoreConverter.DEFINITION_BACKREF: type_definition, - }, ) self.type_map[type_name] = ConcreteType( @@ -343,21 +201,25 @@ def from_interface(self, interface: TypeDefinition) -> GraphQLInterfaceType: interface_name = self.config.name_converter.from_type(interface) # Don't reevaluate known types - cached_type = self.type_map.get(interface_name, None) - if cached_type: - self.validate_same_type_definition(interface_name, interface, cached_type) - graphql_interface = cached_type.implementation + if interface_name in self.type_map: + graphql_interface = self.type_map[interface_name].implementation assert isinstance(graphql_interface, GraphQLInterfaceType) # For mypy return graphql_interface + def get_graphql_fields() -> Dict[str, GraphQLField]: + graphql_fields = {} + + for field in interface.fields: + field_name = self.config.name_converter.from_field(field) + graphql_fields[field_name] = self.from_field(field) + + return graphql_fields + graphql_interface = GraphQLInterfaceType( name=interface_name, - fields=lambda: self.get_graphql_fields(interface), + fields=get_graphql_fields, interfaces=list(map(self.from_interface, interface.interfaces)), description=interface.description, - extensions={ - GraphQLCoreConverter.DEFINITION_BACKREF: interface, - }, ) self.type_map[interface_name] = ConcreteType( @@ -376,42 +238,38 @@ def from_object(self, object_type: TypeDefinition) -> GraphQLObjectType: object_type_name = self.config.name_converter.from_type(object_type) # Don't reevaluate known types - cached_type = self.type_map.get(object_type_name, None) - if cached_type: - self.validate_same_type_definition( - object_type_name, object_type, cached_type - ) - graphql_object_type = cached_type.implementation + if object_type_name in self.type_map: + graphql_object_type = self.type_map[object_type_name].implementation assert isinstance(graphql_object_type, GraphQLObjectType) # For mypy return graphql_object_type - def _get_is_type_of() -> Optional[Callable[[Any, GraphQLResolveInfo], bool]]: - if object_type.is_type_of: - return object_type.is_type_of + def get_graphql_fields() -> Dict[str, GraphQLField]: + graphql_fields = {} - if not object_type.interfaces: - return None + for field in object_type.fields: + field_name = self.config.name_converter.from_field(field) - def is_type_of(obj: Any, _info: GraphQLResolveInfo) -> bool: - if object_type.concrete_of and ( - hasattr(obj, "_type_definition") - and obj._type_definition.origin is object_type.concrete_of.origin - ): - return True + graphql_fields[field_name] = self.from_field(field) + + return graphql_fields + is_type_of: Optional[Callable[[Any, GraphQLResolveInfo], bool]] + if object_type.is_type_of: + is_type_of = object_type.is_type_of + elif object_type.interfaces: + + def is_type_of(obj: Any, _info: GraphQLResolveInfo) -> bool: return isinstance(obj, object_type.origin) - return is_type_of + else: + is_type_of = None graphql_object_type = GraphQLObjectType( name=object_type_name, - fields=lambda: self.get_graphql_fields(object_type), + fields=get_graphql_fields, interfaces=list(map(self.from_interface, object_type.interfaces)), description=object_type.description, - is_type_of=_get_is_type_of(), - extensions={ - GraphQLCoreConverter.DEFINITION_BACKREF: object_type, - }, + is_type_of=is_type_of, ) self.type_map[object_type_name] = ConcreteType( @@ -423,19 +281,6 @@ def is_type_of(obj: Any, _info: GraphQLResolveInfo) -> bool: def from_resolver( self, field: StrawberryField ) -> Callable: # TODO: Take StrawberryResolver - field.default_resolver = self.config.default_resolver - - if field.is_basic_field: - - def _get_basic_result(_source: Any, *args, **kwargs): - # Call `get_result` without an info object or any args or - # kwargs because this is a basic field with no resolver. - return field.get_result(_source, info=None, args=[], kwargs={}) - - _get_basic_result._is_default = True # type: ignore - - return _get_basic_result - def _get_arguments( source: Any, info: Info, @@ -457,16 +302,14 @@ def _get_arguments( args = [] if field.base_resolver: - if field.base_resolver.self_parameter: + if field.base_resolver.has_self_arg: args.append(source) - root_parameter = field.base_resolver.root_parameter - if root_parameter: - kwargs[root_parameter.name] = source + if field.base_resolver.has_root_arg: + kwargs["root"] = source - info_parameter = field.base_resolver.info_parameter - if info_parameter: - kwargs[info_parameter.name] = info + if field.base_resolver.has_info_arg: + kwargs["info"] = info return args, kwargs @@ -536,7 +379,6 @@ def from_scalar(self, scalar: Type) -> GraphQLScalarType: if scalar in self.scalar_registry: _scalar_definition = self.scalar_registry[scalar] - # TODO: check why we need the cast and we are not trying with getattr first if isinstance(_scalar_definition, ScalarWrapper): scalar_definition = _scalar_definition._scalar_definition else: @@ -557,15 +399,8 @@ def from_scalar(self, scalar: Type) -> GraphQLScalarType: definition=scalar_definition, implementation=implementation ) else: - other_definition = self.type_map[scalar_name].definition - - # TODO: the other definition might not be a scalar, we should - # handle this case better, since right now we assume it is a scalar - - if other_definition != scalar_definition: - other_definition = cast(ScalarDefinition, other_definition) - - raise ScalarAlreadyRegisteredError(scalar_definition, other_definition) + if self.type_map[scalar_name].definition != scalar_definition: + raise ScalarAlreadyRegisteredError(scalar_name) implementation = cast( GraphQLScalarType, self.type_map[scalar_name].implementation @@ -629,8 +464,6 @@ def from_union(self, union: StrawberryUnion) -> GraphQLUnionType: for type_ in union.types: graphql_type = self.from_type(type_) - if isinstance(graphql_type, GraphQLInputObjectType): - raise InvalidTypeInputForUnion(graphql_type) assert isinstance(graphql_type, GraphQLObjectType) graphql_types.append(graphql_type) @@ -640,9 +473,6 @@ def from_union(self, union: StrawberryUnion) -> GraphQLUnionType: types=graphql_types, description=union.description, resolve_type=union.get_type_resolver(self.type_map), - extensions={ - GraphQLCoreConverter.DEFINITION_BACKREF: union, - }, ) self.type_map[union_name] = ConcreteType( @@ -650,107 +480,3 @@ def from_union(self, union: StrawberryUnion) -> GraphQLUnionType: ) return graphql_union - - def _get_is_type_of( - self, - object_type: TypeDefinition, - ) -> Optional[Callable[[Any, GraphQLResolveInfo], bool]]: - if object_type.is_type_of: - return object_type.is_type_of - - if object_type.interfaces: - - def is_type_of(obj: Any, _info: GraphQLResolveInfo) -> bool: - if object_type.concrete_of and ( - hasattr(obj, "_type_definition") - and obj._type_definition.origin is object_type.concrete_of.origin - ): - return True - - return isinstance(obj, object_type.origin) - - return is_type_of - - return None - - def validate_same_type_definition( - self, name: str, type_definition: StrawberryType, cached_type: ConcreteType - ) -> None: - # if the type definitions are the same we can return - if cached_type.definition == type_definition: - return - - # otherwise we need to check if we are dealing with different instances - # of the same type generic type. This happens when using the same generic - # type in different places in the schema, like in the following example: - - # >>> @strawberry.type - # >>> class A(Generic[T]): - # >>> a: T - - # >>> @strawberry.type - # >>> class Query: - # >>> first: A[int] - # >>> second: A[int] - - # in theory we won't ever have duplicated definitions for the same generic, - # but we are doing the check in an exhaustive way just in case we missed - # something. - - # we only do this check for TypeDefinitions, as they are the only ones - # that can be generic. - # of they are of the same generic type, we need to check if the type - # var map is the same, in that case we can return - - if ( - isinstance(type_definition, TypeDefinition) - and isinstance(cached_type.definition, TypeDefinition) - and cached_type.definition.concrete_of is not None - and cached_type.definition.concrete_of == type_definition.concrete_of - and ( - cached_type.definition.type_var_map.keys() - == type_definition.type_var_map.keys() - ) - ): - # manually compare type_var_maps while resolving any lazy types - # so that they're considered equal to the actual types they're referencing - equal = True - for type_var, type1 in cached_type.definition.type_var_map.items(): - type2 = type_definition.type_var_map[type_var] - # both lazy types are always resolved because two different lazy types - # may be referencing the same actual type - if isinstance(type1, LazyType): - type1 = type1.resolve_type() - elif isinstance(type1, StrawberryOptional) and isinstance( - type1.of_type, LazyType - ): - type1.of_type = type1.of_type.resolve_type() - - if isinstance(type2, LazyType): - type2 = type2.resolve_type() - elif isinstance(type2, StrawberryOptional) and isinstance( - type2.of_type, LazyType - ): - type2.of_type = type2.of_type.resolve_type() - - if type1 != type2: - equal = False - break - if equal: - return - - if isinstance(type_definition, TypeDefinition): - first_origin = type_definition.origin - elif isinstance(type_definition, EnumDefinition): - first_origin = type_definition.wrapped_cls - else: - first_origin = None - - if isinstance(cached_type.definition, TypeDefinition): - second_origin = cached_type.definition.origin - elif isinstance(cached_type.definition, EnumDefinition): - second_origin = cached_type.definition.wrapped_cls - else: - second_origin = None - - raise DuplicatedTypeName(first_origin, second_origin, name) diff --git a/strawberry/schema/types/__init__.py b/strawberry/schema/types/__init__.py index 380dd3b45e..9f03030d66 100644 --- a/strawberry/schema/types/__init__.py +++ b/strawberry/schema/types/__init__.py @@ -1,3 +1,4 @@ -from .concrete_type import ConcreteType +from .concrete_type import ConcreteType as ConcreteType + __all__ = ["ConcreteType"] diff --git a/strawberry/schema/types/base_scalars.py b/strawberry/schema/types/base_scalars.py index f80b3973bc..98627d55c4 100644 --- a/strawberry/schema/types/base_scalars.py +++ b/strawberry/schema/types/base_scalars.py @@ -5,6 +5,7 @@ from typing import Callable import dateutil.parser + from graphql import GraphQLError from strawberry.custom_scalar import scalar @@ -20,9 +21,9 @@ def inner(value: str): return inner -def parse_decimal(value: object) -> decimal.Decimal: +def parse_decimal(value: str) -> decimal.Decimal: try: - return decimal.Decimal(str(value)) + return decimal.Decimal(value) except decimal.DecimalException: raise GraphQLError(f'Value cannot represent a Decimal: "{value}".') diff --git a/strawberry/schema/types/concrete_type.py b/strawberry/schema/types/concrete_type.py index b8baeb9c6b..b7f5ed26e3 100644 --- a/strawberry/schema/types/concrete_type.py +++ b/strawberry/schema/types/concrete_type.py @@ -8,6 +8,7 @@ from strawberry.types.types import TypeDefinition from strawberry.union import StrawberryUnion + Field = Union[GraphQLInputField, GraphQLField] diff --git a/strawberry/schema/types/scalar.py b/strawberry/schema/types/scalar.py index 7843c9b698..8447ce7145 100644 --- a/strawberry/schema/types/scalar.py +++ b/strawberry/schema/types/scalar.py @@ -19,16 +19,12 @@ def _make_scalar_type(definition: ScalarDefinition) -> GraphQLScalarType: - from strawberry.schema.schema_converter import GraphQLCoreConverter - return GraphQLScalarType( name=definition.name, description=definition.description, - specified_by_url=definition.specified_by_url, serialize=definition.serialize, parse_value=definition.parse_value, parse_literal=definition.parse_literal, - extensions={GraphQLCoreConverter.DEFINITION_BACKREF: definition}, ) @@ -36,7 +32,6 @@ def _make_scalar_definition(scalar_type: GraphQLScalarType) -> ScalarDefinition: return ScalarDefinition( name=scalar_type.name, description=scalar_type.name, - specified_by_url=scalar_type.specified_by_url, serialize=scalar_type.serialize, parse_literal=scalar_type.parse_literal, parse_value=scalar_type.parse_value, @@ -44,22 +39,18 @@ def _make_scalar_definition(scalar_type: GraphQLScalarType) -> ScalarDefinition: ) -def _get_scalar_definition(scalar) -> ScalarDefinition: - return scalar._scalar_definition - - DEFAULT_SCALAR_REGISTRY: Dict[object, ScalarDefinition] = { - type(None): _get_scalar_definition(base_scalars.Void), - None: _get_scalar_definition(base_scalars.Void), + type(None): base_scalars.Void._scalar_definition, + None: base_scalars.Void._scalar_definition, str: _make_scalar_definition(GraphQLString), int: _make_scalar_definition(GraphQLInt), float: _make_scalar_definition(GraphQLFloat), bool: _make_scalar_definition(GraphQLBoolean), ID: _make_scalar_definition(GraphQLID), - UUID: _get_scalar_definition(base_scalars.UUID), - Upload: _get_scalar_definition(Upload), - datetime.date: _get_scalar_definition(base_scalars.Date), - datetime.datetime: _get_scalar_definition(base_scalars.DateTime), - datetime.time: _get_scalar_definition(base_scalars.Time), - decimal.Decimal: _get_scalar_definition(base_scalars.Decimal), + UUID: base_scalars.UUID._scalar_definition, + Upload: Upload._scalar_definition, + datetime.date: base_scalars.Date._scalar_definition, + datetime.datetime: base_scalars.DateTime._scalar_definition, + datetime.time: base_scalars.Time._scalar_definition, + decimal.Decimal: base_scalars.Decimal._scalar_definition, } diff --git a/strawberry/schema_directive.py b/strawberry/schema_directive.py index 5d6132fb41..3d4ca40ae4 100644 --- a/strawberry/schema_directive.py +++ b/strawberry/schema_directive.py @@ -1,13 +1,7 @@ import dataclasses +from dataclasses import dataclass from enum import Enum -from typing import List, Optional, Type, TypeVar - -from strawberry.object_type import _wrap_dataclass -from strawberry.types.type_resolver import _get_fields - -from .directive import directive_field -from .field import StrawberryField, field -from .utils.typing import __dataclass_transform__ +from typing import List, Optional, Type class Location(Enum): @@ -26,49 +20,38 @@ class Location(Enum): @dataclasses.dataclass class StrawberrySchemaDirective: + wrap: Type python_name: str graphql_name: Optional[str] locations: List[Location] - fields: List["StrawberryField"] description: Optional[str] = None - repeatable: bool = False - print_definition: bool = True - origin: Optional[Type] = None + instance: Optional[object] = dataclasses.field(init=False) + def __call__(self, *args, **kwargs): + # TODO: this should be implemented differently -T = TypeVar("T", bound=Type) + x = StrawberrySchemaDirective( + wrap=self.wrap, + python_name=self.python_name, + graphql_name=self.graphql_name, + locations=self.locations, + description=self.description, + ) + x.instance = self.wrap(*args, **kwargs) + return x -@__dataclass_transform__( - order_default=True, - kw_only_default=True, - field_descriptors=(directive_field, field, StrawberryField), -) -def schema_directive( - *, - locations: List[Location], - description: Optional[str] = None, - name: Optional[str] = None, - repeatable: bool = False, - print_definition: bool = True, -): - def _wrap(cls: T) -> T: - cls = _wrap_dataclass(cls) - fields = _get_fields(cls) - cls.__strawberry_directive__ = StrawberrySchemaDirective( +def schema_directive(*, locations: List[Location], description=None, name=None): + def _wrap(cls: Type) -> StrawberrySchemaDirective: + return StrawberrySchemaDirective( python_name=cls.__name__, + wrap=dataclass(cls), graphql_name=name, locations=locations, description=description, - repeatable=repeatable, - fields=fields, - print_definition=print_definition, - origin=cls, ) - return cls - return _wrap diff --git a/strawberry/starlite/__init__.py b/strawberry/starlite/__init__.py deleted file mode 100644 index 182ca8069f..0000000000 --- a/strawberry/starlite/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from strawberry.starlite.controller import BaseContext, make_graphql_controller - -__all__ = ["BaseContext", "make_graphql_controller"] diff --git a/strawberry/starlite/controller.py b/strawberry/starlite/controller.py deleted file mode 100644 index b56ddd7931..0000000000 --- a/strawberry/starlite/controller.py +++ /dev/null @@ -1,411 +0,0 @@ -"""Starlite integration for strawberry-graphql.""" - -from __future__ import annotations - -import json -from dataclasses import dataclass -from datetime import timedelta -from typing import TYPE_CHECKING, Any, Dict, Optional, Union, cast - -from starlite import ( - BackgroundTasks, - Controller, - HttpMethod, - MediaType, - Provide, - Request, - Response, - WebSocket, - get, - post, - websocket, -) -from starlite.exceptions import ( - NotFoundException, - SerializationException, - ValidationException, -) -from starlite.status_codes import ( - HTTP_200_OK, - HTTP_400_BAD_REQUEST, - HTTP_415_UNSUPPORTED_MEDIA_TYPE, -) -from strawberry.exceptions import InvalidCustomContext, MissingQueryError -from strawberry.file_uploads.utils import replace_placeholders_with_files -from strawberry.http import ( - GraphQLHTTPResponse, - parse_query_params, - parse_request_data, - process_result, -) -from strawberry.schema import BaseSchema -from strawberry.schema.exceptions import InvalidOperationTypeError -from strawberry.subscriptions import GRAPHQL_TRANSPORT_WS_PROTOCOL, GRAPHQL_WS_PROTOCOL -from strawberry.subscriptions.protocols.graphql_transport_ws import ( - WS_4406_PROTOCOL_NOT_ACCEPTABLE, -) -from strawberry.types import ExecutionResult -from strawberry.types.graphql import OperationType -from strawberry.utils.debug import pretty_print_graphql_operation -from strawberry.utils.graphiql import get_graphiql_html - -from .handlers.graphql_transport_ws_handler import ( - GraphQLTransportWSHandler as BaseGraphQLTransportWSHandler, -) -from .handlers.graphql_ws_handler import GraphQLWSHandler as BaseGraphQLWSHandler - -if TYPE_CHECKING: - from typing import Iterable, List, Tuple, Type - - from starlite.types import AnyCallable, Dependencies - - MergedContext = Union[ - "BaseContext", - Union[ - Dict[str, Any], - Dict[str, BackgroundTasks], - Dict[str, Request], - Dict[str, Response], - Dict[str, websocket], - ], - ] - -CustomContext = Union["BaseContext", Dict[str, Any]] - - -async def _context_getter( - custom_context: Optional[CustomContext], - request: "Request", -) -> "MergedContext": - if isinstance(custom_context, BaseContext): - custom_context.request = request - return custom_context - default_context = { - "request": request, - } - if isinstance(custom_context, dict): - return { - **default_context, - **custom_context, - } - if custom_context is None: - return default_context - raise InvalidCustomContext() - - -@dataclass -class GraphQLResource: - data: Optional[Dict[str, object]] - errors: Optional[List[object]] - extensions: Optional[Dict[str, object]] - - -@dataclass -class EmptyResponseModel: - pass - - -class GraphQLWSHandler(BaseGraphQLWSHandler): - async def get_context(self) -> "Any": - return await self._get_context() - - async def get_root_value(self) -> "Any": - return await self._get_root_value() - - -class GraphQLTransportWSHandler(BaseGraphQLTransportWSHandler): - async def get_context(self) -> "Any": - return await self._get_context() - - async def get_root_value(self) -> "Any": - return await self._get_root_value() - - -class BaseContext: - def __init__(self): - self.request: Optional[Union[Request, WebSocket]] = None - self.response: Optional[Response] = None - - -def make_graphql_controller( - schema: BaseSchema, - path: str = "", - graphiql: bool = True, - allow_queries_via_get: bool = True, - keep_alive: bool = False, - keep_alive_interval: float = 1, - debug: bool = False, - # TODO: root typevar - root_value_getter: Optional[AnyCallable] = None, - # TODO: context typevar - context_getter: Optional[AnyCallable] = None, - subscription_protocols: Tuple[str, ...] = ( - GRAPHQL_TRANSPORT_WS_PROTOCOL, - GRAPHQL_WS_PROTOCOL, - ), - connection_init_wait_timeout: timedelta = timedelta(minutes=1), -) -> Type[Controller]: - routes_path = path - - if context_getter is None: - - def custom_context_getter_(): - return None - - else: - custom_context_getter_ = context_getter - - if root_value_getter is None: - - def root_value_getter_(): - return None - - else: - root_value_getter_ = root_value_getter - - class GraphQLController(Controller): - path: str = routes_path - dependencies: Optional[Dependencies] = { - "custom_context": Provide(custom_context_getter_), - "context": Provide(_context_getter), - "root_value": Provide(root_value_getter_), - } - graphql_ws_handler_class: Type[GraphQLWSHandler] = GraphQLWSHandler - graphql_transport_ws_handler_class: Type[ - GraphQLTransportWSHandler - ] = GraphQLTransportWSHandler - - _schema: BaseSchema = schema - _graphiql: bool = graphiql - _allow_queries_via_get: bool = allow_queries_via_get - _keep_alive: bool = keep_alive - _keep_alive_interval: float = keep_alive_interval - _debug: bool = debug - _protocols: Tuple[str, ...] = subscription_protocols - _connection_init_wait_timeout: timedelta = connection_init_wait_timeout - - async def execute( - self, - query: "Optional[str]", - variables: "Optional[Dict[str, Any]]" = None, - context: "Optional[CustomContext]" = None, - operation_name: "Optional[str]" = None, - root_value: "Optional[Any]" = None, - allowed_operation_types: "Optional[Iterable[OperationType]]" = None, - ): - if self._debug: - pretty_print_graphql_operation(operation_name, query or "", variables) - - return await self._schema.execute( - query, - root_value=root_value, - variable_values=variables, - operation_name=operation_name, - context_value=context, - allowed_operation_types=allowed_operation_types, - ) - - async def process_result( - self, result: "ExecutionResult" - ) -> "GraphQLHTTPResponse": - return process_result(result) - - async def execute_request( - self, - request: "Request", - data: dict, - context: "CustomContext", - root_value: "Any", - ) -> "Response[Union[GraphQLResource, str]]": - request_data = parse_request_data(data or {}) - - allowed_operation_types = OperationType.from_http(request.method) - - if not self._allow_queries_via_get and request.method == HttpMethod.GET: - allowed_operation_types = allowed_operation_types - { - OperationType.QUERY - } - - response: "Union[Response[dict], Response[BaseContext]]" = Response( - {}, background=BackgroundTasks([]) - ) - - if isinstance(context, BaseContext): - context.response = response - elif isinstance(context, dict): - context["response"] = response - try: - result = await self.execute( - request_data.query, - variables=request_data.variables, - context=context, - operation_name=request_data.operation_name, - root_value=root_value, - allowed_operation_types=allowed_operation_types, - ) - except InvalidOperationTypeError as e: - return Response( - e.as_http_error_reason(request.method), - status_code=HTTP_400_BAD_REQUEST, - media_type=MediaType.TEXT, - ) - except MissingQueryError: - return Response( - "No GraphQL query found in the request", - status_code=HTTP_400_BAD_REQUEST, - media_type=MediaType.TEXT, - ) - - response_data = await self.process_result(result) - - actual_response: Response[GraphQLHTTPResponse] = Response( - response_data, status_code=HTTP_200_OK, media_type=MediaType.JSON - ) - - return self._merge_responses(response, actual_response) - - def should_render_graphiql(self, request: Request) -> bool: - if not self._graphiql: - return False - - return bool( - {"text/html", "*/*"} & (set(request.headers.getall("accept", set()))) - ) - - def get_graphiql_response(self) -> Response[str]: - html = get_graphiql_html() - return Response(html, media_type=MediaType.HTML) - - @staticmethod - def _merge_responses( - response: Response, actual_response: Response - ) -> Response[Union[GraphQLResource, str]]: - actual_response.headers.update(response.headers) - actual_response.cookies.extend(response.cookies) - actual_response.background = response.background - if response.status_code: - actual_response.status_code = response.status_code - - return actual_response - - @get(raises=[ValidationException, NotFoundException]) - async def handle_http_get( - self, - request: "Request", - context: "CustomContext", - root_value: "Any", - ) -> "Response[Union[GraphQLResource, str]]": - if request.query_params: - try: - query_data = parse_query_params( - cast("Dict[str, Any]", request.query_params) - ) - except json.JSONDecodeError: - raise ValidationException( - detail="Unable to parse request body as JSON" - ) - return await self.execute_request( - request=request, - data=query_data, - context=context, - root_value=root_value, - ) - if self.should_render_graphiql(request): - return cast( - "Response[Union[GraphQLResource, str]]", - self.get_graphiql_response(), - ) - raise NotFoundException() - - @post(status_code=HTTP_200_OK) - async def handle_http_post( - self, - request: "Request", - context: "CustomContext", - root_value: "Any", - ) -> "Response[Union[GraphQLResource, str]]": - actual_response: Response[Union[GraphQLResource, str]] - - content_type, _ = request.content_type - - if "application/json" in content_type: - try: - data = await request.json() - except SerializationException: - actual_response = Response( - "Unable to parse request body as JSON", - status_code=HTTP_400_BAD_REQUEST, - media_type=MediaType.TEXT, - ) - return actual_response - elif content_type.startswith("multipart/form-data"): - multipart_data = await request.form() - operations: Dict[str, Any] = multipart_data.get("operations", "{}") - files_map: Dict[str, List[str]] = multipart_data.get("map", "{}") - data = replace_placeholders_with_files( - operations, files_map, multipart_data - ) - else: - actual_response = Response( - "Unsupported Media Type", - status_code=HTTP_415_UNSUPPORTED_MEDIA_TYPE, - media_type=MediaType.TEXT, - ) - return actual_response - - return await self.execute_request( - request=request, - data=data, - context=context, - root_value=root_value, - ) - - @websocket() - async def websocket_endpoint( - self, - socket: "WebSocket", - context: "CustomContext", - root_value: "Any", - ) -> None: - async def _get_context(): - return context - - async def _get_root_value(): - return root_value - - preferred_protocol = self.pick_preferred_protocol(socket) - if preferred_protocol == GRAPHQL_TRANSPORT_WS_PROTOCOL: - await self.graphql_transport_ws_handler_class( - schema=self._schema, - debug=self._debug, - connection_init_wait_timeout=self._connection_init_wait_timeout, - get_context=_get_context, - get_root_value=_get_root_value, - ws=socket, - ).handle() - elif preferred_protocol == GRAPHQL_WS_PROTOCOL: - await self.graphql_ws_handler_class( - schema=self._schema, - debug=self._debug, - keep_alive=self._keep_alive, - keep_alive_interval=self._keep_alive_interval, - get_context=_get_context, - get_root_value=_get_root_value, - ws=socket, - ).handle() - else: - await socket.close(code=WS_4406_PROTOCOL_NOT_ACCEPTABLE) - - def pick_preferred_protocol(self, socket: WebSocket) -> Optional[str]: - protocols: "List[str]" = socket.scope["subprotocols"] - intersection = set(protocols) & set(self._protocols) - return ( - min( - intersection, - key=lambda i: protocols.index(i) if i else "", - default=None, - ) - or None - ) - - return GraphQLController diff --git a/strawberry/starlite/handlers/graphql_transport_ws_handler.py b/strawberry/starlite/handlers/graphql_transport_ws_handler.py deleted file mode 100644 index 28a9d75abc..0000000000 --- a/strawberry/starlite/handlers/graphql_transport_ws_handler.py +++ /dev/null @@ -1,58 +0,0 @@ -from datetime import timedelta -from typing import Any - -from starlite import WebSocket -from starlite.exceptions import SerializationException, WebSocketDisconnect -from strawberry.schema import BaseSchema -from strawberry.subscriptions import GRAPHQL_TRANSPORT_WS_PROTOCOL -from strawberry.subscriptions.protocols.graphql_transport_ws.handlers import ( - BaseGraphQLTransportWSHandler, -) - - -class GraphQLTransportWSHandler(BaseGraphQLTransportWSHandler): - def __init__( - self, - schema: BaseSchema, - debug: bool, - connection_init_wait_timeout: timedelta, - get_context, - get_root_value, - ws: WebSocket, - ): - super().__init__(schema, debug, connection_init_wait_timeout) - self._get_context = get_context - self._get_root_value = get_root_value - self._ws = ws - - async def get_context(self) -> Any: - return await self._get_context() - - async def get_root_value(self) -> Any: - return await self._get_root_value() - - async def send_json(self, data: dict) -> None: - await self._ws.send_json(data) - - async def close(self, code: int, reason: str) -> None: - # Close messages are not part of the ASGI ref yet - await self._ws.close(code=code) - - async def handle_request(self) -> None: - await self._ws.accept(subprotocols=GRAPHQL_TRANSPORT_WS_PROTOCOL) - - try: - while self._ws.connection_state != "disconnect": - try: - message = await self._ws.receive_json() - except (SerializationException, ValueError): - error_message = "WebSocket message type must be text" - await self.handle_invalid_message(error_message) - else: - await self.handle_message(message) - except WebSocketDisconnect: # pragma: no cover - pass - finally: - for operation_id in list(self.subscriptions.keys()): - await self.cleanup_operation(operation_id) - await self.reap_completed_tasks() diff --git a/strawberry/starlite/handlers/graphql_ws_handler.py b/strawberry/starlite/handlers/graphql_ws_handler.py deleted file mode 100644 index 43073c6d72..0000000000 --- a/strawberry/starlite/handlers/graphql_ws_handler.py +++ /dev/null @@ -1,62 +0,0 @@ -from contextlib import suppress -from typing import Any, Optional - -from starlite import WebSocket -from starlite.exceptions import SerializationException, WebSocketDisconnect -from strawberry.schema import BaseSchema -from strawberry.subscriptions import GRAPHQL_WS_PROTOCOL -from strawberry.subscriptions.protocols.graphql_ws.handlers import BaseGraphQLWSHandler -from strawberry.subscriptions.protocols.graphql_ws.types import OperationMessage - - -class GraphQLWSHandler(BaseGraphQLWSHandler): - def __init__( - self, - schema: BaseSchema, - debug: bool, - keep_alive: bool, - keep_alive_interval: float, - get_context, - get_root_value, - ws: WebSocket, - ): - super().__init__(schema, debug, keep_alive, keep_alive_interval) - self._get_context = get_context - self._get_root_value = get_root_value - self._ws = ws - - async def get_context(self) -> Any: - return await self._get_context() - - async def get_root_value(self) -> Any: - return await self._get_root_value() - - async def send_json(self, data: OperationMessage) -> None: - await self._ws.send_json(data) - - async def close(self, code: int = 1000, reason: Optional[str] = None) -> None: - # Close messages are not part of the ASGI ref yet - await self._ws.close(code=code) - - async def handle_request(self) -> Any: - await self._ws.accept(subprotocols=GRAPHQL_WS_PROTOCOL) - - try: - while self._ws.connection_state != "disconnect": - try: - message = await self._ws.receive_json() - except (SerializationException, ValueError): - # Ignore non-text messages - continue - else: - await self.handle_message(message) - except WebSocketDisconnect: # pragma: no cover - pass - finally: - if self.keep_alive_task: - self.keep_alive_task.cancel() - with suppress(BaseException): - await self.keep_alive_task - - for operation_id in list(self.subscriptions.keys()): - await self.cleanup_operation(operation_id) diff --git a/strawberry/static/graphiql.html b/strawberry/static/graphiql.html index aae78355eb..85e934a2d1 100644 --- a/strawberry/static/graphiql.html +++ b/strawberry/static/graphiql.html @@ -1,159 +1,105 @@ - + + Strawberry GraphiQL - - - - - - - - - - -
Loading...
- - + + + + + + + + + + + + + +
- + + diff --git a/strawberry/subscriptions/__init__.py b/strawberry/subscriptions/__init__.py index bca52eea1b..61a8c08f9f 100644 --- a/strawberry/subscriptions/__init__.py +++ b/strawberry/subscriptions/__init__.py @@ -1,8 +1,2 @@ -from typing_extensions import Literal - GRAPHQL_TRANSPORT_WS_PROTOCOL = "graphql-transport-ws" GRAPHQL_WS_PROTOCOL = "graphql-ws" - -SubscriptionProtocolType = Literal[ # type: ignore - GRAPHQL_TRANSPORT_WS_PROTOCOL, GRAPHQL_WS_PROTOCOL # type: ignore -] diff --git a/strawberry/subscriptions/protocols/graphql_transport_ws/__init__.py b/strawberry/subscriptions/protocols/graphql_transport_ws/__init__.py index 1368214127..e69de29bb2 100644 --- a/strawberry/subscriptions/protocols/graphql_transport_ws/__init__.py +++ b/strawberry/subscriptions/protocols/graphql_transport_ws/__init__.py @@ -1,2 +0,0 @@ -# Code 4406 is "Subprotocol not acceptable" -WS_4406_PROTOCOL_NOT_ACCEPTABLE = 4406 diff --git a/strawberry/subscriptions/protocols/graphql_transport_ws/handlers.py b/strawberry/subscriptions/protocols/graphql_transport_ws/handlers.py index 8bf8d92055..26264d6799 100644 --- a/strawberry/subscriptions/protocols/graphql_transport_ws/handlers.py +++ b/strawberry/subscriptions/protocols/graphql_transport_ws/handlers.py @@ -2,11 +2,10 @@ from abc import ABC, abstractmethod from contextlib import suppress from datetime import timedelta -from typing import Any, AsyncGenerator, Callable, Dict, List, Optional +from typing import Any, AsyncGenerator, Dict, Optional -from graphql import ExecutionResult as GraphQLExecutionResult -from graphql import GraphQLError, GraphQLSyntaxError, parse -from graphql.error.graphql_error import format_error as format_graphql_error +from graphql import ExecutionResult as GraphQLExecutionResult, GraphQLError +from graphql.error import format_error as format_graphql_error from strawberry.schema import BaseSchema from strawberry.subscriptions.protocols.graphql_transport_ws.types import ( @@ -21,10 +20,7 @@ SubscribeMessage, SubscribeMessagePayload, ) -from strawberry.types.graphql import OperationType -from strawberry.unset import UNSET from strawberry.utils.debug import pretty_print_graphql_operation -from strawberry.utils.operation import get_operation_type class BaseGraphQLTransportWSHandler(ABC): @@ -42,8 +38,6 @@ def __init__( self.connection_acknowledged = False self.subscriptions: Dict[str, AsyncGenerator] = {} self.tasks: Dict[str, asyncio.Task] = {} - self.completed_tasks: List[asyncio.Task] = [] - self.connection_params: Optional[Dict[str, Any]] = None @abstractmethod async def get_context(self) -> Any: @@ -81,50 +75,36 @@ async def handle_connection_init_timeout(self): await self.close(code=4408, reason=reason) async def handle_message(self, message: dict): - handler: Callable - handler_arg: Any try: message_type = message.pop("type") if message_type == ConnectionInitMessage.type: - handler = self.handle_connection_init - handler_arg = ConnectionInitMessage(**message) + await self.handle_connection_init(ConnectionInitMessage(**message)) elif message_type == PingMessage.type: - handler = self.handle_ping - handler_arg = PingMessage(**message) + await self.handle_ping(PingMessage(**message)) elif message_type == PongMessage.type: - handler = self.handle_pong - handler_arg = PongMessage(**message) + await self.handle_pong(PongMessage(**message)) elif message_type == SubscribeMessage.type: - handler = self.handle_subscribe payload = SubscribeMessagePayload(**message.pop("payload")) - handler_arg = SubscribeMessage(payload=payload, **message) + await self.handle_subscribe( + SubscribeMessage(payload=payload, **message) + ) elif message_type == CompleteMessage.type: - handler = self.handle_complete - handler_arg = CompleteMessage(**message) + await self.handle_complete(CompleteMessage(**message)) else: - handler = self.handle_invalid_message - handler_arg = f"Unknown message type: {message_type}" + error_message = f"Unknown message type: {message_type}" + await self.handle_invalid_message(error_message) except (KeyError, TypeError): - handler = self.handle_invalid_message - handler_arg = "Failed to parse message" - - await handler(handler_arg) - await self.reap_completed_tasks() + error_message = "Failed to parse message" + await self.handle_invalid_message(error_message) async def handle_connection_init(self, message: ConnectionInitMessage) -> None: - if message.payload is not UNSET and not isinstance(message.payload, dict): - await self.close(code=4400, reason="Invalid connection init payload") - return - - self.connection_params = message.payload - if self.connection_init_received: reason = "Too many initialisation requests" await self.close(code=4429, reason=reason) @@ -145,25 +125,14 @@ async def handle_subscribe(self, message: SubscribeMessage) -> None: await self.close(code=4401, reason="Unauthorized") return - try: - graphql_document = parse(message.payload.query) - except GraphQLSyntaxError as exc: - await self.close(code=4400, reason=exc.message) - return - - try: - operation_type = get_operation_type( - graphql_document, message.payload.operationName - ) - except RuntimeError: - await self.close(code=4400, reason="Can't get GraphQL operation type") - return - - if message.id in self.subscriptions: + if message.id in self.subscriptions.keys(): reason = f"Subscriber for {message.id} already exists" await self.close(code=4409, reason=reason) return + context = await self.get_context() + root_value = await self.get_root_value() + if self.debug: # pragma: no cover pretty_print_graphql_operation( message.payload.operationName, @@ -171,13 +140,7 @@ async def handle_subscribe(self, message: SubscribeMessage) -> None: message.payload.variables, ) - context = await self.get_context() - if isinstance(context, dict): - context["connection_params"] = self.connection_params - root_value = await self.get_root_value() - - # Get an AsyncGenerator yielding the results - if operation_type == OperationType.SUBSCRIPTION: + try: result_source = await self.schema.subscribe( query=message.payload.query, variable_values=message.payload.variables, @@ -185,20 +148,12 @@ async def handle_subscribe(self, message: SubscribeMessage) -> None: context_value=context, root_value=root_value, ) - else: - # create AsyncGenerator returning a single result - async def get_result_source(): - yield await self.schema.execute( - query=message.payload.query, - variable_values=message.payload.variables, - context_value=context, - root_value=root_value, - operation_name=message.payload.operationName, - ) - - result_source = get_result_source() + except GraphQLError as error: + payload = [format_graphql_error(error)] + await self.send_message(ErrorMessage(id=message.id, payload=payload)) + self.schema.process_errors([error]) + return - # Handle initial validation errors if isinstance(result_source, GraphQLExecutionResult): assert result_source.errors payload = [format_graphql_error(result_source.errors[0])] @@ -206,42 +161,9 @@ async def get_result_source(): self.schema.process_errors(result_source.errors) return - # Create task to handle this subscription, reserve the operation ID + handler = self.handle_async_results(result_source, message.id) self.subscriptions[message.id] = result_source - self.tasks[message.id] = asyncio.create_task( - self.operation_task(result_source, message.id) - ) - - async def operation_task( - self, result_source: AsyncGenerator, operation_id: str - ) -> None: - """ - Operation task top level method. Cleans up and de-registers the operation - once it is done. - """ - try: - await self.handle_async_results(result_source, operation_id) - except BaseException: # pragma: no cover - # cleanup in case of something really unexpected - # wait for generator to be closed to ensure that any existing - # 'finally' statement is called - result_source = self.subscriptions[operation_id] - with suppress(RuntimeError): - await result_source.aclose() - del self.subscriptions[operation_id] - del self.tasks[operation_id] - raise - else: - # de-register the operation _before_ sending the `Complete` message - # to make the `operation_id` immediately available for re-use - del self.subscriptions[operation_id] - del self.tasks[operation_id] - await self.send_message(CompleteMessage(id=operation_id)) - finally: - # add this task to a list to be reaped later - task = asyncio.current_task() - assert task is not None - self.completed_tasks.append(task) + self.tasks[message.id] = asyncio.create_task(handler) async def handle_async_results( self, @@ -273,6 +195,8 @@ async def handle_async_results( self.schema.process_errors([error]) return + await self.send_message(CompleteMessage(id=operation_id)) + async def handle_complete(self, message: CompleteMessage) -> None: await self.cleanup_operation(operation_id=message.id) @@ -284,22 +208,10 @@ async def send_message(self, message: GraphQLTransportMessage) -> None: await self.send_json(data) async def cleanup_operation(self, operation_id: str) -> None: - if operation_id not in self.subscriptions: - return - result_source = self.subscriptions.pop(operation_id) - task = self.tasks.pop(operation_id) - task.cancel() + await self.subscriptions[operation_id].aclose() + del self.subscriptions[operation_id] + + self.tasks[operation_id].cancel() with suppress(BaseException): - await task - # since python 3.8, generators cannot be reliably closed - with suppress(RuntimeError): - await result_source.aclose() - - async def reap_completed_tasks(self) -> None: - """ - Await tasks that have completed - """ - tasks, self.completed_tasks = self.completed_tasks, [] - for task in tasks: - with suppress(BaseException): - await task + await self.tasks[operation_id] + del self.tasks[operation_id] diff --git a/strawberry/subscriptions/protocols/graphql_transport_ws/types.py b/strawberry/subscriptions/protocols/graphql_transport_ws/types.py index 72033f7ff4..1658d41e68 100644 --- a/strawberry/subscriptions/protocols/graphql_transport_ws/types.py +++ b/strawberry/subscriptions/protocols/graphql_transport_ws/types.py @@ -1,9 +1,7 @@ from dataclasses import asdict, dataclass from typing import Any, Dict, List, Optional -from graphql import GraphQLFormattedError - -from strawberry.unset import UNSET +from strawberry.arguments import UNSET @dataclass @@ -85,9 +83,6 @@ class NextMessage(GraphQLTransportMessage): payload: Dict[str, Any] # TODO: shape like ExecutionResult type: str = "next" - def as_dict(self) -> dict: - return {"id": self.id, "payload": self.payload, "type": self.type} - @dataclass class ErrorMessage(GraphQLTransportMessage): @@ -96,7 +91,7 @@ class ErrorMessage(GraphQLTransportMessage): """ id: str - payload: List[GraphQLFormattedError] + payload: List[Dict[str, Any]] # TODO: shape like List[GraphQLError] type: str = "error" diff --git a/strawberry/subscriptions/protocols/graphql_ws/handlers.py b/strawberry/subscriptions/protocols/graphql_ws/handlers.py index 06d8bc61f4..8fc829aeb4 100644 --- a/strawberry/subscriptions/protocols/graphql_ws/handlers.py +++ b/strawberry/subscriptions/protocols/graphql_ws/handlers.py @@ -3,15 +3,13 @@ from contextlib import suppress from typing import Any, AsyncGenerator, Dict, Optional, cast -from graphql import ExecutionResult as GraphQLExecutionResult -from graphql import GraphQLError -from graphql.error.graphql_error import format_error as format_graphql_error +from graphql import ExecutionResult as GraphQLExecutionResult, GraphQLError +from graphql.error import format_error as format_graphql_error from strawberry.schema import BaseSchema from strawberry.subscriptions.protocols.graphql_ws import ( GQL_COMPLETE, GQL_CONNECTION_ACK, - GQL_CONNECTION_ERROR, GQL_CONNECTION_INIT, GQL_CONNECTION_KEEP_ALIVE, GQL_CONNECTION_TERMINATE, @@ -21,7 +19,6 @@ GQL_STOP, ) from strawberry.subscriptions.protocols.graphql_ws.types import ( - ConnectionInitPayload, OperationMessage, OperationMessagePayload, StartPayload, @@ -44,7 +41,6 @@ def __init__( self.keep_alive_task: Optional[asyncio.Task] = None self.subscriptions: Dict[str, AsyncGenerator] = {} self.tasks: Dict[str, asyncio.Task] = {} - self.connection_params: Optional[ConnectionInitPayload] = None @abstractmethod async def get_context(self) -> Any: @@ -85,18 +81,8 @@ async def handle_message( await self.handle_stop(message) async def handle_connection_init(self, message: OperationMessage) -> None: - payload = message.get("payload") - if payload is not None and not isinstance(payload, dict): - error_message: OperationMessage = {"type": GQL_CONNECTION_ERROR} - await self.send_json(error_message) - await self.close() - return - - payload = cast(Optional[ConnectionInitPayload], payload) - self.connection_params = payload - - acknowledge_message: OperationMessage = {"type": GQL_CONNECTION_ACK} - await self.send_json(acknowledge_message) + data: OperationMessage = {"type": GQL_CONNECTION_ACK} + await self.send_json(data) if self.keep_alive: keep_alive_handler = self.handle_keep_alive() @@ -113,8 +99,6 @@ async def handle_start(self, message: OperationMessage) -> None: variables = payload.get("variables") context = await self.get_context() - if isinstance(context, dict): - context["connection_params"] = self.connection_params root_value = await self.get_root_value() if self.debug: diff --git a/strawberry/subscriptions/protocols/graphql_ws/types.py b/strawberry/subscriptions/protocols/graphql_ws/types.py index abd0ce785e..3fde135a7e 100644 --- a/strawberry/subscriptions/protocols/graphql_ws/types.py +++ b/strawberry/subscriptions/protocols/graphql_ws/types.py @@ -1,7 +1,7 @@ from typing import Any, Dict, List, Optional, Union + from typing_extensions import TypedDict -from graphql import GraphQLFormattedError ConnectionInitPayload = Dict[str, Any] @@ -19,10 +19,14 @@ class DataPayload(TypedDict, total=False): data: Any # Optional list of formatted graphql.GraphQLError objects - errors: Optional[List[GraphQLFormattedError]] + errors: Optional[List[Dict[str, Any]]] + +class ErrorPayload(TypedDict): + id: str -ErrorPayload = GraphQLFormattedError + # Formatted graphql.GraphQLError object + payload: Dict[str, Any] OperationMessagePayload = Union[ diff --git a/strawberry/test/__init__.py b/strawberry/test/__init__.py index 5f6b0b76e5..ad9617f32d 100644 --- a/strawberry/test/__init__.py +++ b/strawberry/test/__init__.py @@ -1,3 +1,4 @@ from .client import BaseGraphQLTestClient, Body, Response + __all__ = ["Body", "Response", "BaseGraphQLTestClient"] diff --git a/strawberry/test/client.py b/strawberry/test/client.py index db26aea2aa..822ebf38f0 100644 --- a/strawberry/test/client.py +++ b/strawberry/test/client.py @@ -1,15 +1,14 @@ import json from abc import ABC, abstractmethod from dataclasses import dataclass -from typing import Any, Coroutine, Dict, List, Mapping, Optional, Union -from typing_extensions import Literal, TypedDict +from typing import Dict, List, Mapping, Optional -from graphql import GraphQLFormattedError +from typing_extensions import Literal, TypedDict @dataclass class Response: - errors: Optional[List[GraphQLFormattedError]] + errors: Optional[Dict[str, object]] data: Optional[Dict[str, object]] extensions: Optional[Dict[str, object]] @@ -20,9 +19,8 @@ class Body(TypedDict, total=False): class BaseGraphQLTestClient(ABC): - def __init__(self, client, url: str = "/graphql/"): + def __init__(self, client): self._client = client - self.url = url def query( self, @@ -31,7 +29,7 @@ def query( headers: Optional[Dict[str, object]] = None, asserts_errors: Optional[bool] = True, files: Optional[Dict[str, object]] = None, - ) -> Union[Coroutine[Any, Any, Response], Response]: + ) -> Response: body = self._build_body(query, variables, files) resp = self.request(body, headers, files) @@ -151,11 +149,7 @@ def _build_multipart_file_map( else: map[key] = [f"variables.{reference}"] - # Variables can be mixed files and other data, we don't want to map non-files - # vars so we need to remove them, we can't remove them before - # because they can be part of a list of files or folder - map_without_vars = {k: v for k, v in map.items() if k in files} - return map_without_vars + return map def _decode(self, response, type: Literal["multipart", "json"]): if type == "multipart": diff --git a/strawberry/tools/__init__.py b/strawberry/tools/__init__.py index b6be7a91dc..b07f442870 100644 --- a/strawberry/tools/__init__.py +++ b/strawberry/tools/__init__.py @@ -1,6 +1,7 @@ from .create_type import create_type from .merge_types import merge_types + __all__ = [ "create_type", "merge_types", diff --git a/strawberry/tools/create_type.py b/strawberry/tools/create_type.py index 9b590eac2d..482b0a9c7e 100644 --- a/strawberry/tools/create_type.py +++ b/strawberry/tools/create_type.py @@ -27,9 +27,11 @@ def create_type(name: str, fields: List[StrawberryField]) -> Type: if field.python_name is None: raise ValueError( - "Field doesn't have a name. Fields passed to " - "`create_type` must define a name by passing the " - "`name` argument to `strawberry.field`." + ( + "Field doesn't have a name. Fields passed to " + "`create_type` must define a name by passing the " + "`name` argument to `strawberry.field`." + ) ) namespace[field.python_name] = field diff --git a/strawberry/type.py b/strawberry/type.py index 8cc3e236d5..4499f2bf11 100644 --- a/strawberry/type.py +++ b/strawberry/type.py @@ -3,6 +3,7 @@ from abc import ABC, abstractmethod from typing import TYPE_CHECKING, List, Mapping, TypeVar, Union + if TYPE_CHECKING: from .types.types import TypeDefinition @@ -23,9 +24,6 @@ def copy_with( def is_generic(self) -> bool: raise NotImplementedError() - def has_generic(self, type_var) -> bool: - return False - def __eq__(self, other: object) -> bool: from strawberry.annotation import StrawberryAnnotation @@ -53,9 +51,6 @@ class StrawberryContainer(StrawberryType): def __init__(self, of_type: Union[StrawberryType, type]): self.of_type = of_type - def __hash__(self) -> int: - return hash((self.__class__, self.of_type)) - def __eq__(self, other: object) -> bool: if isinstance(other, StrawberryType): if isinstance(other, StrawberryContainer): @@ -81,11 +76,13 @@ def type_params(self) -> List[TypeVar]: def copy_with( self, type_var_map: Mapping[TypeVar, Union[StrawberryType, type]] ) -> StrawberryType: - of_type_copy: Union[StrawberryType, type] = self.of_type + of_type_copy: Union[StrawberryType, type] # TODO: Obsolete with StrawberryObject if hasattr(self.of_type, "_type_definition"): - type_definition: TypeDefinition = self.of_type._type_definition + type_definition: TypeDefinition = ( + self.of_type._type_definition # type: ignore + ) if type_definition.is_generic: of_type_copy = type_definition.copy_with(type_var_map) @@ -93,6 +90,8 @@ def copy_with( elif isinstance(self.of_type, StrawberryType) and self.of_type.is_generic: of_type_copy = self.of_type.copy_with(type_var_map) + assert of_type_copy + return type(self)(of_type_copy) @property @@ -100,18 +99,13 @@ def is_generic(self) -> bool: # TODO: Obsolete with StrawberryObject type_ = self.of_type if hasattr(self.of_type, "_type_definition"): - type_ = self.of_type._type_definition + type_ = self.of_type._type_definition # type: ignore if isinstance(type_, StrawberryType): return type_.is_generic return False - def has_generic(self, type_var) -> bool: - if isinstance(self.of_type, StrawberryType): - return self.of_type.has_generic(type_var) - return False - class StrawberryList(StrawberryContainer): ... @@ -125,7 +119,7 @@ class StrawberryTypeVar(StrawberryType): def __init__(self, type_var: TypeVar): self.type_var = type_var - def copy_with( + def copy_with( # type: ignore[override] self, type_var_map: Mapping[TypeVar, Union[StrawberryType, type]] ) -> Union[StrawberryType, type]: return type_var_map[self.type_var] @@ -134,9 +128,6 @@ def copy_with( def is_generic(self) -> bool: return True - def has_generic(self, type_var) -> bool: - return self.type_var == type_var - @property def type_params(self) -> List[TypeVar]: return [self.type_var] diff --git a/strawberry/types/__init__.py b/strawberry/types/__init__.py index 0187701c58..d13246fddc 100644 --- a/strawberry/types/__init__.py +++ b/strawberry/types/__init__.py @@ -1,4 +1,5 @@ from .execution import ExecutionContext, ExecutionResult from .info import Info + __all__ = ["ExecutionContext", "ExecutionResult", "Info"] diff --git a/strawberry/types/execution.py b/strawberry/types/execution.py index 92526d5cc4..09a1864c2e 100644 --- a/strawberry/types/execution.py +++ b/strawberry/types/execution.py @@ -1,22 +1,27 @@ import dataclasses -from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Type +from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Type, cast -from graphql import ASTValidationRule, specified_rules -from graphql import ExecutionResult as GraphQLExecutionResult +from typing_extensions import Literal + +from graphql import ( + ASTValidationRule, + ExecutionResult as GraphQLExecutionResult, + specified_rules, +) from graphql.error.graphql_error import GraphQLError from graphql.language import DocumentNode, OperationDefinitionNode -from strawberry.utils.operation import get_first_operation, get_operation_type - -from .graphql import OperationType if TYPE_CHECKING: from strawberry.schema import Schema +GraphqlOperationTypes = Literal["QUERY", "MUTATION"] + + @dataclasses.dataclass class ExecutionContext: - query: Optional[str] + query: str schema: "Schema" context: Any = None variables: Optional[Dict[str, Any]] = None @@ -52,19 +57,41 @@ def operation_name(self) -> Optional[str]: return definition.name.value @property - def operation_type(self) -> OperationType: + def operation_type(self) -> GraphqlOperationTypes: + definition: Optional[OperationDefinitionNode] = None + graphql_document = self.graphql_document if not graphql_document: raise RuntimeError("No GraphQL document available") - return get_operation_type(graphql_document, self.operation_name) + # If no operation_name has been specified then use the first + # OperationDefinitionNode + if not self._provided_operation_name: + definition = self._get_first_operation() + else: + for d in graphql_document.definitions: + d = cast(OperationDefinitionNode, d) + if d.name and d.name.value == self._provided_operation_name: + definition = d + break + + if not definition: + raise RuntimeError("Can't get GraphQL operation type") + + return cast(GraphqlOperationTypes, definition.operation.name) def _get_first_operation(self) -> Optional[OperationDefinitionNode]: graphql_document = self.graphql_document if not graphql_document: return None - return get_first_operation(graphql_document) + definition: Optional[OperationDefinitionNode] = None + for d in graphql_document.definitions: + if isinstance(d, OperationDefinitionNode): + definition = d + break + + return definition @dataclasses.dataclass diff --git a/strawberry/types/fields/resolver.py b/strawberry/types/fields/resolver.py index fa66ffc47f..0409233f7c 100644 --- a/strawberry/types/fields/resolver.py +++ b/strawberry/types/fields/resolver.py @@ -3,161 +3,24 @@ import builtins import inspect import sys -import warnings from inspect import isasyncgenfunction, iscoroutinefunction -from typing import ( # type: ignore[attr-defined] - Any, - Callable, - Dict, - ForwardRef, - Generic, - List, - Mapping, - NamedTuple, - Optional, - Tuple, - Type, - TypeVar, - Union, - _eval_type, - cast, -) -from typing_extensions import Annotated, Protocol, get_args, get_origin +from typing import Callable, Dict, Generic, List, Mapping, Optional, TypeVar, Union + +from backports.cached_property import cached_property from strawberry.annotation import StrawberryAnnotation from strawberry.arguments import StrawberryArgument from strawberry.exceptions import MissingArgumentsAnnotationsError from strawberry.type import StrawberryType -from strawberry.types.info import Info -from strawberry.utils.cached_property import cached_property - - -class Parameter(inspect.Parameter): - def __hash__(self): - """Override to exclude default value from hash. - - This adds compatibility for using unhashable default values in resolvers such as - list and dict. The present use-case is limited to analyzing parameters from one - resolver. Therefore, the name, kind, and annotation combination are guaranteed - to be unique since two arguments cannot have the same name in a callable. - - Furthermore, even though it is not currently a use-case to collect parameters - from different resolvers, the likelihood of collision from having the same hash - value but different defaults is mitigated by Python invoking the - :py:meth:`__eq__` method if two items have the same hash. See the verification - of this behavior in the `test_parameter_hash_collision` test. - """ - return hash((self.name, self.kind, self.annotation)) - - -class Signature(inspect.Signature): - - _parameter_cls = Parameter - - -class ReservedParameterSpecification(Protocol): - def find( - self, parameters: Tuple[inspect.Parameter, ...], resolver: StrawberryResolver - ) -> Optional[inspect.Parameter]: - """Finds the reserved parameter from ``parameters``.""" - - -class ReservedName(NamedTuple): - name: str - - def find( - self, parameters: Tuple[inspect.Parameter, ...], _: StrawberryResolver - ) -> Optional[inspect.Parameter]: - return next((p for p in parameters if p.name == self.name), None) - - -class ReservedNameBoundParameter(NamedTuple): - name: str - - def find( - self, parameters: Tuple[inspect.Parameter, ...], _: StrawberryResolver - ) -> Optional[inspect.Parameter]: - if parameters: # Add compatibility for resolvers with no arguments - first_parameter = parameters[0] - return first_parameter if first_parameter.name == self.name else None - else: - return None +from strawberry.utils.inspect import get_func_args -class ReservedType(NamedTuple): - """Define a reserved type by name or by type. - - To preserve backwards-comaptibility, if an annotation was defined but does not match - :attr:`type`, then the name is used as a fallback. - """ - - name: str - type: Type - - def find( - self, parameters: Tuple[inspect.Parameter, ...], resolver: StrawberryResolver - ) -> Optional[inspect.Parameter]: - for parameter in parameters: - annotation = parameter.annotation - try: - resolved_annotation = _eval_type( - ForwardRef(annotation) - if isinstance(annotation, str) - else annotation, - resolver._namespace, - None, - ) - resolver._resolved_annotations[parameter] = resolved_annotation - except NameError: - # Type-annotation could not be resolved - resolved_annotation = annotation - if self.is_reserved_type(resolved_annotation): - return parameter - - # Fallback to matching by name - reserved_name = ReservedName(name=self.name).find(parameters, resolver) - if reserved_name: - warning = DeprecationWarning( - f"Argument name-based matching of '{self.name}' is deprecated and will " - "be removed in v1.0. Ensure that reserved arguments are annotated " - "their respective types (i.e. use value: 'DirectiveValue[str]' instead " - "of 'value: str' and 'info: Info' instead of a plain 'info')." - ) - warnings.warn(warning) - return reserved_name - else: - return None - - def is_reserved_type(self, other: Type) -> bool: - origin = cast(type, get_origin(other)) or other - if origin is Annotated: - # Handle annotated arguments such as Private[str] and DirectiveValue[str] - return any(isinstance(argument, self.type) for argument in get_args(other)) - else: - # Handle both concrete and generic types (i.e Info, and Info[Any, Any]) - return ( - issubclass(origin, self.type) - if isinstance(origin, type) - else origin is self.type - ) - - -SELF_PARAMSPEC = ReservedNameBoundParameter("self") -CLS_PARAMSPEC = ReservedNameBoundParameter("cls") -ROOT_PARAMSPEC = ReservedName("root") -INFO_PARAMSPEC = ReservedType("info", Info) - T = TypeVar("T") class StrawberryResolver(Generic[T]): - - RESERVED_PARAMSPEC: Tuple[ReservedParameterSpecification, ...] = ( - SELF_PARAMSPEC, - CLS_PARAMSPEC, - ROOT_PARAMSPEC, - INFO_PARAMSPEC, - ) + # TODO: Move to StrawberryArgument? StrawberryResolver ClassVar? + _SPECIAL_ARGS = {"root", "info", "self", "cls"} def __init__( self, @@ -173,11 +36,6 @@ def __init__( This is used when creating copies of types w/ generics """ - self._resolved_annotations: Dict[inspect.Parameter, Any] = {} - """Populated during reserved parameter determination. - - Caching resolved annotations this way prevents evaling them repeatedly. - """ # TODO: Use this when doing the actual resolving? How to deal with async resolvers? def __call__(self, *args, **kwargs) -> T: @@ -186,90 +44,89 @@ def __call__(self, *args, **kwargs) -> T: return self.wrapped_func(*args, **kwargs) @cached_property - def signature(self) -> inspect.Signature: - return Signature.from_callable(self._unbound_wrapped_func, follow_wrapped=True) + def annotations(self) -> Dict[str, object]: + """Annotations for the resolver. - @cached_property - def reserved_parameters( - self, - ) -> Dict[ReservedParameterSpecification, Optional[inspect.Parameter]]: - """Mapping of reserved parameter specification to parameter.""" - parameters = tuple(self.signature.parameters.values()) - return {spec: spec.find(parameters, self) for spec in self.RESERVED_PARAMSPEC} + Does not include special args defined in _SPECIAL_ARGS (e.g. self, root, info) + """ + annotations = self._unbound_wrapped_func.__annotations__ + + annotations = { + name: annotation + for name, annotation in annotations.items() + if name not in self._SPECIAL_ARGS + } + + return annotations @cached_property def arguments(self) -> List[StrawberryArgument]: - """Resolver arguments exposed in the GraphQL Schema.""" - parameters = self.signature.parameters.values() - reserved_parameters = set(self.reserved_parameters.values()) - - missing_annotations = [] - arguments = [] - user_parameters = (p for p in parameters if p not in reserved_parameters) - - for param in user_parameters: - annotation = self._resolved_annotations.get(param, param.annotation) - if annotation is inspect.Signature.empty: - missing_annotations.append(param.name) - else: - argument = StrawberryArgument( - python_name=param.name, - graphql_name=None, - type_annotation=StrawberryAnnotation( - annotation=annotation, namespace=self._namespace - ), - default=param.default, - ) - arguments.append(argument) - if missing_annotations: - raise MissingArgumentsAnnotationsError(self, missing_annotations) - return arguments + parameters = inspect.signature(self._unbound_wrapped_func).parameters + function_arguments = set(parameters) - self._SPECIAL_ARGS + + arguments = self.annotations.copy() + arguments.pop("return", None) # Discard return annotation to get just arguments + + arguments_missing_annotations = function_arguments - set(arguments) + + if any(arguments_missing_annotations): + raise MissingArgumentsAnnotationsError( + field_name=self.name, + arguments=arguments_missing_annotations, + ) + + module = sys.modules[self._module] + annotation_namespace = module.__dict__ + strawberry_arguments = [] + for arg_name, annotation in arguments.items(): + parameter = parameters[arg_name] + + argument = StrawberryArgument( + python_name=arg_name, + graphql_name=None, + type_annotation=StrawberryAnnotation( + annotation=annotation, namespace=annotation_namespace + ), + default=parameter.default, + ) + + strawberry_arguments.append(argument) + + return strawberry_arguments @cached_property - def info_parameter(self) -> Optional[inspect.Parameter]: - return self.reserved_parameters.get(INFO_PARAMSPEC) + def has_info_arg(self) -> bool: + args = get_func_args(self._unbound_wrapped_func) + return "info" in args @cached_property - def root_parameter(self) -> Optional[inspect.Parameter]: - return self.reserved_parameters.get(ROOT_PARAMSPEC) + def has_root_arg(self) -> bool: + args = get_func_args(self._unbound_wrapped_func) + return "root" in args @cached_property - def self_parameter(self) -> Optional[inspect.Parameter]: - return self.reserved_parameters.get(SELF_PARAMSPEC) + def has_self_arg(self) -> bool: + args = get_func_args(self._unbound_wrapped_func) + return args and args[0] == "self" @cached_property def name(self) -> str: # TODO: What to do if resolver is a lambda? return self._unbound_wrapped_func.__name__ - @cached_property - def annotations(self) -> Dict[str, object]: - """Annotations for the resolver. - - Does not include special args defined in `RESERVED_PARAMSPEC` (e.g. self, root, - info) - """ - reserved_parameters = self.reserved_parameters - reserved_names = {p.name for p in reserved_parameters.values() if p is not None} - - annotations = self._unbound_wrapped_func.__annotations__ - annotations = { - name: annotation - for name, annotation in annotations.items() - if name not in reserved_names - } - - return annotations - @cached_property def type_annotation(self) -> Optional[StrawberryAnnotation]: - return_annotation = self.signature.return_annotation - if return_annotation is inspect.Signature.empty: + try: + return_annotation = self.annotations["return"] + except KeyError: + # No return annotation at all (as opposed to `-> None`) return None - else: - type_annotation = StrawberryAnnotation( - annotation=return_annotation, namespace=self._namespace - ) + + module = sys.modules[self._module] + type_annotation = StrawberryAnnotation( + annotation=return_annotation, namespace=module.__dict__ + ) + return type_annotation @property @@ -294,28 +151,20 @@ def copy_with( if self.type: if isinstance(self.type, StrawberryType): type_override = self.type.copy_with(type_var_map) - elif hasattr(self.type, "_type_definition"): - type_override = self.type._type_definition.copy_with( + else: + type_override = self.type._type_definition.copy_with( # type: ignore type_var_map, ) - other = type(self)( + return type(self)( func=self.wrapped_func, description=self._description, type_override=type_override, ) - # Resolve generic arguments - for argument in other.arguments: - if isinstance(argument.type, StrawberryType) and argument.type.is_generic: - argument.type_annotation = StrawberryAnnotation( - annotation=argument.type.copy_with(type_var_map), - namespace=argument.type_annotation.namespace, - ) - return other @cached_property - def _namespace(self) -> Dict[str, Any]: - return sys.modules[self._unbound_wrapped_func.__module__].__dict__ + def _module(self) -> str: + return self._unbound_wrapped_func.__module__ @cached_property def _unbound_wrapped_func(self) -> Callable[..., T]: diff --git a/strawberry/types/graphql.py b/strawberry/types/graphql.py deleted file mode 100644 index a559601bf8..0000000000 --- a/strawberry/types/graphql.py +++ /dev/null @@ -1,24 +0,0 @@ -from __future__ import annotations - -import enum -from typing import Set - - -class OperationType(enum.Enum): - QUERY = "query" - MUTATION = "mutation" - SUBSCRIPTION = "subscription" - - @staticmethod - def from_http(method: str) -> Set[OperationType]: - if method == "GET": - return {OperationType.QUERY} - - if method == "POST": - return { - OperationType.QUERY, - OperationType.MUTATION, - OperationType.SUBSCRIPTION, - } - - raise ValueError(f"Unsupported HTTP method: {method}") # pragma: no cover diff --git a/strawberry/types/info.py b/strawberry/types/info.py index bf46ef74fa..3c8d3840b6 100644 --- a/strawberry/types/info.py +++ b/strawberry/types/info.py @@ -2,12 +2,14 @@ import warnings from typing import TYPE_CHECKING, Any, Dict, Generic, List, Optional, TypeVar, Union +from backports.cached_property import cached_property + from graphql import GraphQLResolveInfo, OperationDefinitionNode from graphql.language import FieldNode from graphql.pyutils.path import Path from strawberry.type import StrawberryType -from strawberry.utils.cached_property import cached_property + if TYPE_CHECKING: from strawberry.field import StrawberryField @@ -15,6 +17,7 @@ from .nodes import Selection, convert_selections + ContextType = TypeVar("ContextType") RootValueType = TypeVar("RootValueType") @@ -38,7 +41,6 @@ def field_nodes(self) -> List[FieldNode]: # deprecated "`info.field_nodes` is deprecated, use `selected_fields` instead", DeprecationWarning, ) - return self._raw_info.field_nodes @cached_property diff --git a/strawberry/types/nodes.py b/strawberry/types/nodes.py index 1f6bba3ad3..3bf9ae38a2 100644 --- a/strawberry/types/nodes.py +++ b/strawberry/types/nodes.py @@ -1,8 +1,7 @@ """ Abstraction layer for graphql-core field nodes. -Call `convert_sections` on a list of GraphQL `FieldNode`s, -such as in `info.field_nodes`. +Call `convert_sections` on a list of GraphQL `FieldNode`s, such as in `info.field_nodes`. If a node has only one useful value, it's value is inlined. @@ -11,19 +10,22 @@ """ import dataclasses -from typing import Any, Collection, Dict, Iterable, List, Optional, Union +from typing import Any, Dict, Iterable, List, Optional, Union from graphql import GraphQLResolveInfo -from graphql.language import ArgumentNode as GQLArgumentNode -from graphql.language import DirectiveNode as GQLDirectiveNode -from graphql.language import FieldNode as GQLFieldNode -from graphql.language import FragmentSpreadNode as GQLFragmentSpreadNode -from graphql.language import InlineFragmentNode as GQLInlineFragment -from graphql.language import InlineFragmentNode as GQLInlineFragmentNode -from graphql.language import ListValueNode as GQLListValueNode -from graphql.language import ObjectValueNode as GQLObjectValueNode -from graphql.language import ValueNode as GQLValueNode -from graphql.language import VariableNode as GQLVariableNode +from graphql.language import ( + ArgumentNode as GQLArgumentNode, + DirectiveNode as GQLDirectiveNode, + FieldNode as GQLFieldNode, + FragmentSpreadNode as GQLFragmentSpreadNode, + InlineFragmentNode as GQLInlineFragment, + InlineFragmentNode as GQLInlineFragmentNode, + ListValueNode as GQLListValueNode, + ObjectValueNode as GQLObjectValueNode, + ValueNode as GQLValueNode, + VariableNode as GQLVariableNode, +) + Arguments = Dict[str, Any] Directives = Dict[str, Arguments] @@ -60,7 +62,7 @@ def convert_directives( def convert_selections( - info: GraphQLResolveInfo, field_nodes: Collection[GQLFieldNode] + info: GraphQLResolveInfo, field_nodes: List[GQLFieldNode] ) -> List[Selection]: """Return typed `Selection` based on node type.""" selections: List[Selection] = [] diff --git a/strawberry/types/type_resolver.py b/strawberry/types/type_resolver.py index 124381b63c..332a5154b9 100644 --- a/strawberry/types/type_resolver.py +++ b/strawberry/types/type_resolver.py @@ -10,7 +10,8 @@ ) from strawberry.field import StrawberryField from strawberry.private import is_private -from strawberry.unset import UNSET + +from ..arguments import UNSET def _get_fields(cls: Type) -> List[StrawberryField]: @@ -58,7 +59,7 @@ class if one is not set by either using an explicit strawberry.field(name=...) o base_fields = { field.python_name: field # TODO: we need to rename _fields to something else - for field in base._type_definition._fields + for field in base._type_definition._fields # type: ignore } # Add base's fields to cls' fields @@ -71,7 +72,7 @@ class if one is not set by either using an explicit strawberry.field(name=...) o for base in cls.__mro__: if hasattr(base, "_type_definition"): - for field in base._type_definition._fields: + for field in base._type_definition._fields: # type: ignore if field.python_name in base.__annotations__: origins.setdefault(field.name, base) @@ -81,12 +82,11 @@ class if one is not set by either using an explicit strawberry.field(name=...) o if isinstance(field, StrawberryField): # Check that the field type is not Private if is_private(field.type): - raise PrivateStrawberryFieldError(field.python_name, cls) + raise PrivateStrawberryFieldError(field.python_name, cls.__name__) # Check that default is not set if a resolver is defined if ( field.default is not dataclasses.MISSING - and field.default is not UNSET and field.base_resolver is not None ): raise FieldWithResolverAndDefaultValueError( @@ -96,10 +96,8 @@ class if one is not set by either using an explicit strawberry.field(name=...) o # Check that default_factory is not set if a resolver is defined # Note: using getattr because of this issue: # https://github.com/python/mypy/issues/6910 - default_factory = getattr(field, "default_factory", None) if ( - default_factory is not dataclasses.MISSING - and default_factory is not UNSET + getattr(field, "default_factory") is not dataclasses.MISSING # noqa and field.base_resolver is not None ): raise FieldWithResolverAndDefaultFactoryError( @@ -117,15 +115,12 @@ class if one is not set by either using an explicit strawberry.field(name=...) o # the types. field.origin = field.origin or cls - # Set the correct namespace for annotations if a namespace isn't - # already set - # Note: We do this here rather in the `Strawberry.type` setter - # function because at that point we don't have a link to the object - # type that the field as attached to. - if isinstance(field.type_annotation, StrawberryAnnotation): - type_annotation = field.type_annotation - if type_annotation.namespace is None: - type_annotation.set_namespace_from_field(field) + # Make sure types are StrawberryAnnotations + if not isinstance(field.type_annotation, StrawberryAnnotation): + module = sys.modules[field.origin.__module__] + field.type_annotation = StrawberryAnnotation( + annotation=field.type_annotation, namespace=module.__dict__ + ) # Create a StrawberryField for fields that didn't use strawberry.field else: @@ -147,7 +142,7 @@ class if one is not set by either using an explicit strawberry.field(name=...) o namespace=module.__dict__, ), origin=origin, - default=getattr(cls, field.name, dataclasses.MISSING), + default=getattr(cls, field.name, UNSET), ) field_name = field.python_name diff --git a/strawberry/types/types.py b/strawberry/types/types.py index a152be5931..f7be942d87 100644 --- a/strawberry/types/types.py +++ b/strawberry/types/types.py @@ -13,15 +13,16 @@ TypeVar, Union, ) -from typing_extensions import Self from strawberry.type import StrawberryType, StrawberryTypeVar from strawberry.utils.typing import is_generic as is_type_generic + if TYPE_CHECKING: from graphql import GraphQLResolveInfo from strawberry.field import StrawberryField + from strawberry.schema_directive import StrawberrySchemaDirective @dataclasses.dataclass(eq=False) @@ -31,25 +32,19 @@ class TypeDefinition(StrawberryType): is_interface: bool origin: Type description: Optional[str] - interfaces: List[TypeDefinition] + interfaces: List["TypeDefinition"] extend: bool - directives: Optional[Sequence[object]] + directives: Optional[Sequence[StrawberrySchemaDirective]] is_type_of: Optional[Callable[[Any, GraphQLResolveInfo], bool]] - _fields: List[StrawberryField] + _fields: List["StrawberryField"] - concrete_of: Optional[TypeDefinition] = None + concrete_of: Optional["TypeDefinition"] = None """Concrete implementations of Generic TypeDefinitions fill this in""" type_var_map: Mapping[TypeVar, Union[StrawberryType, type]] = dataclasses.field( default_factory=dict ) - def __post_init__(self): - # resolve `Self` annotation with the origin type - for index, field in enumerate(self.fields): - if isinstance(field.type, StrawberryType) and field.type.has_generic(Self): - self.fields[index] = field.copy_with({Self: self.origin}) # type: ignore # noqa: E501 - # TODO: remove wrapped cls when we "merge" this with `StrawberryObject` def resolve_generic(self, wrapped_cls: type) -> type: from strawberry.annotation import StrawberryAnnotation @@ -71,8 +66,20 @@ def resolve_generic(self, wrapped_cls: type) -> type: def copy_with( self, type_var_map: Mapping[TypeVar, Union[StrawberryType, type]] ) -> type: - # TODO: Logic unnecessary with StrawberryObject - fields = [field.copy_with(type_var_map) for field in self.fields] + fields = [] + for field in self.fields: + # TODO: Logic unnecessary with StrawberryObject + field_type = field.type + if hasattr(field_type, "_type_definition"): + field_type = field_type._type_definition # type: ignore + + # TODO: All types should end up being StrawberryTypes + # The first check is here as a symptom of strawberry.ID being a + # Scalar, but not a StrawberryType + if isinstance(field_type, StrawberryType) and field_type.is_generic: + field = field.copy_with(type_var_map) + + fields.append(field) new_type_definition = TypeDefinition( name=self.name, @@ -99,13 +106,13 @@ def copy_with( return new_type - def get_field(self, python_name: str) -> Optional[StrawberryField]: + def get_field(self, python_name: str) -> Optional["StrawberryField"]: return next( (field for field in self.fields if field.python_name == python_name), None ) @property - def fields(self) -> List[StrawberryField]: + def fields(self) -> List["StrawberryField"]: # TODO: rename _fields to fields and remove this property return self._fields diff --git a/strawberry/union.py b/strawberry/union.py index 5278795023..cae1260be7 100644 --- a/strawberry/union.py +++ b/strawberry/union.py @@ -1,10 +1,7 @@ import itertools -from itertools import chain from typing import ( TYPE_CHECKING, Any, - Collection, - Iterable, List, Mapping, NoReturn, @@ -27,14 +24,13 @@ from strawberry.annotation import StrawberryAnnotation from strawberry.exceptions import ( - InvalidTypeForUnionMergeError, - InvalidUnionTypeError, + InvalidUnionType, UnallowedReturnTypeForUnion, WrongReturnTypeForUnion, ) -from strawberry.lazy_type import LazyType from strawberry.type import StrawberryOptional, StrawberryType + if TYPE_CHECKING: from strawberry.schema.types.concrete_type import TypeMap from strawberry.types.types import TypeDefinition @@ -46,12 +42,10 @@ def __init__( name: Optional[str] = None, type_annotations: Tuple["StrawberryAnnotation", ...] = tuple(), description: Optional[str] = None, - directives: Iterable[object] = (), ): self.graphql_name = name self.type_annotations = type_annotations self.description = description - self.directives = directives def __eq__(self, other: object) -> bool: if isinstance(other, StrawberryType): @@ -77,7 +71,7 @@ def __or__(self, other: Union[StrawberryType, type]) -> StrawberryType: # Raise an error in any other case. # There is Work in progress to deal with more merging cases, see: # https://github.com/strawberry-graphql/strawberry/pull/1455 - raise InvalidTypeForUnionMergeError(self, other) + raise InvalidUnionType(other) @property def types(self) -> Tuple[StrawberryType, ...]: @@ -89,9 +83,6 @@ def types(self) -> Tuple[StrawberryType, ...]: @property def type_params(self) -> List[TypeVar]: def _get_type_params(type_: StrawberryType): - if isinstance(type_, LazyType): - type_ = cast(StrawberryType, type_.resolve_type()) - if hasattr(type_, "_type_definition"): parameters = getattr(type_, "__parameters__", None) @@ -120,7 +111,7 @@ def copy_with( new_type: Union[StrawberryType, type] if hasattr(type_, "_type_definition"): - type_definition: TypeDefinition = type_._type_definition + type_definition: TypeDefinition = type_._type_definition # type: ignore if type_definition.is_generic: new_type = type_definition.copy_with(type_var_map) @@ -145,6 +136,8 @@ def __call__(self, *_args, **_kwargs) -> NoReturn: raise ValueError("Cannot use union type directly") def get_type_resolver(self, type_map: "TypeMap") -> GraphQLTypeResolver: + # TODO: Type annotate returned function + def _resolve_union_type( root: Any, info: GraphQLResolveInfo, type_: GraphQLAbstractType ) -> str: @@ -161,20 +154,14 @@ def _resolve_union_type( ): return inner_type.name - # Couldn't resolve using `is_type_of` + # Couldn't resolve using `is_type_of`` raise WrongReturnTypeForUnion(info.field_name, str(type(root))) return_type: Optional[GraphQLType] - # Iterate over all of our known types and find the first concrete - # type that implements the type. We prioritise checking types named in the - # Union in case a nested generic object matches against more than one type. - concrete_types_for_union = (type_map[x.name] for x in type_.types) - - # TODO: do we still need to iterate over all types in `type_map`? - for possible_concrete_type in chain( - concrete_types_for_union, type_map.values() - ): + # Iterate over all of our known types and find the first concrete type that + # implements the type + for possible_concrete_type in type_map.values(): possible_type = possible_concrete_type.definition if not isinstance(possible_type, TypeDefinition): continue @@ -210,12 +197,8 @@ def _resolve_union_type( # yet supported in any python implementation (or in typing_extensions). # See https://www.python.org/dev/peps/pep-0646/ for more information def union( - name: str, - types: Collection[Types], - *, - description: Optional[str] = None, - directives: Iterable[object] = (), -) -> Union[Types]: + name: str, types: Tuple[Types, ...], *, description: str = None +) -> Union[Types]: # type: ignore """Creates a new named Union type. Example usages: @@ -228,18 +211,19 @@ def union( """ # Validate types - if not types: + if len(types) == 0: raise TypeError("No types passed to `union`") - for type_ in types: - if not isinstance(type_, TypeVar) and not hasattr(type_, "_type_definition"): - raise InvalidUnionTypeError(union_name=name, invalid_type=type_) + for _type in types: + if not isinstance(_type, TypeVar) and not hasattr(_type, "_type_definition"): + raise InvalidUnionType( + f"Type `{_type.__name__}` cannot be used in a GraphQL Union" + ) union_definition = StrawberryUnion( name=name, type_annotations=tuple(StrawberryAnnotation(type_) for type_ in types), description=description, - directives=directives, ) return union_definition # type: ignore diff --git a/strawberry/unset.py b/strawberry/unset.py index ecba41b7c2..a165eaf3b6 100644 --- a/strawberry/unset.py +++ b/strawberry/unset.py @@ -1,47 +1,6 @@ -import warnings -from typing import Any, Dict, Optional, Type - -DEPRECATED_NAMES: Dict[str, str] = { - "is_unset": "`is_unset` is deprecated use `value is UNSET` instead", -} - - -class UnsetType: - __instance: Optional["UnsetType"] = None - - def __new__(cls: Type["UnsetType"]) -> "UnsetType": - if cls.__instance is None: - ret = super().__new__(cls) - cls.__instance = ret - return ret - else: - return cls.__instance - +class _Unset: def __str__(self): return "" - def __repr__(self) -> str: - return "UNSET" - def __bool__(self): return False - - -UNSET: Any = UnsetType() - - -def _deprecated_is_unset(value: Any) -> bool: - warnings.warn(DEPRECATED_NAMES["is_unset"], DeprecationWarning, stacklevel=2) - return value is UNSET - - -def __getattr__(name: str) -> Any: - if name in DEPRECATED_NAMES: - warnings.warn(DEPRECATED_NAMES[name], DeprecationWarning, stacklevel=2) - return globals()[f"_deprecated_{name}"] - raise AttributeError(f"module {__name__} has no attribute {name}") - - -__all__ = [ - "UNSET", -] diff --git a/strawberry/utils/await_maybe.py b/strawberry/utils/await_maybe.py index 4256570cb2..4b07e87baa 100644 --- a/strawberry/utils/await_maybe.py +++ b/strawberry/utils/await_maybe.py @@ -1,6 +1,7 @@ import inspect from typing import Awaitable, TypeVar, Union + T = TypeVar("T") AwaitableOrValue = Union[Awaitable[T], T] diff --git a/strawberry/utils/cached_property.py b/strawberry/utils/cached_property.py deleted file mode 100644 index 21f6f43db7..0000000000 --- a/strawberry/utils/cached_property.py +++ /dev/null @@ -1,38 +0,0 @@ -import sys -from typing import TYPE_CHECKING - -if sys.version_info < (3, 8): - from backports.cached_property import cached_property -else: - from functools import cached_property - -if TYPE_CHECKING: - from threading import RLock - from typing import Any, Callable, Generic, Optional, Type, TypeVar, overload - - _T = TypeVar("_T") - _S = TypeVar("_S") - - class cached_property(Generic[_T]): # type: ignore[no-redef] - func: Callable[[Any], _T] - attrname: Optional[str] - lock: RLock - - def __init__(self, func: Callable[[Any], _T]) -> None: - ... - - @overload # type: ignore[no-overload-impl] - def __get__( - self, instance: None, owner: Optional[Type[Any]] = ... - ) -> cached_property[_T]: - ... - - @overload - def __get__(self, instance: _S, owner: Optional[Type[Any]] = ...) -> _T: - ... - - def __set_name__(self, owner: Type[Any], name: str) -> None: - ... - - -__all__ = ["cached_property"] diff --git a/strawberry/utils/dataclasses.py b/strawberry/utils/dataclasses.py deleted file mode 100644 index 36acc10de6..0000000000 --- a/strawberry/utils/dataclasses.py +++ /dev/null @@ -1,31 +0,0 @@ -import sys -from dataclasses import ( # type: ignore - _FIELD, - _FIELD_INITVAR, - _FIELDS, - _POST_INIT_NAME, - _set_new_attribute, -) - -from strawberry.ext.dataclasses.dataclasses import dataclass_init_fn - - -def add_custom_init_fn(cls): - fields = [ - f - for f in getattr(cls, _FIELDS).values() - if f._field_type in (_FIELD, _FIELD_INITVAR) - ] - globals_ = sys.modules[cls.__module__].__dict__ - - _set_new_attribute( - cls, - "__init__", - dataclass_init_fn( - fields=fields, - frozen=False, - has_post_init=hasattr(cls, _POST_INIT_NAME), - self_name="__dataclass_self__" if "self" in fields else "self", - globals_=globals_, - ), - ) diff --git a/strawberry/utils/debug.py b/strawberry/utils/debug.py index 650120492b..f7df1ad29f 100644 --- a/strawberry/utils/debug.py +++ b/strawberry/utils/debug.py @@ -3,6 +3,11 @@ from json import JSONEncoder from typing import Any, Dict, Optional +from pygments import highlight, lexers +from pygments.formatters import Terminal256Formatter + +from .graphql_lexer import GraphQLLexer + class StrawberryJSONEncoder(JSONEncoder): def default(self, o: Any) -> Any: @@ -16,28 +21,15 @@ def pretty_print_graphql_operation( Won't print introspection operation to prevent noise in the output.""" - try: - from pygments import highlight, lexers - from pygments.formatters import Terminal256Formatter - except ImportError as e: - raise ImportError( - "pygments is not installed but is required for debug output, install it " - "directly or run `pip install strawberry-graphql[debug-server]`" - ) from e - - from .graphql_lexer import GraphQLLexer - if operation_name == "IntrospectionQuery": return now = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") - print(f"[{now}]: {operation_name or 'No operation name'}") # noqa: T201 - print(highlight(query, GraphQLLexer(), Terminal256Formatter())) # noqa: T201 + print(f"[{now}]: {operation_name or 'No operation name'}") + print(highlight(query, GraphQLLexer(), Terminal256Formatter())) if variables: variables_json = json.dumps(variables, indent=4, cls=StrawberryJSONEncoder) - print( # noqa: T201 - highlight(variables_json, lexers.JsonLexer(), Terminal256Formatter()) - ) + print(highlight(variables_json, lexers.JsonLexer(), Terminal256Formatter())) diff --git a/strawberry/utils/graphiql.py b/strawberry/utils/graphiql.py deleted file mode 100644 index 99e131d363..0000000000 --- a/strawberry/utils/graphiql.py +++ /dev/null @@ -1,18 +0,0 @@ -import json -import pathlib - - -def get_graphiql_html( - subscription_enabled: bool = True, replace_variables: bool = True -) -> str: - here = pathlib.Path(__file__).parents[1] - path = here / "static/graphiql.html" - - template = path.read_text(encoding="utf-8") - - if replace_variables: - template = template.replace( - "{{ SUBSCRIPTION_ENABLED }}", json.dumps(subscription_enabled) - ) - - return template diff --git a/strawberry/utils/logging.py b/strawberry/utils/logging.py index 97fdab9418..172222ac1b 100644 --- a/strawberry/utils/logging.py +++ b/strawberry/utils/logging.py @@ -1,6 +1,7 @@ import logging import sys from typing import Any, Optional + from typing_extensions import Final from graphql.error import GraphQLError diff --git a/strawberry/utils/operation.py b/strawberry/utils/operation.py deleted file mode 100644 index 558dc0f31a..0000000000 --- a/strawberry/utils/operation.py +++ /dev/null @@ -1,35 +0,0 @@ -from typing import Optional, cast - -from graphql.language import DocumentNode, OperationDefinitionNode - -from strawberry.types.graphql import OperationType - - -def get_first_operation( - graphql_document: DocumentNode, -) -> Optional[OperationDefinitionNode]: - for definition in graphql_document.definitions: - if isinstance(definition, OperationDefinitionNode): - return definition - - return None - - -def get_operation_type( - graphql_document: DocumentNode, operation_name: Optional[str] = None -) -> OperationType: - definition: Optional[OperationDefinitionNode] = None - - if operation_name: - for d in graphql_document.definitions: - d = cast(OperationDefinitionNode, d) - if d.name and d.name.value == operation_name: - definition = d - break - else: - definition = get_first_operation(graphql_document) - - if not definition: - raise RuntimeError("Can't get GraphQL operation type") - - return OperationType(definition.operation.value) diff --git a/strawberry/utils/str_converters.py b/strawberry/utils/str_converters.py index e0e90cc1a1..9d994d9819 100644 --- a/strawberry/utils/str_converters.py +++ b/strawberry/utils/str_converters.py @@ -1,6 +1,3 @@ -import re - - # Adapted from this response in Stackoverflow # http://stackoverflow.com/a/19053800/1072990 def to_camel_case(snake_str: str) -> str: @@ -10,12 +7,5 @@ def to_camel_case(snake_str: str) -> str: return components[0] + "".join(x.capitalize() if x else "_" for x in components[1:]) -TO_KEBAB_CASE_RE = re.compile("((?<=[a-z0-9])[A-Z]|(?!^)[A-Z](?=[a-z]))") - - -def to_kebab_case(name: str) -> str: - return TO_KEBAB_CASE_RE.sub(r"-\1", name).lower() - - def capitalize_first(name: str) -> str: return name[0].upper() + name[1:] diff --git a/strawberry/utils/typing.py b/strawberry/utils/typing.py index ac5f4fa0e1..9dc888618d 100644 --- a/strawberry/utils/typing.py +++ b/strawberry/utils/typing.py @@ -1,5 +1,6 @@ import sys from collections.abc import AsyncGenerator +from typing import _GenericAlias # type: ignore from typing import ( # type: ignore Any, Callable, @@ -9,11 +10,10 @@ Type, TypeVar, Union, - _GenericAlias, ) -def is_list(annotation: object) -> bool: +def is_list(annotation: Type) -> bool: """Returns True if annotation is a List""" annotation_origin = getattr(annotation, "__origin__", None) @@ -21,14 +21,14 @@ def is_list(annotation: object) -> bool: return annotation_origin == list -def is_union(annotation: object) -> bool: +def is_union(annotation: Type) -> bool: """Returns True if annotation is a Union""" # this check is needed because unions declared with the new syntax `A | B` # don't have a `__origin__` property on them, but they are instances of # `UnionType`, which is only available in Python 3.10+ if sys.version_info >= (3, 10): - from types import UnionType + from types import UnionType # type: ignore if isinstance(annotation, UnionType): return True @@ -51,13 +51,13 @@ def is_optional(annotation: Type) -> bool: types = annotation.__args__ # A Union to be optional needs to have at least one None type - return any([x == None.__class__ for x in types]) + return any([x == None.__class__ for x in types]) # noqa:E711 def get_optional_annotation(annotation: Type) -> Type: types = annotation.__args__ - non_none_types = tuple(x for x in types if x != None.__class__) + non_none_types = tuple(x for x in types if x != None.__class__) # noqa:E711 # if we have multiple non none types we want to return a copy of this # type (normally a Union type). @@ -75,7 +75,7 @@ def get_list_annotation(annotation: Type) -> Type: def is_concrete_generic(annotation: type) -> bool: ignored_generics = (list, tuple, Union, ClassVar, AsyncGenerator) return ( - isinstance(annotation, _GenericAlias) + isinstance(annotation, _GenericAlias) # type:ignore and annotation.__origin__ not in ignored_generics ) @@ -100,12 +100,12 @@ def is_generic(annotation: type) -> bool: def is_type_var(annotation: Type) -> bool: """Returns True if the annotation is a TypeVar.""" - return isinstance(annotation, TypeVar) + return isinstance(annotation, TypeVar) # type:ignore def get_parameters(annotation: Type): if ( - isinstance(annotation, _GenericAlias) + isinstance(annotation, _GenericAlias) # type:ignore or isinstance(annotation, type) and issubclass(annotation, Generic) # type:ignore and annotation is not Generic diff --git a/tests/aiohttp/conftest.py b/tests/aiohttp/conftest.py index da19525dd3..0cf851d707 100644 --- a/tests/aiohttp/conftest.py +++ b/tests/aiohttp/conftest.py @@ -1,7 +1,5 @@ -import pytest import pytest_asyncio -from strawberry.aiohttp.test.client import GraphQLTestClient from tests.aiohttp.app import create_app @@ -10,15 +8,3 @@ async def aiohttp_app_client(event_loop, aiohttp_client): app = create_app(graphiql=True) event_loop.set_debug(True) return await aiohttp_client(app) - - -@pytest_asyncio.fixture -async def aiohttp_app_client_no_get(event_loop, aiohttp_client): - app = create_app(graphiql=True, allow_queries_via_get=False) - event_loop.set_debug(True) - return await aiohttp_client(app) - - -@pytest.fixture -def graphql_client(aiohttp_app_client): - return GraphQLTestClient(aiohttp_app_client, url="/graphql") diff --git a/tests/aiohttp/schema.py b/tests/aiohttp/schema.py index e5cbb30739..4a03463fff 100644 --- a/tests/aiohttp/schema.py +++ b/tests/aiohttp/schema.py @@ -6,17 +6,9 @@ import strawberry from strawberry.file_uploads import Upload -from strawberry.permission import BasePermission from strawberry.subscriptions.protocols.graphql_transport_ws.types import PingMessage -class AlwaysFailPermission(BasePermission): - message = "You are not authorized" - - def has_permission(self, source, info, **kwargs) -> bool: - return False - - @strawberry.enum class Flavor(Enum): VANILLA = "vanilla" @@ -37,31 +29,11 @@ class DebugInfo: @strawberry.type class Query: - @strawberry.field - def hello(self, name: typing.Optional[str] = None) -> str: - return f"Hello {name or 'world'}" - - @strawberry.field - async def async_hello(self, name: str, delay: float = 0) -> str: - await asyncio.sleep(delay) - return f"Hello {name or 'world'}" - - @strawberry.field(permission_classes=[AlwaysFailPermission]) - def always_fail(self) -> typing.Optional[str]: - return "Hey" - - @strawberry.field - async def exception(self, message: str) -> str: - raise ValueError(message) - return message + hello: str = "strawberry" @strawberry.type class Mutation: - @strawberry.mutation - def hello(self) -> str: - return "strawberry" - @strawberry.mutation def read_text(self, text_file: Upload) -> str: return text_file.read().decode() @@ -115,7 +87,7 @@ async def exception(self, message: str) -> typing.AsyncGenerator[str, None]: raise ValueError(message) # Without this yield, the method is not recognised as an async generator - yield "Hi" + yield "Hi" # noqa @strawberry.subscription async def flavors(self) -> typing.AsyncGenerator[Flavor, None]: @@ -141,9 +113,5 @@ async def debug(self, info) -> typing.AsyncGenerator[DebugInfo, None]: is_connection_init_timeout_task_done=is_connection_init_timeout_task_done, ) - @strawberry.subscription - async def connection_params(self, info) -> typing.AsyncGenerator[str, None]: - yield info.context["connection_params"]["strawberry"] - schema = strawberry.Schema(query=Query, mutation=Mutation, subscription=Subscription) diff --git a/tests/aiohttp/test_graphql_transport_ws.py b/tests/aiohttp/test_graphql_transport_ws.py index a663f21b38..163687b561 100644 --- a/tests/aiohttp/test_graphql_transport_ws.py +++ b/tests/aiohttp/test_graphql_transport_ws.py @@ -285,11 +285,7 @@ async def test_duplicated_operation_ids(aiohttp_client): assert data.extra == "Subscriber for sub1 already exists" -async def test_reused_operation_ids(aiohttp_client): - """ - Test that an operation id can be re-used after it has been - previously used for a completed operation - """ +async def test_simple_subscription(aiohttp_client): app = create_app() aiohttp_app_client = await aiohttp_client(app) @@ -301,7 +297,6 @@ async def test_reused_operation_ids(aiohttp_client): response = await ws.receive_json() assert response == ConnectionAckMessage().as_dict() - # Use sub1 as an id for an operation await ws.send_json( SubscribeMessage( id="sub1", @@ -317,30 +312,13 @@ async def test_reused_operation_ids(aiohttp_client): == NextMessage(id="sub1", payload={"data": {"echo": "Hi"}}).as_dict() ) - response = await ws.receive_json() - assert response == CompleteMessage(id="sub1").as_dict() - - # operation is now complete. Create a new operation using - # the same ID - await ws.send_json( - SubscribeMessage( - id="sub1", - payload=SubscribeMessagePayload( - query='subscription { echo(message: "Hi") }' - ), - ).as_dict() - ) + await ws.send_json(CompleteMessage(id="sub1").as_dict()) - response = await ws.receive_json() - assert ( - response - == NextMessage(id="sub1", payload={"data": {"echo": "Hi"}}).as_dict() - ) await ws.close() assert ws.closed -async def test_simple_subscription(aiohttp_client): +async def test_subscription_syntax_error(aiohttp_client): app = create_app() aiohttp_app_client = await aiohttp_client(app) @@ -355,49 +333,25 @@ async def test_simple_subscription(aiohttp_client): await ws.send_json( SubscribeMessage( id="sub1", - payload=SubscribeMessagePayload( - query='subscription { echo(message: "Hi") }' - ), + payload=SubscribeMessagePayload(query="subscription { INVALID_SYNTAX "), ).as_dict() ) response = await ws.receive_json() + assert response["type"] == ErrorMessage.type + assert response["id"] == "sub1" + assert len(response["payload"]) == 1 + assert response["payload"][0]["path"] is None + assert response["payload"][0]["locations"] == [{"line": 1, "column": 31}] assert ( - response - == NextMessage(id="sub1", payload={"data": {"echo": "Hi"}}).as_dict() + response["payload"][0]["message"] + == "Syntax Error: Expected Name, found ." ) - await ws.send_json(CompleteMessage(id="sub1").as_dict()) - await ws.close() assert ws.closed -async def test_subscription_syntax_error(aiohttp_client): - app = create_app() - aiohttp_app_client = await aiohttp_client(app) - - async with aiohttp_app_client.ws_connect( - "/graphql", protocols=[GRAPHQL_TRANSPORT_WS_PROTOCOL] - ) as ws: - await ws.send_json(ConnectionInitMessage().as_dict()) - - response = await ws.receive_json() - assert response == ConnectionAckMessage().as_dict() - - await ws.send_json( - SubscribeMessage( - id="sub1", - payload=SubscribeMessagePayload(query="subscription { INVALID_SYNTAX "), - ).as_dict() - ) - - data = await ws.receive(timeout=2) - assert ws.closed - assert ws.close_code == 4400 - assert data.extra == "Syntax Error: Expected Name, found ." - - async def test_subscription_field_errors(aiohttp_client): app = create_app() aiohttp_app_client = await aiohttp_client(app) @@ -423,7 +377,7 @@ async def test_subscription_field_errors(aiohttp_client): assert response["type"] == ErrorMessage.type assert response["id"] == "sub1" assert len(response["payload"]) == 1 - assert response["payload"][0].get("path") is None + assert response["payload"][0]["path"] is None assert response["payload"][0]["locations"] == [{"line": 1, "column": 16}] assert ( response["payload"][0]["message"] @@ -526,7 +480,7 @@ async def test_subscription_errors(aiohttp_client): assert response["type"] == ErrorMessage.type assert response["id"] == "sub1" assert len(response["payload"]) == 1 - assert response["payload"][0].get("path") == ["error"] + assert response["payload"][0]["path"] == ["error"] assert response["payload"][0]["message"] == "TEST ERR" await ws.close() @@ -558,467 +512,9 @@ async def test_subscription_exceptions(aiohttp_client): assert response["type"] == ErrorMessage.type assert response["id"] == "sub1" assert len(response["payload"]) == 1 - assert response["payload"][0].get("path") is None - assert response["payload"][0].get("locations") is None + assert response["payload"][0]["path"] is None + assert response["payload"][0]["locations"] is None assert response["payload"][0]["message"] == "TEST EXC" await ws.close() assert ws.closed - - -async def test_single_result_query_operation(aiohttp_client): - app = create_app() - aiohttp_app_client = await aiohttp_client(app) - - async with aiohttp_app_client.ws_connect( - "/graphql", protocols=[GRAPHQL_TRANSPORT_WS_PROTOCOL] - ) as ws: - await ws.send_json(ConnectionInitMessage().as_dict()) - - response = await ws.receive_json() - assert response == ConnectionAckMessage().as_dict() - - await ws.send_json( - SubscribeMessage( - id="sub1", - payload=SubscribeMessagePayload(query="query { hello }"), - ).as_dict() - ) - - response = await ws.receive_json() - assert ( - response - == NextMessage( - id="sub1", payload={"data": {"hello": "Hello world"}} - ).as_dict() - ) - - response = await ws.receive_json() - assert response == CompleteMessage(id="sub1").as_dict() - - await ws.close() - assert ws.closed - - -async def test_single_result_query_operation_async(aiohttp_client): - """ - Test a single result query operation on an - `async` method in the schema, including an artificial - async delay - """ - app = create_app() - aiohttp_app_client = await aiohttp_client(app) - - async with aiohttp_app_client.ws_connect( - "/graphql", protocols=[GRAPHQL_TRANSPORT_WS_PROTOCOL] - ) as ws: - await ws.send_json(ConnectionInitMessage().as_dict()) - - response = await ws.receive_json() - assert response == ConnectionAckMessage().as_dict() - - await ws.send_json( - SubscribeMessage( - id="sub1", - payload=SubscribeMessagePayload( - query='query { asyncHello(name: "Dolly", delay:0.01)}' - ), - ).as_dict() - ) - - response = await ws.receive_json() - assert ( - response - == NextMessage( - id="sub1", payload={"data": {"asyncHello": "Hello Dolly"}} - ).as_dict() - ) - - response = await ws.receive_json() - assert response == CompleteMessage(id="sub1").as_dict() - - await ws.close() - assert ws.closed - - -async def test_single_result_query_operation_overlapped(aiohttp_client): - """ - Test that two single result queries can be in flight at the same time, - just like regular queries. Start two queries with separate ids. The - first query has a delay, so we expect the response to the second - query to be delivered first. - """ - app = create_app() - aiohttp_app_client = await aiohttp_client(app) - - async with aiohttp_app_client.ws_connect( - "/graphql", protocols=[GRAPHQL_TRANSPORT_WS_PROTOCOL] - ) as ws: - await ws.send_json(ConnectionInitMessage().as_dict()) - - response = await ws.receive_json() - assert response == ConnectionAckMessage().as_dict() - - # first query - await ws.send_json( - SubscribeMessage( - id="sub1", - payload=SubscribeMessagePayload( - query='query { asyncHello(name: "Dolly", delay:1)}' - ), - ).as_dict() - ) - # second query - await ws.send_json( - SubscribeMessage( - id="sub2", - payload=SubscribeMessagePayload( - query='query { asyncHello(name: "Dolly", delay:0)}' - ), - ).as_dict() - ) - - # we expect the response to the second query to arrive first - response = await ws.receive_json() - assert ( - response - == NextMessage( - id="sub2", payload={"data": {"asyncHello": "Hello Dolly"}} - ).as_dict() - ) - response = await ws.receive_json() - assert response == CompleteMessage(id="sub2").as_dict() - - await ws.close() - assert ws.closed - - -async def test_single_result_mutation_operation(aiohttp_client): - app = create_app() - aiohttp_app_client = await aiohttp_client(app) - - async with aiohttp_app_client.ws_connect( - "/graphql", protocols=[GRAPHQL_TRANSPORT_WS_PROTOCOL] - ) as ws: - await ws.send_json(ConnectionInitMessage().as_dict()) - - response = await ws.receive_json() - assert response == ConnectionAckMessage().as_dict() - - await ws.send_json( - SubscribeMessage( - id="sub1", - payload=SubscribeMessagePayload(query="mutation { hello }"), - ).as_dict() - ) - - response = await ws.receive_json() - assert ( - response - == NextMessage( - id="sub1", payload={"data": {"hello": "strawberry"}} - ).as_dict() - ) - - response = await ws.receive_json() - assert response == CompleteMessage(id="sub1").as_dict() - - await ws.close() - assert ws.closed - - -async def test_single_result_operation_selection(aiohttp_client): - app = create_app() - aiohttp_app_client = await aiohttp_client(app) - - async with aiohttp_app_client.ws_connect( - "/graphql", protocols=[GRAPHQL_TRANSPORT_WS_PROTOCOL] - ) as ws: - await ws.send_json(ConnectionInitMessage().as_dict()) - - response = await ws.receive_json() - assert response == ConnectionAckMessage().as_dict() - - query = """ - query Query1 { - hello - } - query Query2 { - hello(name: "Strawberry") - } - """ - - await ws.send_json( - SubscribeMessage( - id="sub1", - payload=SubscribeMessagePayload(query=query, operationName="Query2"), - ).as_dict() - ) - - response = await ws.receive_json() - assert ( - response - == NextMessage( - id="sub1", payload={"data": {"hello": "Hello Strawberry"}} - ).as_dict() - ) - - response = await ws.receive_json() - assert response == CompleteMessage(id="sub1").as_dict() - - await ws.close() - assert ws.closed - - -async def test_single_result_invalid_operation_selection(aiohttp_client): - app = create_app() - aiohttp_app_client = await aiohttp_client(app) - - async with aiohttp_app_client.ws_connect( - "/graphql", protocols=[GRAPHQL_TRANSPORT_WS_PROTOCOL] - ) as ws: - await ws.send_json(ConnectionInitMessage().as_dict()) - - response = await ws.receive_json() - assert response == ConnectionAckMessage().as_dict() - - query = """ - query Query1 { - hello - } - """ - - await ws.send_json( - SubscribeMessage( - id="sub1", - payload=SubscribeMessagePayload(query=query, operationName="Query2"), - ).as_dict() - ) - - data = await ws.receive(timeout=2) - assert ws.closed - assert ws.close_code == 4400 - assert data.extra == "Can't get GraphQL operation type" - - -async def test_single_result_operation_error(aiohttp_client): - app = create_app() - aiohttp_app_client = await aiohttp_client(app) - - async with aiohttp_app_client.ws_connect( - "/graphql", protocols=[GRAPHQL_TRANSPORT_WS_PROTOCOL] - ) as ws: - await ws.send_json(ConnectionInitMessage().as_dict()) - - response = await ws.receive_json() - assert response == ConnectionAckMessage().as_dict() - - await ws.send_json( - SubscribeMessage( - id="sub1", - payload=SubscribeMessagePayload( - query="query { alwaysFail }", - ), - ).as_dict() - ) - - response = await ws.receive_json() - assert response["type"] == ErrorMessage.type - assert response["id"] == "sub1" - assert len(response["payload"]) == 1 - assert response["payload"][0]["message"] == "You are not authorized" - - await ws.close() - assert ws.closed - - -async def test_single_result_operation_exception(aiohttp_client): - """ - Test that single-result-operations which raise exceptions - behave in the same way as streaming operations - """ - app = create_app() - aiohttp_app_client = await aiohttp_client(app) - - async with aiohttp_app_client.ws_connect( - "/graphql", protocols=[GRAPHQL_TRANSPORT_WS_PROTOCOL] - ) as ws: - await ws.send_json(ConnectionInitMessage().as_dict()) - - response = await ws.receive_json() - assert response == ConnectionAckMessage().as_dict() - - await ws.send_json( - SubscribeMessage( - id="sub1", - payload=SubscribeMessagePayload( - query='query { exception(message: "bummer") }', - ), - ).as_dict() - ) - - response = await ws.receive_json() - assert response["type"] == ErrorMessage.type - assert response["id"] == "sub1" - assert len(response["payload"]) == 1 - assert response["payload"][0].get("path") == ["exception"] - assert response["payload"][0]["message"] == "bummer" - - -async def test_single_result_duplicate_ids_sub(aiohttp_client): - """ - Test that single-result-operations and streaming operations - share the same ID namespace. Start a regular subscription, - then issue a single-result operation with same ID and expect an - error due to already existing ID - """ - app = create_app() - aiohttp_app_client = await aiohttp_client(app) - - async with aiohttp_app_client.ws_connect( - "/graphql", protocols=[GRAPHQL_TRANSPORT_WS_PROTOCOL] - ) as ws: - await ws.send_json(ConnectionInitMessage().as_dict()) - - response = await ws.receive_json() - assert response == ConnectionAckMessage().as_dict() - - # regular subscription - await ws.send_json( - SubscribeMessage( - id="sub1", - payload=SubscribeMessagePayload( - query='subscription { echo(message: "Hi", delay: 5) }' - ), - ).as_dict() - ) - # single result subscription with duplicate id - await ws.send_json( - SubscribeMessage( - id="sub1", - payload=SubscribeMessagePayload( - query="query { hello }", - ), - ).as_dict() - ) - - data = await ws.receive(timeout=2) - assert ws.closed - assert ws.close_code == 4409 - assert data.extra == "Subscriber for sub1 already exists" - - await ws.close() - assert ws.closed - - -async def test_single_result_duplicate_ids_query(aiohttp_client): - """ - Test that single-result-operations don't allow duplicate - IDs for two asynchronous queries. Issue one async query - with delay, then another with same id. Expect error. - """ - app = create_app() - aiohttp_app_client = await aiohttp_client(app) - - async with aiohttp_app_client.ws_connect( - "/graphql", protocols=[GRAPHQL_TRANSPORT_WS_PROTOCOL] - ) as ws: - await ws.send_json(ConnectionInitMessage().as_dict()) - - response = await ws.receive_json() - assert response == ConnectionAckMessage().as_dict() - - # single result subscription 1 - await ws.send_json( - SubscribeMessage( - id="sub1", - payload=SubscribeMessagePayload( - query='query { asyncHello(name: "Hi", delay: 5) }' - ), - ).as_dict() - ) - # single result subscription with duplicate id - await ws.send_json( - SubscribeMessage( - id="sub1", - payload=SubscribeMessagePayload( - query="query { hello }", - ), - ).as_dict() - ) - - # We expect the remote to close the socket due to duplicate ID in use - data = await ws.receive(timeout=2) - assert ws.closed - assert ws.close_code == 4409 - assert data.extra == "Subscriber for sub1 already exists" - - await ws.close() - assert ws.closed - - -async def test_injects_connection_params(aiohttp_client): - app = create_app() - aiohttp_app_client = await aiohttp_client(app) - - async with aiohttp_app_client.ws_connect( - "/graphql", protocols=[GRAPHQL_TRANSPORT_WS_PROTOCOL] - ) as ws: - await ws.send_json( - ConnectionInitMessage(payload={"strawberry": "rocks"}).as_dict() - ) - - response = await ws.receive_json() - assert response == ConnectionAckMessage().as_dict() - - await ws.send_json( - SubscribeMessage( - id="sub1", - payload=SubscribeMessagePayload( - query="subscription { connectionParams }" - ), - ).as_dict() - ) - - response = await ws.receive_json() - assert ( - response - == NextMessage( - id="sub1", payload={"data": {"connectionParams": "rocks"}} - ).as_dict() - ) - - await ws.send_json(CompleteMessage(id="sub1").as_dict()) - - await ws.close() - assert ws.closed - - -async def test_rejects_connection_params_not_dict(aiohttp_client): - app = create_app() - aiohttp_app_client = await aiohttp_client(app) - - async with aiohttp_app_client.ws_connect( - "/graphql", protocols=[GRAPHQL_TRANSPORT_WS_PROTOCOL] - ) as ws: - await ws.send_json(ConnectionInitMessage(payload="gonna fail").as_dict()) - - data = await ws.receive(timeout=2) - assert ws.closed - assert ws.close_code == 4400 - assert data.extra == "Invalid connection init payload" - - -async def test_rejects_connection_params_not_unset(aiohttp_client): - app = create_app() - aiohttp_app_client = await aiohttp_client(app) - - async with aiohttp_app_client.ws_connect( - "/graphql", protocols=[GRAPHQL_TRANSPORT_WS_PROTOCOL] - ) as ws: - await ws.send_json(ConnectionInitMessage(payload=None).as_dict()) - - data = await ws.receive(timeout=2) - assert ws.closed - assert ws.close_code == 4400 - assert data.extra == "Invalid connection init payload" diff --git a/tests/aiohttp/test_graphql_ws.py b/tests/aiohttp/test_graphql_ws.py index 6b252772a9..7c67aa1e66 100644 --- a/tests/aiohttp/test_graphql_ws.py +++ b/tests/aiohttp/test_graphql_ws.py @@ -6,7 +6,6 @@ from strawberry.subscriptions.protocols.graphql_ws import ( GQL_COMPLETE, GQL_CONNECTION_ACK, - GQL_CONNECTION_ERROR, GQL_CONNECTION_INIT, GQL_CONNECTION_KEEP_ALIVE, GQL_CONNECTION_TERMINATE, @@ -264,7 +263,9 @@ async def test_subscription_exceptions(aiohttp_client): assert response["type"] == GQL_DATA assert response["id"] == "demo" assert response["payload"]["data"] is None - assert response["payload"]["errors"] == [{"message": "TEST EXC"}] + assert response["payload"]["errors"] == [ + {"locations": None, "message": "TEST EXC", "path": None} + ] await ws.send_json({"type": GQL_STOP, "id": "demo"}) response = await ws.receive_json() @@ -302,6 +303,7 @@ async def test_subscription_field_error(aiohttp_client): assert response["id"] == "invalid-field" assert response["payload"] == { "locations": [{"line": 1, "column": 16}], + "path": None, "message": ( "The subscription field 'notASubscriptionField' is not defined." ), @@ -338,6 +340,7 @@ async def test_subscription_syntax_error(aiohttp_client): assert response["id"] == "syntax-error" assert response["payload"] == { "locations": [{"line": 1, "column": 24}], + "path": None, "message": "Syntax Error: Expected Name, found .", } @@ -572,7 +575,7 @@ def get_result_handler_tasks(): await ws1.send_json({"type": GQL_STOP, "id": "demo"}) await ws1.send_json({"type": GQL_CONNECTION_TERMINATE}) - async for _msg in ws1: + async for msg in ws1: # Receive all outstanding messages including the final close message pass @@ -581,75 +584,8 @@ def get_result_handler_tasks(): await ws2.send_json({"type": GQL_STOP, "id": "demo"}) await ws2.send_json({"type": GQL_CONNECTION_TERMINATE}) - async for _msg in ws2: + async for msg in ws2: # Receive all outstanding messages including the final close message pass assert len(get_result_handler_tasks()) == 0 - - -async def test_injects_connection_params(aiohttp_client): - app = create_app(keep_alive=False) - aiohttp_app_client = await aiohttp_client(app) - - async with aiohttp_app_client.ws_connect( - "/graphql", protocols=[GRAPHQL_WS_PROTOCOL] - ) as ws: - await ws.send_json( - { - "type": GQL_CONNECTION_INIT, - "id": "demo", - "payload": {"strawberry": "rocks"}, - } - ) - await ws.send_json( - { - "type": GQL_START, - "id": "demo", - "payload": { - "query": "subscription { connectionParams }", - }, - } - ) - - response = await ws.receive_json() - assert response["type"] == GQL_CONNECTION_ACK - - response = await ws.receive_json() - assert response["type"] == GQL_DATA - assert response["id"] == "demo" - assert response["payload"]["data"] == {"connectionParams": "rocks"} - - await ws.send_json({"type": GQL_STOP, "id": "demo"}) - response = await ws.receive_json() - assert response["type"] == GQL_COMPLETE - assert response["id"] == "demo" - - await ws.send_json({"type": GQL_CONNECTION_TERMINATE}) - - # make sure the WebSocket is disconnected now - await ws.receive(timeout=2) # receive close - assert ws.closed - - -async def test_rejects_connection_params(aiohttp_client): - app = create_app(keep_alive=False) - aiohttp_app_client = await aiohttp_client(app) - - async with aiohttp_app_client.ws_connect( - "/graphql", protocols=[GRAPHQL_WS_PROTOCOL] - ) as ws: - await ws.send_json( - { - "type": GQL_CONNECTION_INIT, - "id": "demo", - "payload": "gonna fail", - } - ) - - response = await ws.receive_json() - assert response["type"] == GQL_CONNECTION_ERROR - - # make sure the WebSocket is disconnected now - await ws.receive(timeout=2) # receive close - assert ws.closed diff --git a/tests/aiohttp/test_http.py b/tests/aiohttp/test_http.py new file mode 100644 index 0000000000..2d8f93425e --- /dev/null +++ b/tests/aiohttp/test_http.py @@ -0,0 +1,129 @@ +import strawberry +from aiohttp import hdrs, web +from strawberry.aiohttp.views import GraphQLView +from strawberry.types import ExecutionResult, Info + + +async def test_graphql_query(aiohttp_app_client): + query = { + "query": """ + query { + hello + } + """ + } + + response = await aiohttp_app_client.post("/graphql", json=query) + data = await response.json() + assert response.status == 200 + assert data["data"]["hello"] == "strawberry" + + +async def test_custom_context(aiohttp_client): + class CustomGraphQLView(GraphQLView): + async def get_context(self, request: web.Request, response: web.StreamResponse): + return {"request": request, "response": response, "custom_value": "Hi!"} + + @strawberry.type + class Query: + @strawberry.field + def custom_context_value(self, info: Info) -> str: + return info.context["custom_value"] + + schema = strawberry.Schema(query=Query) + + app = web.Application() + app.router.add_route("*", "/graphql", CustomGraphQLView(schema=schema)) + client = await aiohttp_client(app) + + query = "{ customContextValue }" + resp = await client.post("/graphql", json={"query": query}) + data = await resp.json() + + assert resp.status == 200 + assert data["data"] == {"customContextValue": "Hi!"} + + +async def test_custom_process_result(aiohttp_client): + class CustomGraphQLView(GraphQLView): + async def process_result(self, request: web.Request, result: ExecutionResult): + return {} + + @strawberry.type + class Query: + @strawberry.field + def abc(self) -> str: + return "ABC" + + schema = strawberry.Schema(query=Query) + + app = web.Application() + app.router.add_route("*", "/graphql", CustomGraphQLView(schema=schema)) + client = await aiohttp_client(app) + + query = "{ abc }" + response = await client.post("/graphql", json={"query": query}) + data = await response.json() + + assert response.status == 200 + assert data == {} + + +async def test_setting_cookies_via_context(aiohttp_client): + @strawberry.type + class Query: + @strawberry.field + def abc(self, info) -> str: + info.context["response"].set_cookie("TEST_COOKIE", "TEST_VALUE") + return "ABC" + + schema = strawberry.Schema(query=Query) + + app = web.Application() + app.router.add_route("*", "/graphql", GraphQLView(schema=schema)) + client = await aiohttp_client(app) + + query = "{ abc }" + response = await client.post("/graphql", json={"query": query}) + + assert response.status == 200 + assert response.cookies.get("TEST_COOKIE").value == "TEST_VALUE" + + +async def test_malformed_query(aiohttp_app_client): + query = { + "qwary": """ + qwary { + hello + } + """ + } + + response = await aiohttp_app_client.post("/graphql", json=query) + reason = await response.text() + + assert response.status == 400 + assert reason == "400: No GraphQL query found in the request" + + +async def test_sending_invalid_json_body(aiohttp_app_client): + query = "}" + + response = await aiohttp_app_client.post( + "/graphql", data=query, headers={"content-type": "application/json"} + ) + reason = await response.text() + + assert response.status == 400 + assert reason == "400: Unable to parse request body as JSON" + + +async def test_not_allowed_methods(aiohttp_app_client): + # The CONNECT method is not allowed, but would require SSL to be tested. + not_allowed_methods = hdrs.METH_ALL.difference( + {hdrs.METH_GET, hdrs.METH_POST, hdrs.METH_CONNECT} + ) + + for method in not_allowed_methods: + response = await aiohttp_app_client.request(method, "/graphql") + assert response.status == 405, method diff --git a/tests/aiohttp/test_upload.py b/tests/aiohttp/test_upload.py new file mode 100644 index 0000000000..73363a4596 --- /dev/null +++ b/tests/aiohttp/test_upload.py @@ -0,0 +1,172 @@ +import json +from io import BytesIO + +import aiohttp + + +async def test_single_file_upload(aiohttp_app_client): + query = """mutation($textFile: Upload!) { + readText(textFile: $textFile) + }""" + + f = BytesIO(b"strawberry") + operations = json.dumps({"query": query, "variables": {"textFile": None}}) + file_map = json.dumps({"textFile": ["variables.textFile"]}) + + form_data = aiohttp.FormData() + form_data.add_field("textFile", f, filename="textFile.txt") + form_data.add_field("operations", operations) + form_data.add_field("map", file_map) + + response = await aiohttp_app_client.post("/graphql", data=form_data) + assert response.status == 200 + + data = await response.json() + + assert not data.get("errors") + assert data["data"]["readText"] == "strawberry" + + +async def test_file_list_upload(aiohttp_app_client): + query = "mutation($files: [Upload!]!) { readFiles(files: $files) }" + operations = json.dumps({"query": query, "variables": {"files": [None, None]}}) + file_map = json.dumps( + {"file1": ["variables.files.0"], "file2": ["variables.files.1"]} + ) + + file1 = BytesIO(b"strawberry1") + file2 = BytesIO(b"strawberry2") + + form_data = aiohttp.FormData() + form_data.add_field("file1", file1, filename="file1.txt") + form_data.add_field("file2", file2, filename="file2.txt") + form_data.add_field("operations", operations) + form_data.add_field("map", file_map) + + response = await aiohttp_app_client.post("/graphql", data=form_data) + assert response.status == 200 + + data = await response.json() + + assert not data.get("errors") + assert len(data["data"]["readFiles"]) == 2 + assert data["data"]["readFiles"][0] == "strawberry1" + assert data["data"]["readFiles"][1] == "strawberry2" + + +async def test_nested_file_list(aiohttp_app_client): + query = "mutation($folder: FolderInput!) { readFolder(folder: $folder) }" + operations = json.dumps( + {"query": query, "variables": {"folder": {"files": [None, None]}}} + ) + file_map = json.dumps( + {"file1": ["variables.folder.files.0"], "file2": ["variables.folder.files.1"]} + ) + + file1 = BytesIO(b"strawberry1") + file2 = BytesIO(b"strawberry2") + + form_data = aiohttp.FormData() + form_data.add_field("file1", file1, filename="file1.txt") + form_data.add_field("file2", file2, filename="file2.txt") + form_data.add_field("operations", operations) + form_data.add_field("map", file_map) + + response = await aiohttp_app_client.post("/graphql", data=form_data) + assert response.status == 200 + + data = await response.json() + + assert not data.get("errors") + assert len(data["data"]["readFolder"]) == 2 + assert data["data"]["readFolder"][0] == "strawberry1" + assert data["data"]["readFolder"][1] == "strawberry2" + + +async def test_extra_form_data_fields_are_ignored(aiohttp_app_client): + query = """mutation($textFile: Upload!) { + readText(textFile: $textFile) + }""" + + f = BytesIO(b"strawberry") + operations = json.dumps({"query": query, "variables": {"textFile": None}}) + file_map = json.dumps({"textFile": ["variables.textFile"]}) + extra_field_data = json.dumps({}) + + form_data = aiohttp.FormData() + form_data.add_field("textFile", f, filename="textFile.txt") + form_data.add_field("operations", operations) + form_data.add_field("map", file_map) + form_data.add_field("extra_field", extra_field_data) + + response = await aiohttp_app_client.post("/graphql", data=form_data) + assert response.status == 200 + + +async def test_sending_invalid_form_data(aiohttp_app_client): + headers = {"content-type": "multipart/form-data; boundary=----fake"} + response = await aiohttp_app_client.post("/graphql", headers=headers) + reason = await response.text() + + assert response.status == 400 + assert reason == "400: Unable to parse the multipart body" + + +async def test_malformed_query(aiohttp_app_client): + f = BytesIO(b"strawberry") + operations = json.dumps({"qwary": "", "variables": {"textFile": None}}) + file_map = json.dumps({"textFile": ["variables.textFile"]}) + + form_data = aiohttp.FormData() + form_data.add_field("textFile", f, filename="textFile.txt") + form_data.add_field("operations", operations) + form_data.add_field("map", file_map) + + response = await aiohttp_app_client.post("/graphql", data=form_data) + reason = await response.text() + + assert response.status == 400 + assert reason == "400: No GraphQL query found in the request" + + +async def test_sending_invalid_json_body(aiohttp_app_client): + f = BytesIO(b"strawberry") + operations = "}" + file_map = json.dumps({"textFile": ["variables.textFile"]}) + + form_data = aiohttp.FormData() + form_data.add_field("textFile", f, filename="textFile.txt") + form_data.add_field("operations", operations) + form_data.add_field("map", file_map) + + response = await aiohttp_app_client.post("/graphql", data=form_data) + reason = await response.text() + + assert response.status == 400 + assert reason == "400: Unable to parse the multipart body" + + +async def test_upload_with_missing_file(aiohttp_app_client): + # The aiohttp test client prevents us from sending invalid aiohttp.FormData. + # To test invalid data anyway we construct it manually. + data = ( + "------Boundary\r\n" + 'Content-Disposition: form-data; name="operations"\r\n' + "\r\n" + "{" + '"query": "mutation($textFile: Upload!){readText(textFile: $textFile)}",' + '"variables": {"textFile": null}' + "}\r\n" + "------Boundary\r\n" + 'Content-Disposition: form-data; name="map"\r\n' + "\r\n" + '{"textFile": ["variables.textFile"]}\r\n' + "------Boundary--" + ) + headers = {"content-type": "multipart/form-data; boundary=----Boundary"} + + response = await aiohttp_app_client.post("/graphql", data=data, headers=headers) + reason = await response.text() + + assert response.status == 400 + assert reason == "400: File(s) missing in form data" diff --git a/tests/aiohttp/test_websockets.py b/tests/aiohttp/test_view.py similarity index 86% rename from tests/aiohttp/test_websockets.py rename to tests/aiohttp/test_view.py index 83848b80ac..79933b49df 100644 --- a/tests/aiohttp/test_websockets.py +++ b/tests/aiohttp/test_view.py @@ -3,6 +3,21 @@ from .app import create_app +async def test_graphiql_view(aiohttp_app_client): + response = await aiohttp_app_client.get("/graphql", headers={"Accept": "text/html"}) + body = await response.text() + + assert "GraphiQL" in body + + +async def test_graphiql_disabled_view(aiohttp_client): + app = create_app(graphiql=False) + client = await aiohttp_client(app) + + response = await client.get("/graphql", headers={"Accept": "text/html"}) + assert response.status == 404 + + async def test_turning_off_graphql_ws(aiohttp_client): app = create_app(subscription_protocols=[GRAPHQL_TRANSPORT_WS_PROTOCOL]) aiohttp_app_client = await aiohttp_client(app) diff --git a/tests/asgi/conftest.py b/tests/asgi/conftest.py index 6fdc214960..39995ee73d 100644 --- a/tests/asgi/conftest.py +++ b/tests/asgi/conftest.py @@ -1,6 +1,5 @@ -import pathlib - import pytest + from starlette.testclient import TestClient from strawberry.asgi.test import GraphQLTestClient @@ -25,24 +24,6 @@ def test_client_no_graphiql(): return TestClient(app) -@pytest.fixture -def test_client_no_get(): - app = create_app(allow_queries_via_get=False) - return TestClient(app) - - @pytest.fixture def graphql_client(test_client): - return GraphQLTestClient(test_client) - - -def pytest_collection_modifyitems(config, items): - # automatically mark tests with 'starlette' if they are in the asgi subfolder - - rootdir = pathlib.Path(config.rootdir) - - for item in items: - rel_path = pathlib.Path(item.fspath).relative_to(rootdir) - - if str(rel_path).startswith("tests/asgi"): - item.add_marker(pytest.mark.starlette) + yield GraphQLTestClient(test_client) diff --git a/tests/asgi/schema.py b/tests/asgi/schema.py index 7b375bc185..b11a18b745 100644 --- a/tests/asgi/schema.py +++ b/tests/asgi/schema.py @@ -43,11 +43,6 @@ class Query: def hello(self, name: typing.Optional[str] = None) -> str: return f"Hello {name or 'world'}" - @strawberry.field - async def async_hello(self, name: str, delay: float = 0) -> str: - await asyncio.sleep(delay) - return f"Hello {name or 'world'}" - @strawberry.field(permission_classes=[AlwaysFailPermission]) def always_fail(self) -> Optional[str]: return "Hey" @@ -56,18 +51,9 @@ def always_fail(self) -> Optional[str]: def root_name(root) -> str: return type(root).__name__ - @strawberry.field - async def exception(self, message: str) -> str: - raise ValueError(message) - return message - @strawberry.type class Mutation: - @strawberry.mutation - async def hello(self) -> str: - return "strawberry" - @strawberry.mutation async def read_text(self, text_file: Upload) -> str: return (await text_file.read()).decode() @@ -123,7 +109,7 @@ async def exception(self, message: str) -> typing.AsyncGenerator[str, None]: raise ValueError(message) # Without this yield, the method is not recognised as an async generator - yield "Hi" + yield "Hi" # noqa @strawberry.subscription async def flavors(self) -> typing.AsyncGenerator[Flavor, None]: @@ -149,9 +135,5 @@ async def debug(self, info) -> typing.AsyncGenerator[DebugInfo, None]: is_connection_init_timeout_task_done=is_connection_init_timeout_task_done, ) - @strawberry.subscription - async def connection_params(self, info: Info) -> typing.AsyncGenerator[str, None]: - yield info.context["connection_params"]["strawberry"] - schema = strawberry.Schema(Query, mutation=Mutation, subscription=Subscription) diff --git a/tests/asgi/test_async.py b/tests/asgi/test_async.py index e4fdbd366f..b35296ff44 100644 --- a/tests/asgi/test_async.py +++ b/tests/asgi/test_async.py @@ -1,6 +1,7 @@ import typing import pytest + from starlette.testclient import TestClient import strawberry diff --git a/tests/asgi/test_graphql_transport_ws.py b/tests/asgi/test_graphql_transport_ws.py index cef160d8d1..c9cd075d77 100644 --- a/tests/asgi/test_graphql_transport_ws.py +++ b/tests/asgi/test_graphql_transport_ws.py @@ -220,56 +220,6 @@ def test_duplicated_operation_ids(test_client): assert data["code"] == 4409 -def test_reused_operation_ids(test_client): - """ - Test that an operation id can be re-used after it has been - previously used for a completed operation - """ - with test_client.websocket_connect( - "/graphql", [GRAPHQL_TRANSPORT_WS_PROTOCOL] - ) as ws: - ws.send_json(ConnectionInitMessage().as_dict()) - - response = ws.receive_json() - assert response == ConnectionAckMessage().as_dict() - - # Use sub1 as an id for an operation - ws.send_json( - SubscribeMessage( - id="sub1", - payload=SubscribeMessagePayload( - query='subscription { echo(message: "Hi") }' - ), - ).as_dict() - ) - - response = ws.receive_json() - assert ( - response - == NextMessage(id="sub1", payload={"data": {"echo": "Hi"}}).as_dict() - ) - - response = ws.receive_json() - assert response == CompleteMessage(id="sub1").as_dict() - - # operation is now complete. Create a new operation using - # the same ID - ws.send_json( - SubscribeMessage( - id="sub1", - payload=SubscribeMessagePayload( - query='subscription { echo(message: "Hi") }' - ), - ).as_dict() - ) - - response = ws.receive_json() - assert ( - response - == NextMessage(id="sub1", payload={"data": {"echo": "Hi"}}).as_dict() - ) - - def test_simple_subscription(test_client): with test_client.websocket_connect("/", [GRAPHQL_TRANSPORT_WS_PROTOCOL]) as ws: ws.send_json(ConnectionInitMessage().as_dict()) @@ -311,9 +261,18 @@ def test_subscription_syntax_error(test_client): ).as_dict() ) - data = ws.receive() - assert data["type"] == "websocket.close" - assert data["code"] == 4400 + response = ws.receive_json() + assert response["type"] == ErrorMessage.type + assert response["id"] == "sub1" + assert len(response["payload"]) == 1 + assert response["payload"][0]["path"] is None + assert response["payload"][0]["locations"] == [{"line": 1, "column": 31}] + assert ( + response["payload"][0]["message"] + == "Syntax Error: Expected Name, found ." + ) + + ws.close() def test_subscription_field_errors(test_client): @@ -336,7 +295,7 @@ def test_subscription_field_errors(test_client): assert response["type"] == ErrorMessage.type assert response["id"] == "sub1" assert len(response["payload"]) == 1 - assert response["payload"][0].get("path") is None + assert response["payload"][0]["path"] is None assert response["payload"][0]["locations"] == [{"line": 1, "column": 16}] assert ( response["payload"][0]["message"] @@ -453,387 +412,8 @@ def test_subscription_exceptions(test_client): assert response["type"] == ErrorMessage.type assert response["id"] == "sub1" assert len(response["payload"]) == 1 - assert response["payload"][0].get("path") is None - assert response["payload"][0].get("locations") is None + assert response["payload"][0]["path"] is None + assert response["payload"][0]["locations"] is None assert response["payload"][0]["message"] == "TEST EXC" ws.close() - - -def test_single_result_query_operation(test_client): - with test_client.websocket_connect( - "/graphql", [GRAPHQL_TRANSPORT_WS_PROTOCOL] - ) as ws: - ws.send_json(ConnectionInitMessage().as_dict()) - - response = ws.receive_json() - assert response == ConnectionAckMessage().as_dict() - - ws.send_json( - SubscribeMessage( - id="sub1", - payload=SubscribeMessagePayload(query="query { hello }"), - ).as_dict() - ) - - response = ws.receive_json() - assert ( - response - == NextMessage( - id="sub1", payload={"data": {"hello": "Hello world"}} - ).as_dict() - ) - - response = ws.receive_json() - assert response == CompleteMessage(id="sub1").as_dict() - - -def test_single_result_query_operation_async(test_client): - """ - Test a single result query operation on an - `async` method in the schema, including an artificial - async delay - """ - with test_client.websocket_connect( - "/graphql", [GRAPHQL_TRANSPORT_WS_PROTOCOL] - ) as ws: - ws.send_json(ConnectionInitMessage().as_dict()) - - response = ws.receive_json() - assert response == ConnectionAckMessage().as_dict() - - ws.send_json( - SubscribeMessage( - id="sub1", - payload=SubscribeMessagePayload( - query='query { asyncHello(name: "Dolly", delay:0.01)}' - ), - ).as_dict() - ) - - response = ws.receive_json() - assert ( - response - == NextMessage( - id="sub1", payload={"data": {"asyncHello": "Hello Dolly"}} - ).as_dict() - ) - - response = ws.receive_json() - assert response == CompleteMessage(id="sub1").as_dict() - - -def test_single_result_query_operation_overlapped(test_client): - """ - Test that two single result queries can be in flight at the same time, - just like regular queries. Start two queries with separate ids. The - first query has a delay, so we expect the response to the second - query to be delivered first. - """ - with test_client.websocket_connect( - "/graphql", [GRAPHQL_TRANSPORT_WS_PROTOCOL] - ) as ws: - ws.send_json(ConnectionInitMessage().as_dict()) - - response = ws.receive_json() - assert response == ConnectionAckMessage().as_dict() - - # first query - ws.send_json( - SubscribeMessage( - id="sub1", - payload=SubscribeMessagePayload( - query='query { asyncHello(name: "Dolly", delay:1)}' - ), - ).as_dict() - ) - # second query - ws.send_json( - SubscribeMessage( - id="sub2", - payload=SubscribeMessagePayload( - query='query { asyncHello(name: "Dolly", delay:0)}' - ), - ).as_dict() - ) - - # we expect the response to the second query to arrive first - response = ws.receive_json() - assert ( - response - == NextMessage( - id="sub2", payload={"data": {"asyncHello": "Hello Dolly"}} - ).as_dict() - ) - response = ws.receive_json() - assert response == CompleteMessage(id="sub2").as_dict() - - -def test_single_result_mutation_operation(test_client): - with test_client.websocket_connect( - "/graphql", [GRAPHQL_TRANSPORT_WS_PROTOCOL] - ) as ws: - ws.send_json(ConnectionInitMessage().as_dict()) - - response = ws.receive_json() - assert response == ConnectionAckMessage().as_dict() - - ws.send_json( - SubscribeMessage( - id="sub1", - payload=SubscribeMessagePayload(query="mutation { hello }"), - ).as_dict() - ) - - response = ws.receive_json() - assert ( - response - == NextMessage( - id="sub1", payload={"data": {"hello": "strawberry"}} - ).as_dict() - ) - - response = ws.receive_json() - assert response == CompleteMessage(id="sub1").as_dict() - - -def test_single_result_operation_selection(test_client): - with test_client.websocket_connect( - "/graphql", [GRAPHQL_TRANSPORT_WS_PROTOCOL] - ) as ws: - ws.send_json(ConnectionInitMessage().as_dict()) - - response = ws.receive_json() - assert response == ConnectionAckMessage().as_dict() - - query = """ - query Query1 { - hello - } - query Query2 { - hello(name: "Strawberry") - } - """ - - ws.send_json( - SubscribeMessage( - id="sub1", - payload=SubscribeMessagePayload(query=query, operationName="Query2"), - ).as_dict() - ) - - response = ws.receive_json() - assert ( - response - == NextMessage( - id="sub1", payload={"data": {"hello": "Hello Strawberry"}} - ).as_dict() - ) - - response = ws.receive_json() - assert response == CompleteMessage(id="sub1").as_dict() - - -def test_single_result_invalid_operation_selection(test_client): - with test_client.websocket_connect( - "/graphql", [GRAPHQL_TRANSPORT_WS_PROTOCOL] - ) as ws: - ws.send_json(ConnectionInitMessage().as_dict()) - - response = ws.receive_json() - assert response == ConnectionAckMessage().as_dict() - - query = """ - query Query1 { - hello - } - """ - - ws.send_json( - SubscribeMessage( - id="sub1", - payload=SubscribeMessagePayload(query=query, operationName="Query2"), - ).as_dict() - ) - - data = ws.receive() - assert data["type"] == "websocket.close" - assert data["code"] == 4400 - - -def test_single_result_operation_error(test_client): - with test_client.websocket_connect( - "/graphql", [GRAPHQL_TRANSPORT_WS_PROTOCOL] - ) as ws: - ws.send_json(ConnectionInitMessage().as_dict()) - - response = ws.receive_json() - assert response == ConnectionAckMessage().as_dict() - - ws.send_json( - SubscribeMessage( - id="sub1", - payload=SubscribeMessagePayload( - query="query { alwaysFail }", - ), - ).as_dict() - ) - - response = ws.receive_json() - assert response["type"] == ErrorMessage.type - assert response["id"] == "sub1" - assert len(response["payload"]) == 1 - assert response["payload"][0]["message"] == "You are not authorized" - - -def test_single_result_operation_exception(test_client): - """ - Test that single-result-operations which raise exceptions - behave in the same way as streaming operations - """ - with test_client.websocket_connect( - "/graphql", [GRAPHQL_TRANSPORT_WS_PROTOCOL] - ) as ws: - ws.send_json(ConnectionInitMessage().as_dict()) - - response = ws.receive_json() - assert response == ConnectionAckMessage().as_dict() - - ws.send_json( - SubscribeMessage( - id="sub1", - payload=SubscribeMessagePayload( - query='query { exception(message: "bummer") }', - ), - ).as_dict() - ) - - response = ws.receive_json() - assert response["type"] == ErrorMessage.type - assert response["id"] == "sub1" - assert len(response["payload"]) == 1 - assert response["payload"][0].get("path") == ["exception"] - assert response["payload"][0]["message"] == "bummer" - - -def test_single_result_duplicate_ids_sub(test_client): - """ - Test that single-result-operations and streaming operations - share the same ID namespace. Start a regular subscription, - then issue a single-result operation with same ID and expect an - error due to already existing ID - """ - with test_client.websocket_connect( - "/graphql", [GRAPHQL_TRANSPORT_WS_PROTOCOL] - ) as ws: - ws.send_json(ConnectionInitMessage().as_dict()) - - response = ws.receive_json() - assert response == ConnectionAckMessage().as_dict() - - # regular subscription - ws.send_json( - SubscribeMessage( - id="sub1", - payload=SubscribeMessagePayload( - query='subscription { echo(message: "Hi", delay: 5) }' - ), - ).as_dict() - ) - # single result subscription with duplicate id - ws.send_json( - SubscribeMessage( - id="sub1", - payload=SubscribeMessagePayload( - query="query { hello }", - ), - ).as_dict() - ) - - data = ws.receive() - assert data["type"] == "websocket.close" - assert data["code"] == 4409 - - -def test_single_result_duplicate_ids_query(test_client): - """ - Test that single-result-operations don't allow duplicate - IDs for two asynchronous queries. Issue one async query - with delay, then another with same id. Expect error. - """ - with test_client.websocket_connect( - "/graphql", [GRAPHQL_TRANSPORT_WS_PROTOCOL] - ) as ws: - ws.send_json(ConnectionInitMessage().as_dict()) - - response = ws.receive_json() - assert response == ConnectionAckMessage().as_dict() - - # single result subscription 1 - ws.send_json( - SubscribeMessage( - id="sub1", - payload=SubscribeMessagePayload( - query='query { asyncHello(name: "Hi", delay: 5) }' - ), - ).as_dict() - ) - # single result subscription with duplicate id - ws.send_json( - SubscribeMessage( - id="sub1", - payload=SubscribeMessagePayload( - query="query { hello }", - ), - ).as_dict() - ) - - # We expect the remote to close the socket due to duplicate ID in use - data = ws.receive() - assert data["type"] == "websocket.close" - assert data["code"] == 4409 - - -def test_injects_connection_params(test_client): - with test_client.websocket_connect("/", [GRAPHQL_TRANSPORT_WS_PROTOCOL]) as ws: - ws.send_json(ConnectionInitMessage(payload={"strawberry": "rocks"}).as_dict()) - - response = ws.receive_json() - assert response == ConnectionAckMessage().as_dict() - - ws.send_json( - SubscribeMessage( - id="sub1", - payload=SubscribeMessagePayload( - query="subscription { connectionParams }" - ), - ).as_dict() - ) - - response = ws.receive_json() - assert ( - response - == NextMessage( - id="sub1", payload={"data": {"connectionParams": "rocks"}} - ).as_dict() - ) - - ws.send_json(CompleteMessage(id="sub1").as_dict()) - - -def test_rejects_connection_params_not_dict(test_client): - with test_client.websocket_connect("/", [GRAPHQL_TRANSPORT_WS_PROTOCOL]) as ws: - ws.send_json(ConnectionInitMessage(payload="gonna fail").as_dict()) - - data = ws.receive() - assert data["type"] == "websocket.close" - assert data["code"] == 4400 - - -def test_rejects_connection_params_not_unset(test_client): - with test_client.websocket_connect("/", [GRAPHQL_TRANSPORT_WS_PROTOCOL]) as ws: - ws.send_json(ConnectionInitMessage(payload=None).as_dict()) - - data = ws.receive() - assert data["type"] == "websocket.close" - assert data["code"] == 4400 diff --git a/tests/asgi/test_graphql_ws.py b/tests/asgi/test_graphql_ws.py index 8c4bf984d8..3bc17e6244 100644 --- a/tests/asgi/test_graphql_ws.py +++ b/tests/asgi/test_graphql_ws.py @@ -1,11 +1,11 @@ import pytest + from starlette.websockets import WebSocketDisconnect from strawberry.subscriptions import GRAPHQL_WS_PROTOCOL from strawberry.subscriptions.protocols.graphql_ws import ( GQL_COMPLETE, GQL_CONNECTION_ACK, - GQL_CONNECTION_ERROR, GQL_CONNECTION_INIT, GQL_CONNECTION_KEEP_ALIVE, GQL_CONNECTION_TERMINATE, @@ -234,7 +234,9 @@ def test_subscription_exceptions(test_client): assert response["type"] == GQL_DATA assert response["id"] == "demo" assert response["payload"]["data"] is None - assert response["payload"]["errors"] == [{"message": "TEST EXC"}] + assert response["payload"]["errors"] == [ + {"locations": None, "message": "TEST EXC", "path": None} + ] ws.send_json({"type": GQL_STOP, "id": "demo"}) response = ws.receive_json() @@ -267,6 +269,7 @@ def test_subscription_field_error(test_client): assert response["id"] == "invalid-field" assert response["payload"] == { "locations": [{"line": 1, "column": 16}], + "path": None, "message": ( "The subscription field 'notASubscriptionField' is not defined." ), @@ -298,6 +301,7 @@ def test_subscription_syntax_error(test_client): assert response["id"] == "syntax-error" assert response["payload"] == { "locations": [{"line": 1, "column": 24}], + "path": None, "message": "Syntax Error: Expected Name, found .", } @@ -515,62 +519,3 @@ def test_task_cancellation_separation(test_client): response = ws1.receive_json() assert response["type"] == GQL_COMPLETE assert response["id"] == "debug1" - - -def test_injects_connection_params(test_client): - with test_client.websocket_connect("/", [GRAPHQL_WS_PROTOCOL]) as ws: - ws.send_json( - { - "type": GQL_CONNECTION_INIT, - "id": "demo", - "payload": { - "strawberry": "rocks", - }, - } - ) - ws.send_json( - { - "type": GQL_START, - "id": "demo", - "payload": { - "query": "subscription { connectionParams }", - }, - } - ) - - response = ws.receive_json() - assert response["type"] == GQL_CONNECTION_ACK - - response = ws.receive_json() - assert response["type"] == GQL_DATA - assert response["id"] == "demo" - assert response["payload"]["data"] == {"connectionParams": "rocks"} - - ws.send_json({"type": GQL_STOP, "id": "demo"}) - response = ws.receive_json() - assert response["type"] == GQL_COMPLETE - assert response["id"] == "demo" - - ws.send_json({"type": GQL_CONNECTION_TERMINATE}) - - # make sure the websocket is disconnected now - with pytest.raises(WebSocketDisconnect): - ws.receive_json() - - -def test_rejects_connection_params(test_client): - with test_client.websocket_connect("/", [GRAPHQL_WS_PROTOCOL]) as ws: - ws.send_json( - { - "type": GQL_CONNECTION_INIT, - "id": "demo", - "payload": "gonna fail", - } - ) - - response = ws.receive_json() - assert response["type"] == GQL_CONNECTION_ERROR - - # make sure the websocket is disconnected now - with pytest.raises(WebSocketDisconnect): - ws.receive_json() diff --git a/tests/asgi/test_http.py b/tests/asgi/test_http.py new file mode 100644 index 0000000000..cb3c7fc5f3 --- /dev/null +++ b/tests/asgi/test_http.py @@ -0,0 +1,22 @@ +import pytest + +from starlette import status + + +def test_returns_error_when_missing_query(test_client): + response = test_client.post("/", json={}) + + assert response.status_code == status.HTTP_400_BAD_REQUEST + + +def test_returns_error_when_not_sending_wrong_content_type(test_client): + response = test_client.post("/", data="Example") + + assert response.status_code == status.HTTP_415_UNSUPPORTED_MEDIA_TYPE + + +@pytest.mark.parametrize("method", ("PUT", "DELETE")) +def test_returns_error_when_method_is_not_allowed(method, test_client): + response = test_client.request(method, "/", json={}) + + assert response.status_code == status.HTTP_405_METHOD_NOT_ALLOWED diff --git a/tests/asgi/test_query.py b/tests/asgi/test_query.py new file mode 100644 index 0000000000..aa213c9f22 --- /dev/null +++ b/tests/asgi/test_query.py @@ -0,0 +1,175 @@ +from starlette.background import BackgroundTask +from starlette.testclient import TestClient + +import strawberry +from strawberry.asgi import GraphQL as BaseGraphQL +from strawberry.types import ExecutionResult, Info + + +def test_simple_query(graphql_client): + query = "{ hello }" + + response = graphql_client.query(query=query) + + assert response.data == {"hello": "Hello world"} + + +def test_fails_when_request_body_has_invalid_json(test_client): + response = test_client.post( + "/", data='{"qeury": "{__typena"', headers={"content-type": "application/json"} + ) + assert response.status_code == 400 + + +def test_returns_errors(graphql_client): + query = "{ donut }" + + response = graphql_client.query(query=query, asserts_errors=False) + + assert response.errors == [ + { + "locations": [{"column": 3, "line": 1}], + "message": "Cannot query field 'donut' on type 'Query'.", + "path": None, + } + ] + + +def test_can_pass_variables(graphql_client): + query = "query Hello($name: String!) { hello(name: $name) }" + + response = graphql_client.query(query=query, variables={"name": "James"}) + + assert response.data == {"hello": "Hello James"} + + +def test_returns_errors_and_data(graphql_client): + query = "{ hello, alwaysFail }" + + response = graphql_client.query(query=query, asserts_errors=False) + + assert response.data == {"hello": "Hello world", "alwaysFail": None} + assert response.errors == [ + { + "locations": [{"column": 10, "line": 1}], + "message": "You are not authorized", + "path": ["alwaysFail"], + } + ] + + +def test_root_value(graphql_client): + query = "{ rootName }" + + response = graphql_client.query(query=query) + + assert response.data == {"rootName": "Query"} + + +def test_context_response(): + @strawberry.type + class Query: + @strawberry.field + def something(self, info: Info) -> str: + r = info.context["response"] + r.raw_headers.append((b"x-bar", b"bar")) + return "foo" + + schema = strawberry.Schema(query=Query) + app = BaseGraphQL(schema) + + test_client = TestClient(app) + response = test_client.post("/", json={"query": "{ something }"}) + + assert response.status_code == 200 + assert response.json() == {"data": {"something": "foo"}} + assert response.headers.get("x-bar") == "bar" + + +def test_can_set_custom_status_code(): + @strawberry.type + class Query: + @strawberry.field + def something(self, info: Info) -> str: + r = info.context["response"] + r.status_code = 418 + return "foo" + + schema = strawberry.Schema(query=Query) + app = BaseGraphQL(schema) + + test_client = TestClient(app) + response = test_client.post("/", json={"query": "{ something }"}) + + assert response.status_code == 418 + assert response.json() == {"data": {"something": "foo"}} + + +def test_can_set_background_task(): + task_complete = False + + def task(): + nonlocal task_complete + task_complete = True + + @strawberry.type + class Query: + @strawberry.field + def something(self, info: Info) -> str: + r = info.context["response"] + r.background = BackgroundTask(task) + return "foo" + + schema = strawberry.Schema(query=Query) + app = BaseGraphQL(schema) + + test_client = TestClient(app) + response = test_client.post("/", json={"query": "{ something }"}) + + assert response.json() == {"data": {"something": "foo"}} + assert task_complete + + +def test_custom_context(): + class CustomGraphQL(BaseGraphQL): + async def get_context(self, request, response): + return { + "request": request, + "custom_context_value": "Hi!", + } + + @strawberry.type + class Query: + @strawberry.field + def custom_context_value(self, info: Info) -> str: + return info.context["custom_context_value"] + + schema = strawberry.Schema(query=Query) + app = CustomGraphQL(schema) + + test_client = TestClient(app) + response = test_client.post("/", json={"query": "{ customContextValue }"}) + + assert response.status_code == 200 + assert response.json() == {"data": {"customContextValue": "Hi!"}} + + +def test_custom_process_result(): + class CustomGraphQL(BaseGraphQL): + async def process_result(self, request, result: ExecutionResult): + return {} + + @strawberry.type + class Query: + @strawberry.field + def abc(self) -> str: + return "ABC" + + schema = strawberry.Schema(query=Query) + app = CustomGraphQL(schema) + + test_client = TestClient(app) + response = test_client.post("/", json={"query": "{ abc }"}) + + assert response.status_code == 200 + assert response.json() == {} diff --git a/tests/asgi/test_upload.py b/tests/asgi/test_upload.py new file mode 100644 index 0000000000..1daf4054a1 --- /dev/null +++ b/tests/asgi/test_upload.py @@ -0,0 +1,71 @@ +from io import BytesIO + + +def test_single_file_upload(graphql_client): + f = BytesIO(b"strawberry") + + query = """mutation($textFile: Upload!) { + readText(textFile: $textFile) + }""" + + response = graphql_client.query( + query=query, + variables={"textFile": None}, + files={"textFile": f}, + ) + + assert response.data["readText"] == "strawberry" + + +def test_file_list_upload(graphql_client): + query = "mutation($files: [Upload!]!) { readFiles(files: $files) }" + file1 = BytesIO(b"strawberry1") + file2 = BytesIO(b"strawberry2") + + response = graphql_client.query( + query=query, + variables={"files": [None, None]}, + files={"file1": file1, "file2": file2}, + ) + assert len(response.data["readFiles"]) == 2 + assert response.data["readFiles"][0] == "strawberry1" + assert response.data["readFiles"][1] == "strawberry2" + + +def test_nested_file_list(graphql_client): + query = "mutation($folder: FolderInput!) { readFolder(folder: $folder) }" + file1 = BytesIO(b"strawberry1") + file2 = BytesIO(b"strawberry2") + + response = graphql_client.query( + query=query, + variables={"folder": {"files": [None, None]}}, + files={"file1": file1, "file2": file2}, + ) + + assert len(response.data["readFolder"]) == 2 + assert response.data["readFolder"][0] == "strawberry1" + assert response.data["readFolder"][1] == "strawberry2" + + +def test_upload_single_and_list_file_together(graphql_client): + query = """ + mutation($files: [Upload!]!, $textFile: Upload!) { + readFiles(files: $files) + readText(textFile: $textFile) + } + """ + file1 = BytesIO(b"strawberry1") + file2 = BytesIO(b"strawberry2") + file3 = BytesIO(b"strawberry3") + + response = graphql_client.query( + query=query, + variables={"files": [None, None], "textFile": None}, + files={"file1": file1, "file2": file2, "textFile": file3}, + ) + + assert len(response.data["readFiles"]) == 2 + assert response.data["readFiles"][0] == "strawberry1" + assert response.data["readFiles"][1] == "strawberry2" + assert response.data["readText"] == "strawberry3" diff --git a/tests/asgi/test_websockets.py b/tests/asgi/test_view.py similarity index 81% rename from tests/asgi/test_websockets.py rename to tests/asgi/test_view.py index 8b4566d747..eebc3a26dc 100644 --- a/tests/asgi/test_websockets.py +++ b/tests/asgi/test_view.py @@ -1,4 +1,6 @@ import pytest + +from starlette import status from starlette.testclient import TestClient from starlette.websockets import WebSocketDisconnect @@ -6,6 +8,22 @@ from tests.asgi.app import create_app +@pytest.mark.parametrize("path", ("/", "/graphql")) +def test_renders_graphiql(path, test_client): + response = test_client.get(path) + + assert response.status_code == status.HTTP_200_OK + + assert "Strawberry GraphiQL" in response.text + + +@pytest.mark.parametrize("path", ("/", "/graphql")) +def test_renders_graphiql_disabled(path, test_client_no_graphiql): + response = test_client_no_graphiql.get(path) + + assert response.status_code == status.HTTP_404_NOT_FOUND + + def test_turning_off_graphql_ws(): app = create_app(subscription_protocols=[GRAPHQL_TRANSPORT_WS_PROTOCOL]) test_client = TestClient(app) diff --git a/tests/benchmarks/test_execute.py b/tests/benchmarks/test_execute.py index 386bed25ce..df9462bf5a 100644 --- a/tests/benchmarks/test_execute.py +++ b/tests/benchmarks/test_execute.py @@ -4,11 +4,13 @@ from typing import List import pytest + from asgiref.sync import async_to_sync import strawberry +@pytest.mark.asyncio @pytest.mark.parametrize( "items", [25, 100, 250], diff --git a/tests/chalice/app.py b/tests/chalice/app.py index b8446f95d8..70c4003e28 100644 --- a/tests/chalice/app.py +++ b/tests/chalice/app.py @@ -1,9 +1,8 @@ -from typing import Any, Optional - import strawberry from chalice import Chalice # type: ignore +from chalice.app import Request from strawberry.chalice.views import GraphQLView -from strawberry.types.info import Info + app = Chalice(app_name="TheStackBadger") @@ -14,16 +13,6 @@ class Query: def greetings(self) -> str: return "hello" - @strawberry.field - def hello(self, name: Optional[str] = None) -> str: - return f"Hello {name or 'world'}" - - @strawberry.field - def teapot(self, info: Info[Any, None]) -> str: - info.context["response"].status_code = 418 - - return "๐Ÿซ–" - @strawberry.type class Mutation: @@ -33,9 +22,7 @@ def echo(self, string_to_echo: str) -> str: schema = strawberry.Schema(query=Query, mutation=Mutation) -view = GraphQLView(schema=schema, graphiql=True, allow_queries_via_get=True) -view_no_graphiql = GraphQLView(schema=schema, graphiql=False) -view_not_get = GraphQLView(schema=schema, graphiql=False, allow_queries_via_get=False) +view = GraphQLView(schema=schema, render_graphiql=True) @app.route("/") @@ -45,22 +32,6 @@ def index(): @app.route("/graphql", methods=["GET", "POST"], content_types=["application/json"]) def handle_graphql(): - return view.execute_request(app.current_request) - - -@app.route( - "/graphql-no-graphiql", - methods=["GET", "POST", "PUT"], - content_types=["application/json"], -) -def handle_graphql_without_graphiql(): - return view_no_graphiql.execute_request(app.current_request) - - -@app.route( - "/graphql-no-get", - methods=["GET", "POST", "PUT"], - content_types=["application/json"], -) -def handle_graphql_without_queries_via_get(): - return view_not_get.execute_request(app.current_request) + request: Request = app.current_request + result = view.execute_request(request) + return result diff --git a/tests/chalice/test_views.py b/tests/chalice/test_views.py new file mode 100644 index 0000000000..2f244daa7f --- /dev/null +++ b/tests/chalice/test_views.py @@ -0,0 +1,96 @@ +import json +from http import HTTPStatus + +import pytest + +from chalice.test import Client, HTTPResponse + +from .app import app + + +def test_chalice_server_index_route_returns(): + with Client(app) as client: + response = client.http.get("/") + assert response.status_code == HTTPStatus.OK + assert response.json_body == {"strawberry": "cake"} + + +@pytest.mark.parametrize("header_value", ["text/html", "*/*"]) +def test_graphiql_view_is_returned_if_accept_is_html_or_accept_all(header_value): + with Client(app) as client: + headers = {"Accept": header_value} + response = client.http.get("/graphql", headers=headers) + assert response.status_code == HTTPStatus.OK + assert "GraphiQL" in str(response.body) + + +def test_graphiql_view_is_not_returned_if_accept_headers_is_none(): + with Client(app) as client: + response = client.http.get("/graphql", headers=None) + assert response.status_code == HTTPStatus.OK + assert response_is_of_error_type(response) + + +def response_is_of_error_type(response: HTTPResponse) -> bool: + if "errors" in response.json_body.keys(): + if response.status_code == HTTPStatus.OK: + return True + return False + + +def test_get_graphql_view_with_json_accept_type_is_rejected(): + with Client(app) as client: + headers = {"Accept": "application/json"} + response = client.http.get("/graphql", headers=headers) + assert response.status_code == HTTPStatus.OK + assert response_is_of_error_type(response) + + +def test_malformed_unparsable_json_query_returns_error(): + with Client(app) as client: + headers = {"Accept": "application/json", "Content-Type": "application/json"} + + # The query key in the json dict is missing + query = "I am a malformed query" + response = client.http.post("/graphql", headers=headers, body=json.dumps(query)) + assert response_is_of_error_type(response) + + +# These tests are checking that graphql is getting routed through the endpoint +# correctly to the strawberry execute_sync command, so just test one happy path case of a +# query and a mutation +def test_graphiql_query(): + with Client(app) as client: + headers = {"Accept": "application/json", "Content-Type": "application/json"} + + query = {"query": "query GreetMe {greetings}"} + response = client.http.post("/graphql", headers=headers, body=json.dumps(query)) + assert response.status_code == HTTPStatus.OK + assert not response_is_of_error_type(response) + assert "hello" == response.json_body["data"]["greetings"] + + +def test_graphiql_mutation(): + with Client(app) as client: + headers = {"Accept": "application/json", "Content-Type": "application/json"} + query = {"query": """mutation EchoMutation { echo(stringToEcho: "mark")} """} + response = client.http.post("/graphql", headers=headers, body=json.dumps(query)) + assert response.status_code == HTTPStatus.OK + assert "mark" == response.json_body["data"]["echo"] + + +def test_graphiql_query_with_malformed_json_gives_http_ok_and_bad_request_message(): + with Client(app) as client: + headers = {"Accept": "application/json", "Content-Type": "application/json"} + + response = client.http.post("/graphql", headers=headers, body="{invalidjson") + assert response.status_code == HTTPStatus.OK + assert response_is_of_error_type(response) + + +def test_graphiql_query_with_no_request_body(): + with Client(app) as client: + headers = {"Accept": "application/json"} + response = client.http.post("/graphql", headers=headers, body="") + assert response.status_code == HTTPStatus.OK + assert response_is_of_error_type(response) diff --git a/tests/channels/__init__.py b/tests/channels/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/channels/conftest.py b/tests/channels/conftest.py deleted file mode 100644 index 95442c5677..0000000000 --- a/tests/channels/conftest.py +++ /dev/null @@ -1,15 +0,0 @@ -from pathlib import Path - -import pytest - - -def pytest_collection_modifyitems(config, items): - # automatically mark tests with 'channels' if they are in the channels subfolder - - rootdir = Path(config.rootdir) - - for item in items: - rel_path = Path(item.fspath).relative_to(rootdir) - - if str(rel_path).startswith("tests/channels"): - item.add_marker(pytest.mark.channels) diff --git a/tests/channels/schema.py b/tests/channels/schema.py deleted file mode 100644 index e0763fafa0..0000000000 --- a/tests/channels/schema.py +++ /dev/null @@ -1,164 +0,0 @@ -import asyncio -import typing -from enum import Enum -from typing import Any, Optional - -from graphql import GraphQLError - -import strawberry -from strawberry.channels.context import StrawberryChannelsContext -from strawberry.file_uploads import Upload -from strawberry.permission import BasePermission -from strawberry.subscriptions.protocols.graphql_transport_ws.types import PingMessage -from strawberry.types import Info - - -class AlwaysFailPermission(BasePermission): - message = "You are not authorized" - - def has_permission(self, source: typing.Any, info: Info, **kwargs) -> bool: - return False - - -@strawberry.enum -class Flavor(Enum): - VANILLA = "vanilla" - STRAWBERRY = "strawberry" - CHOCOLATE = "chocolate" - - -@strawberry.input -class FolderInput: - files: typing.List[Upload] - - -@strawberry.type -class DebugInfo: - num_active_result_handlers: int - is_connection_init_timeout_task_done: typing.Optional[bool] - - -@strawberry.type -class Query: - @strawberry.field - def hello(self, name: typing.Optional[str] = None) -> str: - return f"Hello {name or 'world'}" - - @strawberry.field(permission_classes=[AlwaysFailPermission]) - def always_fail(self) -> Optional[str]: - return "Hey" - - @strawberry.field - def root_name(root) -> str: - return type(root).__name__ - - -@strawberry.type -class Mutation: - @strawberry.field - def hello(self, name: typing.Optional[str] = None) -> str: - return f"Hello {name or 'world'}" - - @strawberry.mutation - async def read_text(self, text_file: Upload) -> str: - return (await text_file.read()).decode() - - @strawberry.mutation - async def read_files(self, files: typing.List[Upload]) -> typing.List[str]: - contents = [] - for file in files: - content = (await file.read()).decode() - contents.append(content) - return contents - - @strawberry.mutation - async def read_folder(self, folder: FolderInput) -> typing.List[str]: - contents = [] - for file in folder.files: - content = (await file.read()).decode() - contents.append(content) - return contents - - -@strawberry.type -class Subscription: - @strawberry.subscription - async def echo( - self, message: str, delay: float = 0 - ) -> typing.AsyncGenerator[str, None]: - await asyncio.sleep(delay) - yield message - - @strawberry.subscription - async def request_ping(self, info) -> typing.AsyncGenerator[bool, None]: - ws = info.context.ws - await ws.send_json(PingMessage().as_dict()) - yield True - - @strawberry.subscription - async def infinity(self, message: str) -> typing.AsyncGenerator[str, None]: - while True: - yield message - await asyncio.sleep(1) - - @strawberry.subscription - async def context(self, info) -> typing.AsyncGenerator[str, None]: - yield info.context.custom_value - - @strawberry.subscription - async def error(self, message: str) -> typing.AsyncGenerator[str, None]: - yield GraphQLError(message) # type: ignore - - @strawberry.subscription - async def exception(self, message: str) -> typing.AsyncGenerator[str, None]: - raise ValueError(message) - - # Without this yield, the method is not recognised as an async generator - yield "Hi" - - @strawberry.subscription - async def flavors(self) -> typing.AsyncGenerator[Flavor, None]: - yield Flavor.VANILLA - yield Flavor.STRAWBERRY - yield Flavor.CHOCOLATE - - @strawberry.subscription - async def debug(self, info) -> typing.AsyncGenerator[DebugInfo, None]: - active_result_handlers = [ - task for task in info.context.tasks.values() if not task.done() - ] - - connection_init_timeout_task = info.context.connectionInitTimeoutTask - is_connection_init_timeout_task_done = ( - connection_init_timeout_task.done() - if connection_init_timeout_task - else None - ) - - yield DebugInfo( - num_active_result_handlers=len(active_result_handlers), - is_connection_init_timeout_task_done=is_connection_init_timeout_task_done, - ) - - @strawberry.subscription - async def listener( - self, - info: Info[StrawberryChannelsContext, Any], - timeout: Optional[float] = None, - group: Optional[str] = None, - ) -> typing.AsyncGenerator[str, None]: - yield info.context.request.channel_name - - async for message in info.context.request.channel_listen( - type="test.message", - timeout=timeout, - groups=[group] if group is not None else [], - ): - yield message["text"] - - @strawberry.subscription - async def connection_params(self, info: Info) -> typing.AsyncGenerator[str, None]: - yield info.context.connection_params["strawberry"] - - -schema = strawberry.Schema(Query, mutation=Mutation, subscription=Subscription) diff --git a/tests/channels/test_graphql_transport_ws.py b/tests/channels/test_graphql_transport_ws.py deleted file mode 100644 index 9875ed50ea..0000000000 --- a/tests/channels/test_graphql_transport_ws.py +++ /dev/null @@ -1,464 +0,0 @@ -import asyncio -import json -from datetime import timedelta - -import pytest - -from channels.testing import WebsocketCommunicator -from strawberry.channels import GraphQLWSConsumer -from strawberry.subscriptions import GRAPHQL_TRANSPORT_WS_PROTOCOL -from strawberry.subscriptions.protocols.graphql_transport_ws.types import ( - CompleteMessage, - ConnectionAckMessage, - ConnectionInitMessage, - ErrorMessage, - NextMessage, - PingMessage, - PongMessage, - SubscribeMessage, - SubscribeMessagePayload, -) -from tests.channels.schema import schema - -pytestmark = [ - pytest.mark.asyncio, -] - - -class DebuggableGraphQLTransportWSConsumer(GraphQLWSConsumer): - async def get_context(self, *args, **kwargs) -> object: - context = await super().get_context(*args, **kwargs) - context.tasks = self._handler.tasks - context.connectionInitTimeoutTask = self._handler.connection_init_timeout_task - return context - - -@pytest.fixture -async def ws(): - client = WebsocketCommunicator( - DebuggableGraphQLTransportWSConsumer.as_asgi( - schema=schema, subscription_protocols=(GRAPHQL_TRANSPORT_WS_PROTOCOL,) - ), - "/graphql", - subprotocols=[ - GRAPHQL_TRANSPORT_WS_PROTOCOL, - ], - ) - res = await client.connect() - assert res == (True, GRAPHQL_TRANSPORT_WS_PROTOCOL) - - yield client - - await client.disconnect() - - -async def test_unknown_message_type(ws): - await ws.send_json_to({"type": "NOT_A_MESSAGE_TYPE"}) - data = await ws.receive_output() - - assert data["type"] == "websocket.close" - assert data["code"] == 4400 - - -async def test_missing_message_type(ws): - await ws.send_json_to({"notType": None}) - - data = await ws.receive_output() - assert data["type"] == "websocket.close" - assert data["code"] == 4400 - - -async def test_parsing_an_invalid_message(ws): - await ws.send_json_to({"type": "subscribe", "notPayload": None}) - - data = await ws.receive_output() - assert data["type"] == "websocket.close" - assert data["code"] == 4400 - - -async def test_parsing_an_invalid_payload(ws): - await ws.send_json_to({"type": "subscribe", "payload": {"unexpectedField": 42}}) - - data = await ws.receive_output() - assert data["type"] == "websocket.close" - assert data["code"] == 4400 - - -async def test_ws_messages_must_be_text(ws): - await ws.send_to(bytes_data=json.dumps(ConnectionInitMessage().as_dict()).encode()) - - data = await ws.receive_output() - assert data["type"] == "websocket.close" - assert data["code"] == 4400 - - -async def test_connection_init_timeout(): - client = WebsocketCommunicator( - GraphQLWSConsumer.as_asgi( - schema=schema, - connection_init_wait_timeout=timedelta(seconds=0), - subscription_protocols=(GRAPHQL_TRANSPORT_WS_PROTOCOL,), - ), - "/graphql", - subprotocols=[ - GRAPHQL_TRANSPORT_WS_PROTOCOL, - ], - ) - await asyncio.sleep(0.1) - # Hope that the connection init timeout expired - res = await client.connect() - assert res == (True, GRAPHQL_TRANSPORT_WS_PROTOCOL) - - data = await client.receive_output() - assert data["type"] == "websocket.close" - assert data["code"] == 4408 - - -async def test_connection_init_timeout_cancellation(): - client = WebsocketCommunicator( - DebuggableGraphQLTransportWSConsumer.as_asgi( - schema=schema, - connection_init_wait_timeout=timedelta(milliseconds=500), - subscription_protocols=(GRAPHQL_TRANSPORT_WS_PROTOCOL,), - ), - "/graphql", - subprotocols=[ - GRAPHQL_TRANSPORT_WS_PROTOCOL, - ], - ) - await client.connect() - await client.send_json_to(ConnectionInitMessage().as_dict()) - - response = await client.receive_json_from() - assert response == ConnectionAckMessage().as_dict() - - await asyncio.sleep(1) - - await client.send_json_to( - SubscribeMessage( - id="sub1", - payload=SubscribeMessagePayload( - query="subscription { debug { isConnectionInitTimeoutTaskDone } }" - ), - ).as_dict() - ) - - response = await client.receive_json_from() - assert ( - response - == NextMessage( - id="sub1", - payload={"data": {"debug": {"isConnectionInitTimeoutTaskDone": True}}}, - ).as_dict() - ) - - -async def test_too_many_initialisation_requests(ws): - await ws.send_json_to(ConnectionInitMessage().as_dict()) - - response = await ws.receive_json_from() - assert response == ConnectionAckMessage().as_dict() - - await ws.send_json_to(ConnectionInitMessage().as_dict()) - - data = await ws.receive_output() - assert data["type"] == "websocket.close" - assert data["code"] == 4429 - - -async def test_ping_pong(ws): - await ws.send_json_to(ConnectionInitMessage().as_dict()) - - response = await ws.receive_json_from() - assert response == ConnectionAckMessage().as_dict() - - await ws.send_json_to(PingMessage().as_dict()) - - response = await ws.receive_json_from() - assert response == PongMessage().as_dict() - - -async def test_server_sent_ping(ws): - await ws.send_json_to(ConnectionInitMessage().as_dict()) - - response = await ws.receive_json_from() - assert response == ConnectionAckMessage().as_dict() - - await ws.send_json_to( - SubscribeMessage( - id="sub1", - payload=SubscribeMessagePayload(query="subscription { requestPing }"), - ).as_dict() - ) - - response = await ws.receive_json_from() - assert response == PingMessage().as_dict() - - await ws.send_json_to(PongMessage().as_dict()) - - response = await ws.receive_json_from() - assert ( - response - == NextMessage(id="sub1", payload={"data": {"requestPing": True}}).as_dict() - ) - - response = await ws.receive_json_from() - assert response == CompleteMessage(id="sub1").as_dict() - - -async def test_unauthorized_subscriptions(ws): - await ws.send_json_to( - SubscribeMessage( - id="sub1", - payload=SubscribeMessagePayload( - query='subscription { echo(message: "Hi") }' - ), - ).as_dict() - ) - - data = await ws.receive_output() - assert data["type"] == "websocket.close" - assert data["code"] == 4401 - - -async def test_duplicated_operation_ids(ws): - await ws.send_json_to(ConnectionInitMessage().as_dict()) - - response = await ws.receive_json_from() - assert response == ConnectionAckMessage().as_dict() - - await ws.send_json_to( - SubscribeMessage( - id="sub1", - payload=SubscribeMessagePayload( - query='subscription { echo(message: "Hi", delay: 5) }' - ), - ).as_dict() - ) - - await ws.send_json_to( - SubscribeMessage( - id="sub1", - payload=SubscribeMessagePayload( - query='subscription { echo(message: "Hi", delay: 5) }' - ), - ).as_dict() - ) - - data = await ws.receive_output() - assert data["type"] == "websocket.close" - assert data["code"] == 4409 - - -async def test_simple_subscription(ws): - await ws.send_json_to(ConnectionInitMessage().as_dict()) - - response = await ws.receive_json_from() - assert response == ConnectionAckMessage().as_dict() - - await ws.send_json_to( - SubscribeMessage( - id="sub1", - payload=SubscribeMessagePayload( - query='subscription { echo(message: "Hi") }' - ), - ).as_dict() - ) - - response = await ws.receive_json_from() - assert ( - response == NextMessage(id="sub1", payload={"data": {"echo": "Hi"}}).as_dict() - ) - - await ws.send_json_to(CompleteMessage(id="sub1").as_dict()) - - -async def test_subscription_syntax_error(ws): - await ws.send_json_to(ConnectionInitMessage().as_dict()) - - response = await ws.receive_json_from() - assert response == ConnectionAckMessage().as_dict() - - await ws.send_json_to( - SubscribeMessage( - id="sub1", - payload=SubscribeMessagePayload(query="subscription { INVALID_SYNTAX "), - ).as_dict() - ) - - # An invalid syntax will close the websocket - response = await ws.receive_output(timeout=2) - assert response == { - "type": "websocket.close", - "code": 4400, - "reason": "Syntax Error: Expected Name, found .", - } - - -async def test_subscription_field_errors(ws): - await ws.send_json_to(ConnectionInitMessage().as_dict()) - - response = await ws.receive_json_from() - assert response == ConnectionAckMessage().as_dict() - - await ws.send_json_to( - SubscribeMessage( - id="sub1", - payload=SubscribeMessagePayload( - query="subscription { notASubscriptionField }", - ), - ).as_dict() - ) - - response = await ws.receive_json_from() - assert response["type"] == ErrorMessage.type - assert response["id"] == "sub1" - assert len(response["payload"]) == 1 - assert response["payload"][0]["locations"] == [{"line": 1, "column": 16}] - assert ( - response["payload"][0]["message"] - == "The subscription field 'notASubscriptionField' is not defined." - ) - - -async def test_subscription_cancellation(ws): - await ws.send_json_to(ConnectionInitMessage().as_dict()) - - response = await ws.receive_json_from() - assert response == ConnectionAckMessage().as_dict() - - await ws.send_json_to( - SubscribeMessage( - id="sub1", - payload=SubscribeMessagePayload( - query='subscription { echo(message: "Hi", delay: 99) }' - ), - ).as_dict() - ) - - await ws.send_json_to( - SubscribeMessage( - id="sub2", - payload=SubscribeMessagePayload( - query="subscription { debug { numActiveResultHandlers } }", - ), - ).as_dict() - ) - - response = await ws.receive_json_from() - assert ( - response - == NextMessage( - id="sub2", payload={"data": {"debug": {"numActiveResultHandlers": 2}}} - ).as_dict() - ) - - response = await ws.receive_json_from() - assert response == CompleteMessage(id="sub2").as_dict() - - await ws.send_json_to(CompleteMessage(id="sub1").as_dict()) - - await ws.send_json_to( - SubscribeMessage( - id="sub3", - payload=SubscribeMessagePayload( - query="subscription { debug { numActiveResultHandlers } }", - ), - ).as_dict() - ) - - response = await ws.receive_json_from() - assert ( - response - == NextMessage( - id="sub3", payload={"data": {"debug": {"numActiveResultHandlers": 1}}} - ).as_dict() - ) - - response = await ws.receive_json_from() - assert response == CompleteMessage(id="sub3").as_dict() - - -async def test_subscription_errors(ws): - await ws.send_json_to(ConnectionInitMessage().as_dict()) - - response = await ws.receive_json_from() - assert response == ConnectionAckMessage().as_dict() - - await ws.send_json_to( - SubscribeMessage( - id="sub1", - payload=SubscribeMessagePayload( - query='subscription { error(message: "TEST ERR") }', - ), - ).as_dict() - ) - - response = await ws.receive_json_from() - assert response["type"] == ErrorMessage.type - assert response["id"] == "sub1" - assert len(response["payload"]) == 1 - assert response["payload"][0]["path"] == ["error"] - assert response["payload"][0]["message"] == "TEST ERR" - - -async def test_subscription_exceptions(ws): - await ws.send_json_to(ConnectionInitMessage().as_dict()) - - response = await ws.receive_json_from() - assert response == ConnectionAckMessage().as_dict() - - await ws.send_json_to( - SubscribeMessage( - id="sub1", - payload=SubscribeMessagePayload( - query='subscription { exception(message: "TEST EXC") }', - ), - ).as_dict() - ) - - response = await ws.receive_json_from() - assert response["type"] == ErrorMessage.type - assert response["id"] == "sub1" - assert len(response["payload"]) == 1 - assert response["payload"][0] == {"message": "TEST EXC"} - - -async def test_injects_connection_params(ws): - await ws.send_json_to( - ConnectionInitMessage(payload={"strawberry": "rocks"}).as_dict() - ) - - response = await ws.receive_json_from() - assert response == ConnectionAckMessage().as_dict() - - await ws.send_json_to( - SubscribeMessage( - id="sub1", - payload=SubscribeMessagePayload(query="subscription { connectionParams }"), - ).as_dict() - ) - - response = await ws.receive_json_from() - assert ( - response - == NextMessage( - id="sub1", payload={"data": {"connectionParams": "rocks"}} - ).as_dict() - ) - - await ws.send_json_to(CompleteMessage(id="sub1").as_dict()) - - -async def test_rejects_connection_params_not_dict(ws): - await ws.send_json_to(ConnectionInitMessage(payload="gonna fail").as_dict()) - data = await ws.receive_output() - assert data["type"] == "websocket.close" - assert data["code"] == 4400 - - -async def test_rejects_connection_params_not_unset(ws): - await ws.send_json_to(ConnectionInitMessage(payload=None).as_dict()) - data = await ws.receive_output() - assert data["type"] == "websocket.close" - assert data["code"] == 4400 diff --git a/tests/channels/test_graphql_ws.py b/tests/channels/test_graphql_ws.py deleted file mode 100644 index 2b6f020054..0000000000 --- a/tests/channels/test_graphql_ws.py +++ /dev/null @@ -1,633 +0,0 @@ -import pytest - -from channels.testing import WebsocketCommunicator -from strawberry.channels import GraphQLWSConsumer -from strawberry.subscriptions import GRAPHQL_WS_PROTOCOL -from strawberry.subscriptions.protocols.graphql_ws import ( - GQL_COMPLETE, - GQL_CONNECTION_ACK, - GQL_CONNECTION_ERROR, - GQL_CONNECTION_INIT, - GQL_CONNECTION_KEEP_ALIVE, - GQL_CONNECTION_TERMINATE, - GQL_DATA, - GQL_ERROR, - GQL_START, - GQL_STOP, -) -from tests.channels.schema import schema - -pytestmark = [ - pytest.mark.asyncio, -] - - -class DebuggableGraphQLWSConsumer(GraphQLWSConsumer): - async def get_context(self, *args, **kwargs) -> object: - context = await super().get_context(*args, **kwargs) - context.tasks = self._handler.tasks - context.connectionInitTimeoutTask = None - return context - - -@pytest.fixture -async def ws(): - client = WebsocketCommunicator( - DebuggableGraphQLWSConsumer.as_asgi( - schema=schema, subscription_protocols=(GRAPHQL_WS_PROTOCOL,) - ), - "/graphql", - subprotocols=[ - GRAPHQL_WS_PROTOCOL, - ], - ) - res = await client.connect() - assert res == (True, GRAPHQL_WS_PROTOCOL) - - yield client - - await client.disconnect() - - -async def test_simple_subscription(ws): - await ws.send_json_to({"type": GQL_CONNECTION_INIT}) - await ws.send_json_to( - { - "type": GQL_START, - "id": "demo", - "payload": { - "query": 'subscription { echo(message: "Hi") }', - }, - } - ) - - response = await ws.receive_json_from() - assert response["type"] == GQL_CONNECTION_ACK - - response = await ws.receive_json_from() - assert response["type"] == GQL_DATA - assert response["id"] == "demo" - assert response["payload"]["data"] == {"echo": "Hi"} - - await ws.send_json_to({"type": GQL_STOP, "id": "demo"}) - response = await ws.receive_json_from() - assert response["type"] == GQL_COMPLETE - assert response["id"] == "demo" - - await ws.send_json_to({"type": GQL_CONNECTION_TERMINATE}) - - # make sure the websocket is disconnected now - data = await ws.receive_output() - assert data == {"type": "websocket.close", "code": 1000} - - -async def test_operation_selection(ws): - await ws.send_json_to({"type": GQL_CONNECTION_INIT}) - await ws.send_json_to( - { - "type": GQL_START, - "id": "demo", - "payload": { - "query": """ - subscription Subscription1 { echo(message: "Hi1") } - subscription Subscription2 { echo(message: "Hi2") } - """, - "operationName": "Subscription2", - }, - } - ) - - response = await ws.receive_json_from() - assert response["type"] == GQL_CONNECTION_ACK - - response = await ws.receive_json_from() - assert response["type"] == GQL_DATA - assert response["id"] == "demo" - assert response["payload"]["data"] == {"echo": "Hi2"} - - await ws.send_json_to({"type": GQL_STOP, "id": "demo"}) - response = await ws.receive_json_from() - assert response["type"] == GQL_COMPLETE - assert response["id"] == "demo" - - await ws.send_json_to({"type": GQL_CONNECTION_TERMINATE}) - - # make sure the websocket is disconnected now - data = await ws.receive_output() - assert data == {"type": "websocket.close", "code": 1000} - - -async def test_sends_keep_alive(): - client = WebsocketCommunicator( - DebuggableGraphQLWSConsumer.as_asgi( - schema=schema, - keep_alive=True, - keep_alive_interval=0.1, - subscription_protocols=(GRAPHQL_WS_PROTOCOL,), - ), - "/graphql", - subprotocols=[ - GRAPHQL_WS_PROTOCOL, - ], - ) - res = await client.connect() - assert res == (True, GRAPHQL_WS_PROTOCOL) - await client.send_json_to({"type": GQL_CONNECTION_INIT}) - await client.send_json_to( - { - "type": GQL_START, - "id": "demo", - "payload": {"query": 'subscription { echo(message: "Hi", delay: 0.15) }'}, - } - ) - - response = await client.receive_json_from() - assert response["type"] == GQL_CONNECTION_ACK - - response = await client.receive_json_from() - assert response["type"] == GQL_CONNECTION_KEEP_ALIVE - - response = await client.receive_json_from() - assert response["type"] == GQL_CONNECTION_KEEP_ALIVE - - response = await client.receive_json_from() - assert response["type"] == GQL_DATA - assert response["id"] == "demo" - assert response["payload"]["data"] == {"echo": "Hi"} - - response = await client.receive_json_from() - assert response["type"] == GQL_COMPLETE - assert response["id"] == "demo" - - await client.send_json_to({"type": GQL_CONNECTION_TERMINATE}) - - await client.disconnect() - - -async def test_subscription_cancellation(ws): - await ws.send_json_to({"type": GQL_CONNECTION_INIT}) - response = await ws.receive_json_from() - assert response["type"] == GQL_CONNECTION_ACK - - await ws.send_json_to( - { - "type": GQL_START, - "id": "demo", - "payload": {"query": 'subscription { echo(message: "Hi", delay: 99) }'}, - } - ) - - await ws.send_json_to( - { - "type": GQL_START, - "id": "debug1", - "payload": { - "query": "subscription { debug { numActiveResultHandlers } }", - }, - } - ) - - response = await ws.receive_json_from() - assert response["type"] == GQL_DATA - assert response["id"] == "debug1" - assert response["payload"]["data"] == {"debug": {"numActiveResultHandlers": 2}} - - response = await ws.receive_json_from() - assert response["type"] == GQL_COMPLETE - assert response["id"] == "debug1" - - await ws.send_json_to({"type": GQL_STOP, "id": "demo"}) - response = await ws.receive_json_from() - assert response["type"] == GQL_COMPLETE - assert response["id"] == "demo" - - await ws.send_json_to( - { - "type": GQL_START, - "id": "debug2", - "payload": { - "query": "subscription { debug { numActiveResultHandlers} }", - }, - } - ) - - response = await ws.receive_json_from() - assert response["type"] == GQL_DATA - assert response["id"] == "debug2" - assert response["payload"]["data"] == {"debug": {"numActiveResultHandlers": 1}} - - response = await ws.receive_json_from() - assert response["type"] == GQL_COMPLETE - assert response["id"] == "debug2" - - await ws.send_json_to({"type": GQL_CONNECTION_TERMINATE}) - - # make sure the websocket is disconnected now - data = await ws.receive_output() - assert data == {"type": "websocket.close", "code": 1000} - - -async def test_subscription_errors(ws): - await ws.send_json_to({"type": GQL_CONNECTION_INIT}) - await ws.send_json_to( - { - "type": GQL_START, - "id": "demo", - "payload": {"query": 'subscription { error(message: "TEST ERR") }'}, - } - ) - - response = await ws.receive_json_from() - assert response["type"] == GQL_CONNECTION_ACK - - response = await ws.receive_json_from() - assert response["type"] == GQL_DATA - assert response["id"] == "demo" - assert response["payload"]["data"] is None - assert response["payload"]["errors"][0]["path"] == ["error"] - assert response["payload"]["errors"][0]["message"] == "TEST ERR" - - response = await ws.receive_json_from() - assert response["type"] == GQL_COMPLETE - assert response["id"] == "demo" - - await ws.send_json_to({"type": GQL_CONNECTION_TERMINATE}) - - # make sure the websocket is disconnected now - data = await ws.receive_output() - assert data == {"type": "websocket.close", "code": 1000} - - -async def test_subscription_exceptions(ws): - await ws.send_json_to({"type": GQL_CONNECTION_INIT}) - await ws.send_json_to( - { - "type": GQL_START, - "id": "demo", - "payload": {"query": 'subscription { exception(message: "TEST EXC") }'}, - } - ) - - response = await ws.receive_json_from() - assert response["type"] == GQL_CONNECTION_ACK - - response = await ws.receive_json_from() - assert response["type"] == GQL_DATA - assert response["id"] == "demo" - assert response["payload"]["data"] is None - assert response["payload"]["errors"] == [{"message": "TEST EXC"}] - - await ws.send_json_to({"type": GQL_STOP, "id": "demo"}) - response = await ws.receive_json_from() - assert response["type"] == GQL_COMPLETE - assert response["id"] == "demo" - - await ws.send_json_to({"type": GQL_CONNECTION_TERMINATE}) - - # make sure the websocket is disconnected now - data = await ws.receive_output() - assert data == {"type": "websocket.close", "code": 1000} - - -async def test_subscription_field_error(ws): - await ws.send_json_to({"type": GQL_CONNECTION_INIT}) - await ws.send_json_to( - { - "type": GQL_START, - "id": "invalid-field", - "payload": {"query": "subscription { notASubscriptionField }"}, - } - ) - - response = await ws.receive_json_from() - assert response["type"] == GQL_CONNECTION_ACK - - response = await ws.receive_json_from() - assert response["type"] == GQL_ERROR - assert response["id"] == "invalid-field" - assert response["payload"] == { - "locations": [{"line": 1, "column": 16}], - "message": ("The subscription field 'notASubscriptionField' is not defined."), - } - - await ws.send_json_to({"type": GQL_CONNECTION_TERMINATE}) - - # make sure the websocket is disconnected now - data = await ws.receive_output() - assert data == {"type": "websocket.close", "code": 1000} - - -async def test_subscription_syntax_error(ws): - await ws.send_json_to({"type": GQL_CONNECTION_INIT}) - await ws.send_json_to( - { - "type": GQL_START, - "id": "syntax-error", - "payload": {"query": "subscription { example "}, - } - ) - - response = await ws.receive_json_from() - assert response["type"] == GQL_CONNECTION_ACK - - response = await ws.receive_json_from() - assert response["type"] == GQL_ERROR - assert response["id"] == "syntax-error" - assert response["payload"] == { - "locations": [{"line": 1, "column": 24}], - "message": "Syntax Error: Expected Name, found .", - } - - await ws.send_json_to({"type": GQL_CONNECTION_TERMINATE}) - - # make sure the websocket is disconnected now - data = await ws.receive_output() - assert data == {"type": "websocket.close", "code": 1000} - - -async def test_non_text_ws_messages_are_ignored(ws): - await ws.send_to(bytes_data=b"foo") - await ws.send_json_to({"type": GQL_CONNECTION_INIT}) - - await ws.send_to(bytes_data=b"foo") - await ws.send_json_to( - { - "type": GQL_START, - "id": "demo", - "payload": { - "query": 'subscription { echo(message: "Hi") }', - }, - } - ) - - response = await ws.receive_json_from() - assert response["type"] == GQL_CONNECTION_ACK - - response = await ws.receive_json_from() - assert response["type"] == GQL_DATA - assert response["id"] == "demo" - assert response["payload"]["data"] == {"echo": "Hi"} - - await ws.send_to(bytes_data=b"foo") - await ws.send_json_to({"type": GQL_STOP, "id": "demo"}) - response = await ws.receive_json_from() - assert response["type"] == GQL_COMPLETE - assert response["id"] == "demo" - - await ws.send_to(bytes_data=b"foo") - await ws.send_json_to({"type": GQL_CONNECTION_TERMINATE}) - - # make sure the websocket is disconnected now - data = await ws.receive_output() - assert data == {"type": "websocket.close", "code": 1000} - - -async def test_unknown_protocol_messages_are_ignored(ws): - await ws.send_json_to({"type": "NotAProtocolMessage"}) - await ws.send_json_to({"type": GQL_CONNECTION_INIT}) - - await ws.send_json_to({"type": "NotAProtocolMessage"}) - await ws.send_json_to( - { - "type": GQL_START, - "id": "demo", - "payload": { - "query": 'subscription { echo(message: "Hi") }', - }, - } - ) - - response = await ws.receive_json_from() - assert response["type"] == GQL_CONNECTION_ACK - - response = await ws.receive_json_from() - assert response["type"] == GQL_DATA - assert response["id"] == "demo" - assert response["payload"]["data"] == {"echo": "Hi"} - - await ws.send_json_to({"type": "NotAProtocolMessage"}) - await ws.send_json_to({"type": GQL_STOP, "id": "demo"}) - response = await ws.receive_json_from() - assert response["type"] == GQL_COMPLETE - assert response["id"] == "demo" - - await ws.send_json_to({"type": "NotAProtocolMessage"}) - await ws.send_json_to({"type": GQL_CONNECTION_TERMINATE}) - - # make sure the websocket is disconnected now - data = await ws.receive_output() - assert data == {"type": "websocket.close", "code": 1000} - - -async def test_custom_context(): - class CustomDebuggableGraphQLWSConsumer(DebuggableGraphQLWSConsumer): - async def get_context(self, *args, **kwargs) -> object: - context = await super().get_context(*args, **kwargs) - context.custom_value = "Hi!" - return context - - client = WebsocketCommunicator( - CustomDebuggableGraphQLWSConsumer.as_asgi( - schema=schema, subscription_protocols=(GRAPHQL_WS_PROTOCOL,) - ), - "/graphql", - subprotocols=[ - GRAPHQL_WS_PROTOCOL, - ], - ) - res = await client.connect() - assert res == (True, GRAPHQL_WS_PROTOCOL) - await client.send_json_to({"type": GQL_CONNECTION_INIT}) - await client.send_json_to( - { - "type": GQL_START, - "id": "demo", - "payload": { - "query": "subscription { context }", - }, - } - ) - - response = await client.receive_json_from() - assert response["type"] == GQL_CONNECTION_ACK - - response = await client.receive_json_from() - assert response["type"] == GQL_DATA - assert response["id"] == "demo" - assert response["payload"]["data"] == {"context": "Hi!"} - - await client.send_json_to({"type": GQL_STOP, "id": "demo"}) - response = await client.receive_json_from() - assert response["type"] == GQL_COMPLETE - assert response["id"] == "demo" - - await client.send_json_to({"type": GQL_CONNECTION_TERMINATE}) - - # make sure the websocket is disconnected now - data = await client.receive_output() - assert data == {"type": "websocket.close", "code": 1000} - - -async def test_resolving_enums(ws): - await ws.send_json_to({"type": GQL_CONNECTION_INIT}) - await ws.send_json_to( - { - "type": GQL_START, - "id": "demo", - "payload": { - "query": "subscription { flavors }", - }, - } - ) - - response = await ws.receive_json_from() - assert response["type"] == GQL_CONNECTION_ACK - - response = await ws.receive_json_from() - assert response["type"] == GQL_DATA - assert response["id"] == "demo" - assert response["payload"]["data"] == {"flavors": "VANILLA"} - - response = await ws.receive_json_from() - assert response["type"] == GQL_DATA - assert response["id"] == "demo" - assert response["payload"]["data"] == {"flavors": "STRAWBERRY"} - - response = await ws.receive_json_from() - assert response["type"] == GQL_DATA - assert response["id"] == "demo" - assert response["payload"]["data"] == {"flavors": "CHOCOLATE"} - - await ws.send_json_to({"type": GQL_STOP, "id": "demo"}) - response = await ws.receive_json_from() - assert response["type"] == GQL_COMPLETE - assert response["id"] == "demo" - - await ws.send_json_to({"type": GQL_CONNECTION_TERMINATE}) - - # make sure the websocket is disconnected now - data = await ws.receive_output() - assert data == {"type": "websocket.close", "code": 1000} - - -async def test_task_cancellation_separation(): - ws1 = WebsocketCommunicator( - DebuggableGraphQLWSConsumer.as_asgi( - schema=schema, subscription_protocols=(GRAPHQL_WS_PROTOCOL,) - ), - "/graphql", - subprotocols=[ - GRAPHQL_WS_PROTOCOL, - ], - ) - res = await ws1.connect() - assert res == (True, GRAPHQL_WS_PROTOCOL) - ws2 = WebsocketCommunicator( - DebuggableGraphQLWSConsumer.as_asgi( - schema=schema, subscription_protocols=(GRAPHQL_WS_PROTOCOL,) - ), - "/graphql", - subprotocols=[ - GRAPHQL_WS_PROTOCOL, - ], - ) - res = await ws2.connect() - assert res == (True, GRAPHQL_WS_PROTOCOL) - - start_payload = { - "type": GQL_START, - "id": "demo", - "payload": {"query": 'subscription { echo(message: "Hi", delay: 99) }'}, - } - - # 0 active result handler tasks - - await ws1.send_json_to({"type": GQL_CONNECTION_INIT}) - await ws1.send_json_to(start_payload) - await ws1.receive_json_from() - - # 1 active result handler tasks - - await ws2.send_json_to({"type": GQL_CONNECTION_INIT}) - await ws2.send_json_to(start_payload) - await ws2.receive_json_from() - - # 2 active result handler tasks - - await ws1.send_json_to({"type": GQL_STOP, "id": "demo"}) - await ws1.receive_json_from() # complete - - # 1 active result handler tasks - - await ws2.send_json_to({"type": GQL_STOP, "id": "demo"}) - await ws2.receive_json_from() # complete - - # 1 active result handler tasks - - await ws1.send_json_to( - { - "type": GQL_START, - "id": "debug1", - "payload": { - "query": "subscription { debug { numActiveResultHandlers } }", - }, - } - ) - - response = await ws1.receive_json_from() - assert response["type"] == GQL_DATA - assert response["id"] == "debug1" - - # The one active result handler is the one for this debug subscription - assert response["payload"]["data"] == {"debug": {"numActiveResultHandlers": 1}} - - response = await ws1.receive_json_from() - assert response["type"] == GQL_COMPLETE - assert response["id"] == "debug1" - - -async def test_injects_connection_params(ws): - await ws.send_json_to( - {"type": GQL_CONNECTION_INIT, "id": "demo", "payload": {"strawberry": "rocks"}} - ) - await ws.send_json_to( - { - "type": GQL_START, - "id": "demo", - "payload": { - "query": "subscription { connectionParams }", - }, - } - ) - - response = await ws.receive_json_from() - assert response["type"] == GQL_CONNECTION_ACK - - response = await ws.receive_json_from() - assert response["type"] == GQL_DATA - assert response["id"] == "demo" - assert response["payload"]["data"] == {"connectionParams": "rocks"} - - await ws.send_json_to({"type": GQL_STOP, "id": "demo"}) - response = await ws.receive_json_from() - assert response["type"] == GQL_COMPLETE - assert response["id"] == "demo" - - await ws.send_json_to({"type": GQL_CONNECTION_TERMINATE}) - - # make sure the websocket is disconnected now - data = await ws.receive_output() - assert data == {"type": "websocket.close", "code": 1000} - - -async def test_rejects_connection_params(ws): - await ws.send_json_to( - {"type": GQL_CONNECTION_INIT, "id": "demo", "payload": "gonna fail"} - ) - - response = await ws.receive_json_from() - assert response["type"] == GQL_CONNECTION_ERROR - - # make sure the websocket is disconnected now - data = await ws.receive_output() - assert data == {"type": "websocket.close", "code": 1000} diff --git a/tests/channels/test_http_handler.py b/tests/channels/test_http_handler.py deleted file mode 100644 index 1d2107926f..0000000000 --- a/tests/channels/test_http_handler.py +++ /dev/null @@ -1,296 +0,0 @@ -import json -from typing import Any, Dict, Optional - -import pytest - -from channels.testing import HttpCommunicator -from strawberry.channels import GraphQLHTTPConsumer -from strawberry.channels.handlers.http_handler import SyncGraphQLHTTPConsumer -from tests.channels.schema import schema - -pytestmark = pytest.mark.xfail( - reason=( - "Some of these tests seems to crash on windows " - "due to usage of database_sync_to_async" - ) -) - - -def generate_body(query: str, variables: Optional[Dict[str, Any]] = None): - body: Dict[str, Any] = {"query": query} - if variables is not None: - body["variables"] = variables - - return json.dumps(body).encode() - - -def generate_get_path(path, query: str, variables: Optional[Dict[str, Any]] = None): - body: Dict[str, Any] = {"query": query} - if variables is not None: - body["variables"] = json.dumps(variables) - - parts = [f"{k}={v}" for k, v in body.items()] - return f"{path}?{'&'.join(parts)}" - - -def assert_response( - response: Dict[str, Any], expected: Any, errors: Optional[Any] = None -): - assert response["status"] == 200 - body = json.loads(response["body"]) - assert "errors" not in body - assert body["data"] == expected - - -@pytest.mark.parametrize("consumer", [GraphQLHTTPConsumer, SyncGraphQLHTTPConsumer]) -async def test_graphiql_view(consumer): - client = HttpCommunicator( - consumer.as_asgi(schema=schema), - "GET", - "/graphql", - headers=[(b"accept", b"text/html")], - ) - response = await client.get_response() - assert response["headers"] == [(b"Content-Type", b"text/html")] - assert response["status"] == 200 - assert b"GraphiQL" in response["body"] - - -@pytest.mark.parametrize("consumer", [GraphQLHTTPConsumer, SyncGraphQLHTTPConsumer]) -async def test_graphiql_view_disabled(consumer): - client = HttpCommunicator( - consumer.as_asgi(schema=schema, graphiql=False), - "GET", - "/graphql", - headers=[(b"accept", b"text/html")], - ) - response = await client.get_response() - assert response == { - "headers": [(b"Allow", b"GET, POST")], - "status": 405, - "body": b"Method not allowed", - } - - -@pytest.mark.parametrize("consumer", [GraphQLHTTPConsumer, SyncGraphQLHTTPConsumer]) -async def test_graphiql_view_not_allowed(consumer): - client = HttpCommunicator( - consumer.as_asgi(schema=schema), - "GET", - "/graphql", - ) - response = await client.get_response() - assert response == { - "headers": [(b"Allow", b"GET, POST")], - "status": 405, - "body": b"Method not allowed", - } - - -@pytest.mark.parametrize("consumer", [GraphQLHTTPConsumer, SyncGraphQLHTTPConsumer]) -@pytest.mark.parametrize("method", ["DELETE", "HEAD", "PUT", "PATCH"]) -async def test_disabled_methods(consumer, method: str): - client = HttpCommunicator( - consumer.as_asgi(schema=schema), - method, - "/graphql", - headers=[(b"accept", b"text/html")], - ) - response = await client.get_response() - assert response == { - "headers": [(b"Allow", b"GET, POST")], - "status": 405, - "body": b"Method not allowed", - } - - -@pytest.mark.parametrize("consumer", [GraphQLHTTPConsumer, SyncGraphQLHTTPConsumer]) -async def test_fails_on_multipart_body(consumer): - client = HttpCommunicator( - consumer.as_asgi(schema=schema), - "POST", - "/graphql", - body=generate_body("{ hello }"), - headers=[(b"content-type", b"multipart/form-data")], - ) - response = await client.get_response() - assert response == { - "status": 500, - "headers": [], - "body": b"Unable to parse the multipart body", - } - - -@pytest.mark.parametrize("consumer", [GraphQLHTTPConsumer, SyncGraphQLHTTPConsumer]) -@pytest.mark.parametrize("body", [b"{}", b'{"foo": "bar"}']) -async def test_fails_on_missing_query(consumer, body: bytes): - - client = HttpCommunicator( - consumer.as_asgi(schema=schema), - "POST", - "/graphql", - body=body, - ) - response = await client.get_response() - assert response == { - "status": 500, - "headers": [], - "body": b"No GraphQL query found in the request", - } - - -@pytest.mark.parametrize("consumer", [GraphQLHTTPConsumer, SyncGraphQLHTTPConsumer]) -@pytest.mark.parametrize("body", [b"", b"definitely-not-json-string"]) -async def test_fails_on_invalid_query(consumer, body: bytes): - client = HttpCommunicator( - consumer.as_asgi(schema=schema), - "POST", - "/graphql", - body=body, - ) - response = await client.get_response() - assert response == { - "status": 500, - "headers": [], - "body": b"Unable to parse request body as JSON", - } - - -@pytest.mark.parametrize("consumer", [GraphQLHTTPConsumer, SyncGraphQLHTTPConsumer]) -async def test_graphql_post_query_fails_using_params(consumer): - client = HttpCommunicator( - consumer.as_asgi(schema=schema), - "GET", - "/graphql?foo=bar", - ) - response = await client.get_response() - assert response == { - "status": 500, - "headers": [], - "body": b"No GraphQL query found in the request", - } - - -# FIXME: All the tests bellow runs fine if running tests in this file only, -# but fail for Sync when running the whole testsuite, unless using. -# @pytest.mark.django_db. Probably because of the `database_sync_to_async`? - - -@pytest.mark.django_db -@pytest.mark.parametrize("consumer", [GraphQLHTTPConsumer, SyncGraphQLHTTPConsumer]) -async def test_graphql_query(consumer): - client = HttpCommunicator( - consumer.as_asgi(schema=schema), - "POST", - "/graphql", - body=generate_body("{ hello }"), - ) - assert_response( - await client.get_response(), - {"hello": "Hello world"}, - ) - - -@pytest.mark.django_db -@pytest.mark.parametrize("consumer", [GraphQLHTTPConsumer, SyncGraphQLHTTPConsumer]) -async def test_graphql_can_pass_variables(consumer): - client = HttpCommunicator( - consumer.as_asgi(schema=schema), - "POST", - "/graphql", - body=generate_body( - "query Hello($name: String!) { hello(name: $name) }", - variables={"name": "James"}, - ), - ) - assert_response( - await client.get_response(), - {"hello": "Hello James"}, - ) - - -@pytest.mark.django_db -@pytest.mark.parametrize("consumer", [GraphQLHTTPConsumer, SyncGraphQLHTTPConsumer]) -async def test_graphql_get_query_using_params(consumer): - client = HttpCommunicator( - consumer.as_asgi(schema=schema), - "GET", - generate_get_path("/graphql", "{ hello }"), - ) - assert_response( - await client.get_response(), - {"hello": "Hello world"}, - ) - - -@pytest.mark.django_db -@pytest.mark.parametrize("consumer", [GraphQLHTTPConsumer, SyncGraphQLHTTPConsumer]) -async def test_graphql_can_pass_variables_using_params(consumer): - client = HttpCommunicator( - consumer.as_asgi(schema=schema), - "GET", - generate_get_path( - "/graphql", - "query Hello($name: String!) { hello(name: $name) }", - variables={"name": "James"}, - ), - ) - assert_response( - await client.get_response(), - {"hello": "Hello James"}, - ) - - -@pytest.mark.django_db -@pytest.mark.parametrize("consumer", [GraphQLHTTPConsumer, SyncGraphQLHTTPConsumer]) -async def test_returns_errors_and_data(consumer): - client = HttpCommunicator( - consumer.as_asgi(schema=schema), - "POST", - "/graphql", - body=generate_body("{ hello, alwaysFail }"), - ) - response = await client.get_response() - assert response["status"] == 200 - assert json.loads(response["body"]) == { - "data": {"alwaysFail": None, "hello": "Hello world"}, - "errors": [ - { - "locations": [{"column": 10, "line": 1}], - "message": "You are not authorized", - "path": ["alwaysFail"], - } - ], - } - - -@pytest.mark.django_db -@pytest.mark.parametrize("consumer", [GraphQLHTTPConsumer, SyncGraphQLHTTPConsumer]) -async def test_graphql_get_does_not_allow_mutation(consumer): - client = HttpCommunicator( - consumer.as_asgi(schema=schema), - "GET", - generate_get_path("/graphql", "mutation { hello }"), - ) - response = await client.get_response() - assert response == { - "status": 406, - "headers": [], - "body": b"mutations are not allowed when using GET", - } - - -@pytest.mark.django_db -@pytest.mark.parametrize("consumer", [GraphQLHTTPConsumer, SyncGraphQLHTTPConsumer]) -async def test_graphql_get_not_allowed(consumer): - client = HttpCommunicator( - consumer.as_asgi(schema=schema, allow_queries_via_get=False), - "GET", - generate_get_path("/graphql", "query { hello }"), - ) - response = await client.get_response() - assert response == { - "status": 406, - "headers": [], - "body": b"queries are not allowed when using GET", - } diff --git a/tests/channels/test_layers.py b/tests/channels/test_layers.py deleted file mode 100644 index 6bdbdd931d..0000000000 --- a/tests/channels/test_layers.py +++ /dev/null @@ -1,202 +0,0 @@ -import pytest - -from channels.layers import get_channel_layer -from channels.testing import WebsocketCommunicator -from strawberry.channels import GraphQLWSConsumer -from strawberry.channels.handlers.base import ChannelsConsumer -from strawberry.subscriptions import GRAPHQL_TRANSPORT_WS_PROTOCOL -from strawberry.subscriptions.protocols.graphql_transport_ws.types import ( - CompleteMessage, - ConnectionAckMessage, - ConnectionInitMessage, - NextMessage, - SubscribeMessage, - SubscribeMessagePayload, -) -from tests.channels.schema import schema - - -@pytest.fixture -async def ws(): - client = WebsocketCommunicator( - GraphQLWSConsumer.as_asgi(schema=schema), - "/graphql", - subprotocols=[GRAPHQL_TRANSPORT_WS_PROTOCOL], - ) - res = await client.connect() - assert res == (True, GRAPHQL_TRANSPORT_WS_PROTOCOL) - - yield client - - await client.disconnect() - - -async def test_no_layers(): - consumer = ChannelsConsumer() - # Mimic lack of layers. If layers is not installed/configured in channels, - # consumer.channel_layer will be `None` - consumer.channel_layer = None - - msg = ( - "Layers integration is required listening for channels.\n" - "Check https://channels.readthedocs.io/en/stable/topics/channel_layers.html " - "for more information" - ) - with pytest.raises(RuntimeError, match=msg): - await consumer.channel_listen("foobar").__anext__() - - -async def test_channel_listen(ws: WebsocketCommunicator): - await ws.send_json_to(ConnectionInitMessage().as_dict()) - - response = await ws.receive_json_from() - assert response == ConnectionAckMessage().as_dict() - - await ws.send_json_to( - SubscribeMessage( - id="sub1", - payload=SubscribeMessagePayload( - query="subscription { listener }", - ), - ).as_dict() - ) - - channel_layer = get_channel_layer() - assert channel_layer - - response = await ws.receive_json_from() - channel_name = response["payload"]["data"]["listener"] - - await channel_layer.send( - channel_name, - { - "type": "test.message", - "text": "Hello there!", - }, - ) - - response = await ws.receive_json_from() - assert ( - response - == NextMessage( - id="sub1", payload={"data": {"listener": "Hello there!"}} - ).as_dict() - ) - - await ws.send_json_to(CompleteMessage(id="sub1").as_dict()) - - -async def test_channel_listen_timeout(ws: WebsocketCommunicator): - await ws.send_json_to(ConnectionInitMessage().as_dict()) - - response = await ws.receive_json_from() - assert response == ConnectionAckMessage().as_dict() - - await ws.send_json_to( - SubscribeMessage( - id="sub1", - payload=SubscribeMessagePayload( - query="subscription { listener(timeout: 0.5) }", - ), - ).as_dict() - ) - - channel_layer = get_channel_layer() - assert channel_layer - - response = await ws.receive_json_from() - channel_name = response["payload"]["data"]["listener"] - assert channel_name - - response = await ws.receive_json_from() - assert response == CompleteMessage(id="sub1").as_dict() - - -async def test_channel_listen_no_message_on_channel(ws: WebsocketCommunicator): - await ws.send_json_to(ConnectionInitMessage().as_dict()) - - response = await ws.receive_json_from() - assert response == ConnectionAckMessage().as_dict() - - await ws.send_json_to( - SubscribeMessage( - id="sub1", - payload=SubscribeMessagePayload( - query="subscription { listener(timeout: 0.5) }", - ), - ).as_dict() - ) - - channel_layer = get_channel_layer() - assert channel_layer - - response = await ws.receive_json_from() - channel_name = response["payload"]["data"]["listener"] - assert channel_name - - await channel_layer.send( - "totally-not-out-channel", - { - "type": "test.message", - "text": "Hello there!", - }, - ) - - response = await ws.receive_json_from() - assert response == CompleteMessage(id="sub1").as_dict() - - -async def test_channel_listen_group(ws: WebsocketCommunicator): - await ws.send_json_to(ConnectionInitMessage().as_dict()) - - response = await ws.receive_json_from() - assert response == ConnectionAckMessage().as_dict() - - await ws.send_json_to( - SubscribeMessage( - id="sub1", - payload=SubscribeMessagePayload( - query='subscription { listener(group: "foobar") }', - ), - ).as_dict() - ) - - channel_layer = get_channel_layer() - assert channel_layer - - response = await ws.receive_json_from() - channel_name = response["payload"]["data"]["listener"] - - # Sent at least once to the consumer to make sure the groups were registered - await channel_layer.send( - channel_name, - { - "type": "test.message", - "text": "Hello there!", - }, - ) - response = await ws.receive_json_from() - assert ( - response - == NextMessage( - id="sub1", payload={"data": {"listener": "Hello there!"}} - ).as_dict() - ) - - await channel_layer.group_send( - "foobar", - { - "type": "test.message", - "text": "Hello there!", - }, - ) - - response = await ws.receive_json_from() - assert ( - response - == NextMessage( - id="sub1", payload={"data": {"listener": "Hello there!"}} - ).as_dict() - ) - - await ws.send_json_to(CompleteMessage(id="sub1").as_dict()) diff --git a/tests/channels/test_router.py b/tests/channels/test_router.py deleted file mode 100644 index be773ecfd4..0000000000 --- a/tests/channels/test_router.py +++ /dev/null @@ -1,71 +0,0 @@ -from unittest import mock - -import pytest - -from strawberry.channels.router import GraphQLProtocolTypeRouter -from tests.channels.schema import schema - - -def _fake_asgi(): - return lambda: None - - -@mock.patch("strawberry.channels.router.GraphQLHTTPConsumer.as_asgi") -@mock.patch("strawberry.channels.router.GraphQLWSConsumer.as_asgi") -@pytest.mark.parametrize("pattern", ["^graphql", "^foo"]) -def test_included_paths(ws_asgi: mock.Mock, http_asgi: mock.Mock, pattern: str): - http_ret = _fake_asgi() - http_asgi.return_value = http_ret - - ws_ret = _fake_asgi() - ws_asgi.return_value = ws_ret - - router = GraphQLProtocolTypeRouter(schema, url_pattern=pattern) - assert set(router.application_mapping) == {"http", "websocket"} - - assert len(router.application_mapping["http"].routes) == 1 - http_route = router.application_mapping["http"].routes[0] - assert http_route.pattern._regex == pattern - assert http_route.callback is http_ret - - assert len(router.application_mapping["websocket"].routes) == 1 - http_route = router.application_mapping["websocket"].routes[0] - assert http_route.pattern._regex == pattern - assert http_route.callback is ws_ret - - -@mock.patch("strawberry.channels.router.GraphQLHTTPConsumer.as_asgi") -@mock.patch("strawberry.channels.router.GraphQLWSConsumer.as_asgi") -@pytest.mark.parametrize("pattern", ["^graphql", "^foo"]) -def test_included_paths_with_django_app( - ws_asgi: mock.Mock, - http_asgi: mock.Mock, - pattern: str, -): - http_ret = _fake_asgi() - http_asgi.return_value = http_ret - - ws_ret = _fake_asgi() - ws_asgi.return_value = ws_ret - - django_app = _fake_asgi() - router = GraphQLProtocolTypeRouter( - schema, - django_application=django_app, - url_pattern=pattern, - ) - assert set(router.application_mapping) == {"http", "websocket"} - - assert len(router.application_mapping["http"].routes) == 2 - http_route = router.application_mapping["http"].routes[0] - assert http_route.pattern._regex == pattern - assert http_route.callback is http_ret - - django_route = router.application_mapping["http"].routes[1] - assert django_route.pattern._regex == "^" - assert django_route.callback is django_app - - assert len(router.application_mapping["websocket"].routes) == 1 - http_route = router.application_mapping["websocket"].routes[0] - assert http_route.pattern._regex == pattern - assert http_route.callback is ws_ret diff --git a/tests/channels/test_ws_handler.py b/tests/channels/test_ws_handler.py deleted file mode 100644 index eb46839ca8..0000000000 --- a/tests/channels/test_ws_handler.py +++ /dev/null @@ -1,44 +0,0 @@ -import pytest - -from channels.testing.websocket import WebsocketCommunicator -from strawberry.channels.handlers.graphql_transport_ws_handler import ( - GraphQLTransportWSHandler, -) -from strawberry.channels.handlers.graphql_ws_handler import GraphQLWSHandler -from strawberry.channels.handlers.ws_handler import GraphQLWSConsumer -from strawberry.subscriptions import GRAPHQL_TRANSPORT_WS_PROTOCOL, GRAPHQL_WS_PROTOCOL -from tests.channels.schema import schema - - -async def test_wrong_protocol(): - GraphQLWSConsumer.as_asgi(schema=schema) - client = WebsocketCommunicator( - GraphQLWSConsumer.as_asgi(schema=schema), - "/graphql", - subprotocols=[ - "non-existing", - ], - ) - res = await client.connect() - assert res == (False, 4406) - - -@pytest.mark.parametrize( - ("protocol", "handler"), - [ - (GRAPHQL_TRANSPORT_WS_PROTOCOL, GraphQLTransportWSHandler), - (GRAPHQL_WS_PROTOCOL, GraphQLWSHandler), - ], -) -async def test_correct_protocol(protocol, handler): - consumer = GraphQLWSConsumer(schema=schema) - client = WebsocketCommunicator( - consumer, - "/graphql", - subprotocols=[ - protocol, - ], - ) - res = await client.connect() - assert res == (True, protocol) - assert isinstance(consumer._handler, handler) diff --git a/tests/cli/conftest.py b/tests/cli/conftest.py index 3881c4803e..c81a96335d 100644 --- a/tests/cli/conftest.py +++ b/tests/cli/conftest.py @@ -1,6 +1,7 @@ import sys import pytest + from click.testing import CliRunner from starlette.testclient import TestClient diff --git a/tests/cli/test_codegen.py b/tests/cli/test_codegen.py deleted file mode 100644 index fbcfd38614..0000000000 --- a/tests/cli/test_codegen.py +++ /dev/null @@ -1,190 +0,0 @@ -from pathlib import Path -from typing import List - -import pytest - -from strawberry.cli.commands.codegen import ConsolePlugin -from strawberry.cli.commands.codegen import codegen as cmd_codegen -from strawberry.codegen import CodegenFile, CodegenResult, QueryCodegenPlugin -from strawberry.codegen.types import GraphQLOperation, GraphQLType - - -class ConsoleTestPlugin(ConsolePlugin): - def on_end(self, result: CodegenResult): - result.files[0].path = "renamed.py" - - return super().on_end(result) - - -class QueryCodegenTestPlugin(QueryCodegenPlugin): - def generate_code( - self, types: List[GraphQLType], operation: GraphQLOperation - ) -> List[CodegenFile]: - return [ - CodegenFile( - path="test.py", - content=f"# This is a test file for {operation.name}", - ) - ] - - -class EmptyPlugin(QueryCodegenPlugin): - def generate_code( - self, types: List[GraphQLType], operation: GraphQLOperation - ) -> List[CodegenFile]: - return [ - CodegenFile( - path="test.py", - content="# Empty", - ) - ] - - -@pytest.fixture -def query_file_path(tmp_path: Path) -> Path: - output_path = tmp_path / "query.graphql" - output_path.write_text( - """ - query GetUser { - user { - name - } - } - """ - ) - return output_path - - -def test_codegen(cli_runner, query_file_path: Path, tmp_path: Path): - selector = "tests.fixtures.sample_package.sample_module:schema" - result = cli_runner.invoke( - cmd_codegen, - [ - "-p", - "tests.cli.test_codegen:QueryCodegenTestPlugin", - "-o", - str(tmp_path), - "--schema", - selector, - str(query_file_path), - ], - ) - - assert result.exit_code == 0 - - code_path = tmp_path / "test.py" - - assert code_path.exists() - assert code_path.read_text() == "# This is a test file for GetUser" - - -def test_codegen_passing_plugin_symbol( - cli_runner, query_file_path: Path, tmp_path: Path -): - selector = "tests.fixtures.sample_package.sample_module:schema" - result = cli_runner.invoke( - cmd_codegen, - [ - "-p", - "tests.cli.test_codegen:EmptyPlugin", - "-o", - str(tmp_path), - "--schema", - selector, - str(query_file_path), - ], - ) - - assert result.exit_code == 0 - - code_path = tmp_path / "test.py" - - assert code_path.exists() - assert code_path.read_text() == "# Empty" - - -def test_codegen_returns_error_when_symbol_does_not_exist( - cli_runner, query_file_path: Path -): - selector = "tests.fixtures.sample_package.sample_module:schema" - result = cli_runner.invoke( - cmd_codegen, - [ - "-p", - "tests.cli.test_codegen:SomePlugin", - "--schema", - selector, - str(query_file_path), - ], - ) - - assert result.exit_code == 1 - assert result.exception.args == ( - "module 'tests.cli.test_codegen' has no attribute 'SomePlugin'", - ) - - -def test_codegen_returns_error_when_module_does_not_exist( - cli_runner, query_file_path: Path -): - selector = "tests.fixtures.sample_package.sample_module:schema" - result = cli_runner.invoke( - cmd_codegen, - ["-p", "fake_module_plugin", "--schema", selector, str(query_file_path)], - ) - - assert result.exit_code == 1 - assert "Error: Plugin fake_module_plugin not found" in result.output - - -def test_codegen_returns_error_when_does_not_find_plugin( - cli_runner, query_file_path: Path -): - selector = "tests.fixtures.sample_package.sample_module:schema" - result = cli_runner.invoke( - cmd_codegen, - ["-p", "tests.cli.test_server", "--schema", selector, str(query_file_path)], - ) - - assert result.exit_code == 1 - assert "Error: Plugin tests.cli.test_server not found" in result.output - - -def test_codegen_finds_our_plugins(cli_runner, query_file_path: Path, tmp_path: Path): - selector = "tests.fixtures.sample_package.sample_module:schema" - result = cli_runner.invoke( - cmd_codegen, - ["-p", "python", "--schema", selector, "-o", tmp_path, str(query_file_path)], - ) - - assert result.exit_code == 0 - - code_path = tmp_path / "types.py" - - assert code_path.exists() - assert "class GetUserResult" in code_path.read_text() - - -def test_can_use_custom_cli_plugin(cli_runner, query_file_path: Path, tmp_path: Path): - selector = "tests.fixtures.sample_package.sample_module:schema" - result = cli_runner.invoke( - cmd_codegen, - [ - "--cli-plugin", - "tests.cli.test_codegen:ConsoleTestPlugin", - "-p", - "python", - "--schema", - selector, - "-o", - tmp_path, - str(query_file_path), - ], - ) - - assert result.exit_code == 0 - - code_path = tmp_path / "renamed.py" - - assert code_path.exists() - assert "class GetUserResult" in code_path.read_text() diff --git a/tests/cli/test_server.py b/tests/cli/test_server.py index 801ab305e3..5a9844c0fc 100644 --- a/tests/cli/test_server.py +++ b/tests/cli/test_server.py @@ -1,11 +1,17 @@ +import os import re import sys +import time import pytest + +import requests import uvicorn +from xprocess import ProcessStarter from strawberry.cli.commands.server import server as cmd_server + BOOT_MSG = "Running strawberry on http://0.0.0.0:8000/graphql" if sys.platform != "win32": # UTF-8 chars are not supported by default console on Windows @@ -92,3 +98,88 @@ def test_debug_server_routes(debug_server_client): for path in ["/", "/graphql"]: response = debug_server_client.get(path) assert response.status_code == 200 + + +def test_automatic_reloading(xprocess, tmp_path): + + # used to start our dev server + class Starter(ProcessStarter): + # Unbuffered output improves start up detection reliabiity on Windows + env = {"PYTHONUNBUFFERED": "1", **os.environ} + # considered started once this pattern is found + pattern = BOOT_MSG + terminate_on_interrupt = True + timeout = 10 + args = [ + "poetry", + "run", + "strawberry", + "server", + # logging at warning prints when schema.py has changed + # we key on this to perform the next test + "--log-level", + "warning", + "--app-dir", + # Python Versions < 3.8 on Windows do not have an Iterable WindowsPath + # casting to str prevents this from throwing a TypeError on Windows + str(tmp_path), + "schema", + ] + + source = ( + "import strawberry\n" + "@strawberry.type\n" + "class Query:\n" + " @strawberry.field\n" + " def number(self) -> int:\n" + " return {}\n" + "schema = strawberry.Schema(query=Query)\n" + ) + + schema_file_path = tmp_path / "schema.py" + schema_file_path.touch() + + url = "http://127.0.0.1:8000/graphql" + query = {"query": "{ number }"} + + def make_request(expected_answer: int) -> None: + for _ in range(5): + try: + response = requests.post(url, json=query) + assert response.status_code == 200 + assert response.json() == {"data": {"number": expected_answer}} + break + except requests.RequestException: + time.sleep(0.5) + + def wait_for_reload() -> None: + # attempt to detect the reload; continue either way + for _ in range(5): + with open(xprocess.getinfo("dev_server").logpath) as logfile: + # when a reload is detected a line ending + # with "Reloading..." is output to the log + found_reloading_line = any( + line for line in logfile if line.strip().endswith("Reloading...") + ) + if found_reloading_line: + break + else: + time.sleep(0.5) + + try: + schema_file_path.write_text(source.format(42)) + + # blocks until either success or failure of starting the dev server + xprocess.ensure("dev_server", Starter) + + make_request(expected_answer=42) + + # trigger reload + schema_file_path.write_text(source.format(1234)) + + wait_for_reload() + + make_request(expected_answer=1234) + finally: + # always attempt to terminate the server + xprocess.getinfo("dev_server").terminate() diff --git a/tests/codegen/conftest.py b/tests/codegen/conftest.py deleted file mode 100644 index 8aed1d89c8..0000000000 --- a/tests/codegen/conftest.py +++ /dev/null @@ -1,103 +0,0 @@ -import datetime -import decimal -import enum -from typing import TYPE_CHECKING, List, NewType, Optional -from typing_extensions import Annotated -from uuid import UUID - -import pytest - -import strawberry - -if TYPE_CHECKING: - - from .lazy_type import LaziestType - -JSON = strawberry.scalar(NewType("JSON", str)) - - -@strawberry.enum -class Color(enum.Enum): - RED = "red" - GREEN = "green" - BLUE = "blue" - - -@strawberry.type -class Person: - name: str - age: int - - -@strawberry.type -class Animal: - name: str - age: int - - -PersonOrAnimal = strawberry.union("PersonOrAnimal", (Person, Animal)) - - -@strawberry.interface -class Node: - id: str - - -@strawberry.type -class BlogPost(Node): - title: str - - -@strawberry.type -class Image(Node): - url: str - - -@strawberry.input -class PersonInput: - name: str - - -@strawberry.input -class ExampleInput: - id: strawberry.ID - name: str - age: int - person: Optional[PersonInput] - people: List[PersonInput] - optional_people: Optional[List[PersonInput]] - - -@strawberry.type -class Query: - id: strawberry.ID - integer: int - float: float - boolean: bool - uuid: UUID - date: datetime.date - datetime: datetime.datetime - time: datetime.time - decimal: decimal.Decimal - optional_int: Optional[int] - list_of_int: List[int] - list_of_optional_int: List[Optional[int]] - optional_list_of_optional_int: Optional[List[Optional[int]]] - person: Person - optional_person: Optional[Person] - list_of_people: List[Person] - enum: Color - json: JSON - union: PersonOrAnimal - optional_union: Optional[PersonOrAnimal] - interface: Node - lazy: Annotated["LaziestType", strawberry.lazy("tests.codegen.lazy_type")] - - @strawberry.field - def with_inputs(self, id: Optional[strawberry.ID], input: ExampleInput) -> bool: - return True - - -@pytest.fixture -def schema(): - return strawberry.Schema(query=Query, types=[BlogPost, Image]) diff --git a/tests/codegen/lazy_type.py b/tests/codegen/lazy_type.py deleted file mode 100644 index 0f7a10118f..0000000000 --- a/tests/codegen/lazy_type.py +++ /dev/null @@ -1,6 +0,0 @@ -import strawberry - - -@strawberry.type -class LaziestType: - something: bool diff --git a/tests/codegen/queries/alias.graphql b/tests/codegen/queries/alias.graphql deleted file mode 100644 index 992b263715..0000000000 --- a/tests/codegen/queries/alias.graphql +++ /dev/null @@ -1,8 +0,0 @@ -query OperationName { - id - second_id: id - a_float: float - lazy { - lazy: something - } -} diff --git a/tests/codegen/queries/basic.graphql b/tests/codegen/queries/basic.graphql deleted file mode 100644 index eaa30c09c5..0000000000 --- a/tests/codegen/queries/basic.graphql +++ /dev/null @@ -1,14 +0,0 @@ -query OperationName { - id - integer - float - boolean - uuid - date - datetime - time - decimal - lazy { - something - } -} diff --git a/tests/codegen/queries/custom_scalar.graphql b/tests/codegen/queries/custom_scalar.graphql deleted file mode 100644 index 4e741fd55c..0000000000 --- a/tests/codegen/queries/custom_scalar.graphql +++ /dev/null @@ -1,3 +0,0 @@ -query OperationName { - json -} diff --git a/tests/codegen/queries/enum.graphql b/tests/codegen/queries/enum.graphql deleted file mode 100644 index a9b059992a..0000000000 --- a/tests/codegen/queries/enum.graphql +++ /dev/null @@ -1,3 +0,0 @@ -query OperationName { - enum -} diff --git a/tests/codegen/queries/interface.graphql b/tests/codegen/queries/interface.graphql deleted file mode 100644 index 9975343d66..0000000000 --- a/tests/codegen/queries/interface.graphql +++ /dev/null @@ -1,5 +0,0 @@ -query OperationName { - interface { - id - } -} diff --git a/tests/codegen/queries/interface_fragments.graphql b/tests/codegen/queries/interface_fragments.graphql deleted file mode 100644 index 8488358518..0000000000 --- a/tests/codegen/queries/interface_fragments.graphql +++ /dev/null @@ -1,11 +0,0 @@ -query OperationName { - interface { - id - ... on BlogPost { - title - } - ... on Image { - url - } - } -} diff --git a/tests/codegen/queries/interface_single_fragment.graphql b/tests/codegen/queries/interface_single_fragment.graphql deleted file mode 100644 index 16f381babe..0000000000 --- a/tests/codegen/queries/interface_single_fragment.graphql +++ /dev/null @@ -1,8 +0,0 @@ -query OperationName { - interface { - id - ... on BlogPost { - title - } - } -} diff --git a/tests/codegen/queries/multiple_types.graphql b/tests/codegen/queries/multiple_types.graphql deleted file mode 100644 index b8b76219ba..0000000000 --- a/tests/codegen/queries/multiple_types.graphql +++ /dev/null @@ -1,8 +0,0 @@ -query OperationName { - person { - name - } - listOfPeople { - name - } -} diff --git a/tests/codegen/queries/multiple_types_optional.graphql b/tests/codegen/queries/multiple_types_optional.graphql deleted file mode 100644 index 6d52444248..0000000000 --- a/tests/codegen/queries/multiple_types_optional.graphql +++ /dev/null @@ -1,5 +0,0 @@ -query OperationName { - optionalPerson { - name - } -} diff --git a/tests/codegen/queries/optional_and_lists.graphql b/tests/codegen/queries/optional_and_lists.graphql deleted file mode 100644 index 889a670279..0000000000 --- a/tests/codegen/queries/optional_and_lists.graphql +++ /dev/null @@ -1,6 +0,0 @@ -query OperationName { - optionalInt - listOfInt - listOfOptionalInt - optionalListOfOptionalInt -} diff --git a/tests/codegen/queries/union.graphql b/tests/codegen/queries/union.graphql deleted file mode 100644 index fc85557c5c..0000000000 --- a/tests/codegen/queries/union.graphql +++ /dev/null @@ -1,18 +0,0 @@ -query OperationName { - union { - ... on Animal { - age - } - ... on Person { - name - } - } - optionalUnion { - ... on Animal { - age - } - ... on Person { - name - } - } -} diff --git a/tests/codegen/queries/union_with_one_type.graphql b/tests/codegen/queries/union_with_one_type.graphql deleted file mode 100644 index 1b6dcc292b..0000000000 --- a/tests/codegen/queries/union_with_one_type.graphql +++ /dev/null @@ -1,7 +0,0 @@ -query OperationName { - union { - ... on Animal { - age - } - } -} diff --git a/tests/codegen/queries/variables.graphql b/tests/codegen/queries/variables.graphql deleted file mode 100644 index ace2df810c..0000000000 --- a/tests/codegen/queries/variables.graphql +++ /dev/null @@ -1,3 +0,0 @@ -query OperationName($id: ID, $input: ExampleInput!, $ids: [ID!]!, $ids2: [ID], $ids3: [[ID]]) { - withInputs(id: $id, input: $input) -} diff --git a/tests/codegen/queries/with_directives.graphql b/tests/codegen/queries/with_directives.graphql deleted file mode 100644 index 383d58b0b1..0000000000 --- a/tests/codegen/queries/with_directives.graphql +++ /dev/null @@ -1,5 +0,0 @@ -query OperationName @owner(name: "Patrick", age: 30, items: [1, 2, 3], enum: NAME, bool: true) { - person { - name @root - } -} diff --git a/tests/codegen/snapshots/python/alias.py b/tests/codegen/snapshots/python/alias.py deleted file mode 100644 index 189cb7d826..0000000000 --- a/tests/codegen/snapshots/python/alias.py +++ /dev/null @@ -1,11 +0,0 @@ -class OperationNameResultLazy: - # alias for something - lazy: bool - -class OperationNameResult: - id: str - # alias for id - second_id: str - # alias for float - a_float: float - lazy: OperationNameResultLazy diff --git a/tests/codegen/snapshots/python/basic.py b/tests/codegen/snapshots/python/basic.py deleted file mode 100644 index f21ee1bf9f..0000000000 --- a/tests/codegen/snapshots/python/basic.py +++ /dev/null @@ -1,18 +0,0 @@ -from uuid import UUID -from datetime import date, datetime, time -from decimal import Decimal - -class OperationNameResultLazy: - something: bool - -class OperationNameResult: - id: str - integer: int - float: float - boolean: bool - uuid: UUID - date: date - datetime: datetime - time: time - decimal: Decimal - lazy: OperationNameResultLazy diff --git a/tests/codegen/snapshots/python/custom_scalar.py b/tests/codegen/snapshots/python/custom_scalar.py deleted file mode 100644 index 2e10e62cc2..0000000000 --- a/tests/codegen/snapshots/python/custom_scalar.py +++ /dev/null @@ -1,6 +0,0 @@ -from typing import NewType - -JSON = NewType("JSON", str) - -class OperationNameResult: - json: JSON diff --git a/tests/codegen/snapshots/python/enum.py b/tests/codegen/snapshots/python/enum.py deleted file mode 100644 index 1116307dd7..0000000000 --- a/tests/codegen/snapshots/python/enum.py +++ /dev/null @@ -1,9 +0,0 @@ -from enum import Enum - -class Color(Enum): - RED = "RED" - GREEN = "GREEN" - BLUE = "BLUE" - -class OperationNameResult: - enum: Color diff --git a/tests/codegen/snapshots/python/interface.py b/tests/codegen/snapshots/python/interface.py deleted file mode 100644 index 7e810d3f2c..0000000000 --- a/tests/codegen/snapshots/python/interface.py +++ /dev/null @@ -1,5 +0,0 @@ -class OperationNameResultInterface: - id: str - -class OperationNameResult: - interface: OperationNameResultInterface diff --git a/tests/codegen/snapshots/python/interface_fragments.py b/tests/codegen/snapshots/python/interface_fragments.py deleted file mode 100644 index 89e278c675..0000000000 --- a/tests/codegen/snapshots/python/interface_fragments.py +++ /dev/null @@ -1,14 +0,0 @@ -from typing import Union - -class OperationNameResultInterfaceBlogPost: - id: str - title: str - -class OperationNameResultInterfaceImage: - id: str - url: str - -OperationNameResultInterface = Union[OperationNameResultInterfaceBlogPost, OperationNameResultInterfaceImage] - -class OperationNameResult: - interface: OperationNameResultInterface diff --git a/tests/codegen/snapshots/python/interface_single_fragment.py b/tests/codegen/snapshots/python/interface_single_fragment.py deleted file mode 100644 index 0ffa768775..0000000000 --- a/tests/codegen/snapshots/python/interface_single_fragment.py +++ /dev/null @@ -1,6 +0,0 @@ -class OperationNameResultInterfaceBlogPost: - id: str - title: str - -class OperationNameResult: - interface: OperationNameResultInterfaceBlogPost diff --git a/tests/codegen/snapshots/python/multiple_types.py b/tests/codegen/snapshots/python/multiple_types.py deleted file mode 100644 index 39f115fe17..0000000000 --- a/tests/codegen/snapshots/python/multiple_types.py +++ /dev/null @@ -1,11 +0,0 @@ -from typing import List - -class OperationNameResultPerson: - name: str - -class OperationNameResultListOfPeople: - name: str - -class OperationNameResult: - person: OperationNameResultPerson - list_of_people: List[OperationNameResultListOfPeople] diff --git a/tests/codegen/snapshots/python/multiple_types_optional.py b/tests/codegen/snapshots/python/multiple_types_optional.py deleted file mode 100644 index 01530dc434..0000000000 --- a/tests/codegen/snapshots/python/multiple_types_optional.py +++ /dev/null @@ -1,7 +0,0 @@ -from typing import Optional - -class OperationNameResultOptionalPerson: - name: str - -class OperationNameResult: - optional_person: Optional[OperationNameResultOptionalPerson] diff --git a/tests/codegen/snapshots/python/optional_and_lists.py b/tests/codegen/snapshots/python/optional_and_lists.py deleted file mode 100644 index 57d57b58f1..0000000000 --- a/tests/codegen/snapshots/python/optional_and_lists.py +++ /dev/null @@ -1,7 +0,0 @@ -from typing import List, Optional - -class OperationNameResult: - optional_int: Optional[int] - list_of_int: List[int] - list_of_optional_int: List[Optional[int]] - optional_list_of_optional_int: Optional[List[Optional[int]]] diff --git a/tests/codegen/snapshots/python/union.py b/tests/codegen/snapshots/python/union.py deleted file mode 100644 index 4c92cf5184..0000000000 --- a/tests/codegen/snapshots/python/union.py +++ /dev/null @@ -1,21 +0,0 @@ -from typing import Optional, Union - -class OperationNameResultUnionAnimal: - age: int - -class OperationNameResultUnionPerson: - name: str - -OperationNameResultUnion = Union[OperationNameResultUnionAnimal, OperationNameResultUnionPerson] - -class OperationNameResultOptionalUnionAnimal: - age: int - -class OperationNameResultOptionalUnionPerson: - name: str - -OperationNameResultOptionalUnion = Union[OperationNameResultOptionalUnionAnimal, OperationNameResultOptionalUnionPerson] - -class OperationNameResult: - union: OperationNameResultUnion - optional_union: Optional[OperationNameResultOptionalUnion] diff --git a/tests/codegen/snapshots/python/union_with_one_type.py b/tests/codegen/snapshots/python/union_with_one_type.py deleted file mode 100644 index 58f7e1f82a..0000000000 --- a/tests/codegen/snapshots/python/union_with_one_type.py +++ /dev/null @@ -1,5 +0,0 @@ -class OperationNameResultUnionAnimal: - age: int - -class OperationNameResult: - union: OperationNameResultUnionAnimal diff --git a/tests/codegen/snapshots/python/variables.py b/tests/codegen/snapshots/python/variables.py deleted file mode 100644 index cbd55dc0d5..0000000000 --- a/tests/codegen/snapshots/python/variables.py +++ /dev/null @@ -1,22 +0,0 @@ -from typing import List, Optional - -class OperationNameResult: - with_inputs: bool - -class OperationNameVariables: - id: Optional[str] - input: ExampleInput - ids: List[str] - ids2: Optional[List[Optional[str]]] - ids3: Optional[List[Optional[List[Optional[str]]]]] - -class PersonInput: - name: str - -class ExampleInput: - id: str - name: str - age: int - person: Optional[PersonInput] - people: Optional[PersonInput] - optional_people: Optional[Optional[PersonInput]] diff --git a/tests/codegen/snapshots/python/with_directives.py b/tests/codegen/snapshots/python/with_directives.py deleted file mode 100644 index 3fbc413822..0000000000 --- a/tests/codegen/snapshots/python/with_directives.py +++ /dev/null @@ -1,5 +0,0 @@ -class OperationNameResultPerson: - name: str - -class OperationNameResult: - person: OperationNameResultPerson diff --git a/tests/codegen/snapshots/typescript/alias.ts b/tests/codegen/snapshots/typescript/alias.ts deleted file mode 100644 index b4888318d7..0000000000 --- a/tests/codegen/snapshots/typescript/alias.ts +++ /dev/null @@ -1,13 +0,0 @@ -type OperationNameResultLazy = { - // alias for something - lazy: boolean -} - -type OperationNameResult = { - id: string - // alias for id - second_id: string - // alias for float - a_float: number - lazy: OperationNameResultLazy -} diff --git a/tests/codegen/snapshots/typescript/basic.ts b/tests/codegen/snapshots/typescript/basic.ts deleted file mode 100644 index 8a89a82b09..0000000000 --- a/tests/codegen/snapshots/typescript/basic.ts +++ /dev/null @@ -1,16 +0,0 @@ -type OperationNameResultLazy = { - something: boolean -} - -type OperationNameResult = { - id: string - integer: number - float: number - boolean: boolean - uuid: string - date: string - datetime: string - time: string - decimal: string - lazy: OperationNameResultLazy -} diff --git a/tests/codegen/snapshots/typescript/custom_scalar.ts b/tests/codegen/snapshots/typescript/custom_scalar.ts deleted file mode 100644 index c966551eb0..0000000000 --- a/tests/codegen/snapshots/typescript/custom_scalar.ts +++ /dev/null @@ -1,5 +0,0 @@ -type JSON = string - -type OperationNameResult = { - json: JSON -} diff --git a/tests/codegen/snapshots/typescript/enum.ts b/tests/codegen/snapshots/typescript/enum.ts deleted file mode 100644 index 2356061132..0000000000 --- a/tests/codegen/snapshots/typescript/enum.ts +++ /dev/null @@ -1,9 +0,0 @@ -enum Color { - RED = "RED", - GREEN = "GREEN", - BLUE = "BLUE", -} - -type OperationNameResult = { - enum: Color -} diff --git a/tests/codegen/snapshots/typescript/interface.ts b/tests/codegen/snapshots/typescript/interface.ts deleted file mode 100644 index 6161ec66e8..0000000000 --- a/tests/codegen/snapshots/typescript/interface.ts +++ /dev/null @@ -1,7 +0,0 @@ -type OperationNameResultInterface = { - id: string -} - -type OperationNameResult = { - interface: OperationNameResultInterface -} diff --git a/tests/codegen/snapshots/typescript/interface_fragments.ts b/tests/codegen/snapshots/typescript/interface_fragments.ts deleted file mode 100644 index 36a972825d..0000000000 --- a/tests/codegen/snapshots/typescript/interface_fragments.ts +++ /dev/null @@ -1,15 +0,0 @@ -type OperationNameResultInterfaceBlogPost = { - id: string - title: string -} - -type OperationNameResultInterfaceImage = { - id: string - url: string -} - -type OperationNameResultInterface = OperationNameResultInterfaceBlogPost | OperationNameResultInterfaceImage - -type OperationNameResult = { - interface: OperationNameResultInterface -} diff --git a/tests/codegen/snapshots/typescript/interface_single_fragment.ts b/tests/codegen/snapshots/typescript/interface_single_fragment.ts deleted file mode 100644 index 4ac7922412..0000000000 --- a/tests/codegen/snapshots/typescript/interface_single_fragment.ts +++ /dev/null @@ -1,8 +0,0 @@ -type OperationNameResultInterfaceBlogPost = { - id: string - title: string -} - -type OperationNameResult = { - interface: OperationNameResultInterfaceBlogPost -} diff --git a/tests/codegen/snapshots/typescript/multiple_types.ts b/tests/codegen/snapshots/typescript/multiple_types.ts deleted file mode 100644 index f6aaeb5a1a..0000000000 --- a/tests/codegen/snapshots/typescript/multiple_types.ts +++ /dev/null @@ -1,12 +0,0 @@ -type OperationNameResultPerson = { - name: string -} - -type OperationNameResultListOfPeople = { - name: string -} - -type OperationNameResult = { - person: OperationNameResultPerson - list_of_people: OperationNameResultListOfPeople[] -} diff --git a/tests/codegen/snapshots/typescript/multiple_types_optional.ts b/tests/codegen/snapshots/typescript/multiple_types_optional.ts deleted file mode 100644 index f9796c15f8..0000000000 --- a/tests/codegen/snapshots/typescript/multiple_types_optional.ts +++ /dev/null @@ -1,7 +0,0 @@ -type OperationNameResultOptionalPerson = { - name: string -} - -type OperationNameResult = { - optional_person: OperationNameResultOptionalPerson | undefined -} diff --git a/tests/codegen/snapshots/typescript/optional_and_lists.ts b/tests/codegen/snapshots/typescript/optional_and_lists.ts deleted file mode 100644 index d043e718ab..0000000000 --- a/tests/codegen/snapshots/typescript/optional_and_lists.ts +++ /dev/null @@ -1,6 +0,0 @@ -type OperationNameResult = { - optional_int: number | undefined - list_of_int: number[] - list_of_optional_int: (number | undefined)[] - optional_list_of_optional_int: (number | undefined)[] | undefined -} diff --git a/tests/codegen/snapshots/typescript/union.ts b/tests/codegen/snapshots/typescript/union.ts deleted file mode 100644 index 866e91a5b7..0000000000 --- a/tests/codegen/snapshots/typescript/union.ts +++ /dev/null @@ -1,24 +0,0 @@ -type OperationNameResultUnionAnimal = { - age: number -} - -type OperationNameResultUnionPerson = { - name: string -} - -type OperationNameResultUnion = OperationNameResultUnionAnimal | OperationNameResultUnionPerson - -type OperationNameResultOptionalUnionAnimal = { - age: number -} - -type OperationNameResultOptionalUnionPerson = { - name: string -} - -type OperationNameResultOptionalUnion = OperationNameResultOptionalUnionAnimal | OperationNameResultOptionalUnionPerson - -type OperationNameResult = { - union: OperationNameResultUnion - optional_union: OperationNameResultOptionalUnion | undefined -} diff --git a/tests/codegen/snapshots/typescript/union_with_one_type.ts b/tests/codegen/snapshots/typescript/union_with_one_type.ts deleted file mode 100644 index f9922492a1..0000000000 --- a/tests/codegen/snapshots/typescript/union_with_one_type.ts +++ /dev/null @@ -1,7 +0,0 @@ -type OperationNameResultUnionAnimal = { - age: number -} - -type OperationNameResult = { - union: OperationNameResultUnionAnimal -} diff --git a/tests/codegen/snapshots/typescript/variables.ts b/tests/codegen/snapshots/typescript/variables.ts deleted file mode 100644 index 6cbc516e6f..0000000000 --- a/tests/codegen/snapshots/typescript/variables.ts +++ /dev/null @@ -1,24 +0,0 @@ -type OperationNameResult = { - with_inputs: boolean -} - -type OperationNameVariables = { - id: string | undefined - input: ExampleInput - ids: string[] - ids2: (string | undefined)[] | undefined - ids3: ((string | undefined)[] | undefined)[] | undefined -} - -type PersonInput = { - name: string -} - -type ExampleInput = { - id: string - name: string - age: number - person: PersonInput | undefined - people: PersonInput | undefined - optional_people: PersonInput | undefined | undefined -} diff --git a/tests/codegen/snapshots/typescript/with_directives.ts b/tests/codegen/snapshots/typescript/with_directives.ts deleted file mode 100644 index 3cd820f2c3..0000000000 --- a/tests/codegen/snapshots/typescript/with_directives.ts +++ /dev/null @@ -1,7 +0,0 @@ -type OperationNameResultPerson = { - name: string -} - -type OperationNameResult = { - person: OperationNameResultPerson -} diff --git a/tests/codegen/test_print_operation.py b/tests/codegen/test_print_operation.py deleted file mode 100644 index fd99a3997e..0000000000 --- a/tests/codegen/test_print_operation.py +++ /dev/null @@ -1,22 +0,0 @@ -from pathlib import Path - -import pytest - -from strawberry.codegen import QueryCodegen -from strawberry.codegen.plugins.print_operation import PrintOperationPlugin - -HERE = Path(__file__).parent -QUERIES = list(HERE.glob("queries/*.graphql")) - - -@pytest.mark.parametrize("query", QUERIES, ids=[x.name for x in QUERIES]) -def test_codegen( - query: Path, - schema, -): - generator = QueryCodegen(schema, plugins=[PrintOperationPlugin()]) - query_content = query.read_text() - - result = generator.run(query_content) - - assert result.to_string() == query_content diff --git a/tests/codegen/test_query_codegen.py b/tests/codegen/test_query_codegen.py deleted file mode 100644 index a016205a06..0000000000 --- a/tests/codegen/test_query_codegen.py +++ /dev/null @@ -1,71 +0,0 @@ -# - 1. test fragments -# - 2. test variables -# - 3. test input objects -# - 4. test mutations (raise?) -# - 5. test subscriptions (raise) - -from pathlib import Path -from typing import Type - -import pytest -from pytest_snapshot.plugin import Snapshot - -from strawberry.codegen import QueryCodegen, QueryCodegenPlugin -from strawberry.codegen.exceptions import ( - MultipleOperationsProvidedError, - NoOperationNameProvidedError, - NoOperationProvidedError, -) -from strawberry.codegen.plugins.python import PythonPlugin -from strawberry.codegen.plugins.typescript import TypeScriptPlugin - -HERE = Path(__file__).parent -QUERIES = list(HERE.glob("queries/*.graphql")) - - -@pytest.mark.parametrize( - ("plugin_class", "plugin_name", "extension"), - [ - (PythonPlugin, "python", "py"), - (TypeScriptPlugin, "typescript", "ts"), - ], - ids=["python", "typescript"], -) -@pytest.mark.parametrize("query", QUERIES, ids=[x.name for x in QUERIES]) -def test_codegen( - query: Path, - plugin_class: Type[QueryCodegenPlugin], - plugin_name: str, - extension: str, - snapshot: Snapshot, - schema, -): - generator = QueryCodegen(schema, plugins=[plugin_class()]) - - result = generator.run(query.read_text()) - - code = result.to_string() - - snapshot.snapshot_dir = HERE / "snapshots" / plugin_name - snapshot.assert_match(code, f"{query.with_suffix('').stem}.{extension}") - - -def test_codegen_fails_if_no_operation_name(schema): - generator = QueryCodegen(schema, plugins=[PythonPlugin()]) - - with pytest.raises(NoOperationNameProvidedError): - generator.run("query { hello }") - - -def test_codegen_fails_if_no_operation(schema): - generator = QueryCodegen(schema, plugins=[PythonPlugin()]) - - with pytest.raises(NoOperationProvidedError): - generator.run("type X { hello: String }") - - -def test_fails_with_multiple_operations(schema): - generator = QueryCodegen(schema, plugins=[PythonPlugin()]) - - with pytest.raises(MultipleOperationsProvidedError): - generator.run("query { hello } query { world }") diff --git a/tests/conftest.py b/tests/conftest.py index 09ddc9cb62..3bbaf85280 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,5 +1,2 @@ def pytest_emoji_xfailed(config): return "๐Ÿคทโ€โ™‚๏ธ ", "XFAIL ๐Ÿคทโ€โ™‚๏ธ " - - -pytest_plugins = ("tests.plugins.strawberry_exceptions",) diff --git a/tests/django/app/schema.py b/tests/django/app/schema.py index a3100e4df5..ff37d0e01b 100644 --- a/tests/django/app/schema.py +++ b/tests/django/app/schema.py @@ -17,10 +17,6 @@ class FolderInput: @strawberry.type class Mutation: - @strawberry.mutation - def hello(self) -> str: - return "strawberry" - @strawberry.mutation def read_text(self, text_file: Upload) -> str: return text_file.read().decode() @@ -39,11 +35,6 @@ def read_folder(self, folder: FolderInput) -> typing.List[str]: contents.append(file.read().decode()) return contents - @strawberry.mutation - def match_text(self, text_file: Upload, pattern: str) -> str: - text = text_file.read().decode() - return pattern if pattern in text else "" - @strawberry.type class Query: diff --git a/tests/django/conftest.py b/tests/django/conftest.py index 18e0816c96..874021454c 100644 --- a/tests/django/conftest.py +++ b/tests/django/conftest.py @@ -1,6 +1,7 @@ import pathlib import pytest + from django.test.client import Client from strawberry.django.test import GraphQLTestClient @@ -20,4 +21,4 @@ def pytest_collection_modifyitems(config, items): @pytest.fixture() def graphql_client(): - return GraphQLTestClient(Client()) + yield GraphQLTestClient(Client()) diff --git a/tests/django/django_settings.py b/tests/django/django_settings.py index a0f62fe24b..ff4d40640c 100644 --- a/tests/django/django_settings.py +++ b/tests/django/django_settings.py @@ -12,7 +12,3 @@ ] DATABASES = {"default": {"ENGINE": "django.db.backends.sqlite3", "NAME": ":memory:"}} - -# This is for channels integration, but only one django settings can be used -# per pytest_django settings -CHANNEL_LAYERS = {"default": {"BACKEND": "channels.layers.InMemoryChannelLayer"}} diff --git a/tests/django/test_async_view.py b/tests/django/test_async_view.py index f9e41c2029..d9d3cd3258 100644 --- a/tests/django/test_async_view.py +++ b/tests/django/test_async_view.py @@ -1,17 +1,19 @@ import json -import django import pytest + from asgiref.sync import sync_to_async -from django.core.exceptions import BadRequest, SuspiciousOperation + +import django +from django.core.exceptions import SuspiciousOperation from django.test.client import RequestFactory -from django.utils.http import urlencode import strawberry from strawberry.django.views import AsyncGraphQLView as AsyncBaseGraphQLView from .app.models import Example + pytestmark = [ pytest.mark.asyncio, pytest.mark.skipif( @@ -36,7 +38,7 @@ def _get_name(): get_name = sync_to_async(_get_name) - return await get_name() + return await get_name() # type: ignore schema = strawberry.Schema(query=Query) @@ -71,65 +73,6 @@ async def test_graphiql_view(): assert "GraphiQL" in body -async def test_async_graphql_get_query_using_params(): - params = {"query": "{ helloAsync }"} - - factory = RequestFactory() - request = factory.get( - "/graphql", - data=params, - ) - - response = await AsyncGraphQLView.as_view(schema=schema)(request) - data = json.loads(response.content.decode()) - - assert data["data"]["helloAsync"] == "async strawberry" - - -async def test_async_graphql_post_query_fails_using_params(): - params = {"query": "{ helloAsync }"} - - factory = RequestFactory() - request = factory.post( - "/graphql", - content_type="application/x-www-form-urlencoded", - QUERY_STRING=urlencode(params, doseq=True), - ) - - with pytest.raises( - SuspiciousOperation, match="No GraphQL query found in the request" - ): - await AsyncGraphQLView.as_view(schema=schema)(request) - - -async def test_async_graphql_get_does_not_allow_mutation(): - params = {"query": "mutation { hello }"} - - factory = RequestFactory() - request = factory.get( - "/graphql", - data=params, - ) - - with pytest.raises(BadRequest, match="mutations are not allowed when using GET"): - await AsyncGraphQLView.as_view(schema=schema)(request) - - -async def test_async_graphql_get_does_get_when_disabled(): - params = {"query": "{ helloAsync }"} - - factory = RequestFactory() - request = factory.get( - "/graphql", - data=params, - ) - - with pytest.raises(BadRequest, match="queries are not allowed when using GET"): - await AsyncGraphQLView.as_view(schema=schema, allow_queries_via_get=False)( - request - ) - - @pytest.mark.parametrize("method", ["DELETE", "HEAD", "PUT", "PATCH"]) async def test_disabled_methods(method): factory = RequestFactory() @@ -148,11 +91,11 @@ async def test_fails_when_not_sending_query(): request = factory.post("/graphql/") - with pytest.raises( - SuspiciousOperation, match="No GraphQL query found in the request" - ): + with pytest.raises(SuspiciousOperation) as e: await AsyncGraphQLView.as_view(schema=schema)(request) + assert e.value.args == ("No GraphQL query found in the request",) + async def test_fails_when_request_body_has_invalid_json(): factory = RequestFactory() @@ -161,11 +104,11 @@ async def test_fails_when_request_body_has_invalid_json(): "/graphql/", "definitely-not-json-string", content_type="application/json" ) - with pytest.raises( - SuspiciousOperation, match="Unable to parse request body as JSON" - ): + with pytest.raises(SuspiciousOperation) as e: await AsyncGraphQLView.as_view(schema=schema, graphiql=False)(request) + assert e.value.args == ("Unable to parse request body as JSON",) + @pytest.mark.django_db async def test_async_graphql_query_model(): diff --git a/tests/django/test_dataloaders.py b/tests/django/test_dataloaders.py index 1036094fc3..8d9e74ebf1 100644 --- a/tests/django/test_dataloaders.py +++ b/tests/django/test_dataloaders.py @@ -1,9 +1,11 @@ import json from typing import List -import django import pytest + from asgiref.sync import sync_to_async + +import django from django.test.client import RequestFactory import strawberry @@ -12,6 +14,7 @@ from .app.models import Example + pytestmark = [ pytest.mark.asyncio, pytest.mark.skipif( diff --git a/tests/django/test_graphql_test_client.py b/tests/django/test_graphql_test_client.py index 40442442da..8b4a211306 100644 --- a/tests/django/test_graphql_test_client.py +++ b/tests/django/test_graphql_test_client.py @@ -4,4 +4,4 @@ def test_assertion_error_not_raised_when_asserts_errors_is_false(graphql_client) try: graphql_client.query(query, asserts_errors=False) except AssertionError: - raise AssertionError() + assert False diff --git a/tests/django/test_upload.py b/tests/django/test_upload.py new file mode 100644 index 0000000000..7326e95fcf --- /dev/null +++ b/tests/django/test_upload.py @@ -0,0 +1,71 @@ +from django.core.files.uploadedfile import SimpleUploadedFile + + +def test_upload(graphql_client): + f = SimpleUploadedFile("file.txt", b"strawberry") + query = """mutation($textFile: Upload!) { + readText(textFile: $textFile) + }""" + + response = graphql_client.query( + query=query, + variables={"textFile": None}, + files={"textFile": f}, + ) + + assert response.data["readText"] == "strawberry" + + +def test_file_list_upload(graphql_client): + query = "mutation($files: [Upload!]!) { readFiles(files: $files) }" + file1 = SimpleUploadedFile("file1.txt", b"strawberry1") + file2 = SimpleUploadedFile("file2.txt", b"strawberry2") + + response = graphql_client.query( + query=query, + variables={"files": [None, None]}, + files={"file1": file1, "file2": file2}, + ) + + assert len(response.data["readFiles"]) == 2 + assert response.data["readFiles"][0] == "strawberry1" + assert response.data["readFiles"][1] == "strawberry2" + + +def test_nested_file_list(graphql_client): + query = "mutation($folder: FolderInput!) { readFolder(folder: $folder) }" + file1 = SimpleUploadedFile("file1.txt", b"strawberry1") + file2 = SimpleUploadedFile("file2.txt", b"strawberry2") + + response = graphql_client.query( + query=query, + variables={"folder": {"files": [None, None]}}, + files={"file1": file1, "file2": file2}, + ) + + assert len(response.data["readFolder"]) == 2 + assert response.data["readFolder"][0] == "strawberry1" + assert response.data["readFolder"][1] == "strawberry2" + + +def test_upload_single_and_list_file_together(graphql_client): + query = """ + mutation($files: [Upload!]!, $textFile: Upload!) { + readFiles(files: $files) + readText(textFile: $textFile) + } + """ + file1 = SimpleUploadedFile("file1.txt", b"strawberry1") + file2 = SimpleUploadedFile("file2.txt", b"strawberry2") + file3 = SimpleUploadedFile("file3.txt", b"strawberry3") + + response = graphql_client.query( + query=query, + variables={"files": [None, None], "textFile": None}, + files={"file1": file1, "file2": file2, "textFile": file3}, + ) + + assert len(response.data["readFiles"]) == 2 + assert response.data["readFiles"][0] == "strawberry1" + assert response.data["readFiles"][1] == "strawberry2" + assert response.data["readText"] == "strawberry3" diff --git a/tests/django/test_views.py b/tests/django/test_views.py index d0a13c050d..1e4bfe3595 100644 --- a/tests/django/test_views.py +++ b/tests/django/test_views.py @@ -2,15 +2,15 @@ from typing import Any, Optional import pytest -from django.http import JsonResponse + +from django.core.exceptions import SuspiciousOperation +from django.http import Http404 from django.test.client import RequestFactory import strawberry from strawberry.django.views import GraphQLView as BaseGraphQLView -from strawberry.django.views import TemporalHttpResponse -from strawberry.http import GraphQLHTTPResponse from strawberry.permission import BasePermission -from strawberry.types import Info +from strawberry.types import ExecutionResult, Info from .app.models import Example @@ -26,10 +26,6 @@ def has_permission(self, source: Any, info: Info, **kwargs) -> bool: class Query: hello: str = "strawberry" - @strawberry.field - def hi(self, name: Optional[str] = None) -> str: - return f"Hello {name or 'world'}" - @strawberry.field(permission_classes=[AlwaysFailPermission]) def always_fail(self) -> Optional[str]: return "Hey" @@ -68,6 +64,78 @@ def get_root_value(self, request): return Query() +def test_graphiql_view(): + factory = RequestFactory() + + request = factory.get("/graphql/", HTTP_ACCEPT="text/html") + + response = GraphQLView.as_view(schema=schema)(request) + body = response.content.decode() + + assert "GraphiQL" in body + + +@pytest.mark.parametrize("method", ["DELETE", "HEAD", "PUT", "PATCH"]) +def test_disabled_methods(method): + factory = RequestFactory() + + rf = getattr(factory, method.lower()) + + request = rf("/graphql/") + + response = GraphQLView.as_view(schema=schema, graphiql=False)(request) + + assert response.status_code == 405 + + +def test_fails_when_not_sending_query(): + factory = RequestFactory() + + request = factory.post("/graphql/") + + with pytest.raises(SuspiciousOperation) as e: + GraphQLView.as_view(schema=schema, graphiql=False)(request) + + assert e.value.args == ("No GraphQL query found in the request",) + + +def test_fails_when_request_body_has_invalid_json(): + factory = RequestFactory() + + request = factory.post( + "/graphql/", "definitely-not-json-string", content_type="application/json" + ) + + with pytest.raises(SuspiciousOperation) as e: + GraphQLView.as_view(schema=schema, graphiql=False)(request) + + assert e.value.args == ("Unable to parse request body as JSON",) + + +def test_graphiql_disabled_view(): + factory = RequestFactory() + + request = factory.get("/graphql/", HTTP_ACCEPT="text/html") + + with pytest.raises(Http404): + GraphQLView.as_view(schema=schema, graphiql=False)(request) + + +def test_graphql_query(): + query = "{ hello }" + + factory = RequestFactory() + request = factory.post( + "/graphql/", {"query": query}, content_type="application/json" + ) + + response = GraphQLView.as_view(schema=schema)(request) + data = json.loads(response.content.decode()) + + assert response["content-type"] == "application/json" + assert data["data"]["hello"] == "strawberry" + + @pytest.mark.django_db def test_graphql_query_model(): Example.objects.create(name="This is a demo") @@ -88,6 +156,104 @@ def test_graphql_query_model(): Example.objects.all().delete() +def test_returns_errors_and_data(): + query = "{ hello, alwaysFail }" + + factory = RequestFactory() + request = factory.post( + "/graphql/", {"query": query}, content_type="application/json" + ) + + response = GraphQLView.as_view(schema=schema)(request) + data = json.loads(response.content.decode()) + + assert response.status_code == 200 + + assert data["data"]["hello"] == "strawberry" + assert data["data"]["alwaysFail"] is None + + assert len(data["errors"]) == 1 + assert data["errors"][0]["message"] == "You are not authorized" + + +@pytest.mark.parametrize( + "query", + ( + GetRequestValueWithDotNotationQuery, + GetRequestValueUsingGetQuery, + GetRequestValueQuery, + ), +) +def test_strawberry_django_context(query): + factory = RequestFactory() + + schema = strawberry.Schema(query=query) + + query = "{ getRequestValue }" + request = factory.post( + "/graphql/", {"query": query}, content_type="application/json" + ) + + response = GraphQLView.as_view(schema=schema)(request) + data = json.loads(response.content.decode()) + assert response.status_code == 200 + assert data["data"] == {"getRequestValue": ""} + + +def test_custom_context(): + class CustomGraphQLView(BaseGraphQLView): + def get_context(self, request, response): + return {"request": request, "custom_value": "Hi!"} + + factory = RequestFactory() + + @strawberry.type + class Query: + @strawberry.field + def custom_context_value(self, info: Info) -> str: + return info.context["custom_value"] + + schema = strawberry.Schema(query=Query) + + query = "{ customContextValue }" + request = factory.post( + "/graphql/", {"query": query}, content_type="application/json" + ) + + response = CustomGraphQLView.as_view(schema=schema)(request) + data = json.loads(response.content.decode()) + + assert response.status_code == 200 + assert data["data"] == {"customContextValue": "Hi!"} + + +def test_custom_process_result(): + class CustomGraphQLView(BaseGraphQLView): + def process_result(self, request, result: ExecutionResult): + return {} + + factory = RequestFactory() + + @strawberry.type + class Query: + @strawberry.field + def abc(self) -> str: + return "ABC" + + schema = strawberry.Schema(query=Query) + + query = "{ abc }" + request = factory.post( + "/graphql/", {"query": query}, content_type="application/json" + ) + + response = CustomGraphQLView.as_view(schema=schema)(request) + data = json.loads(response.content.decode()) + + assert response.status_code == 200 + assert data == {} + + def test_can_set_cookies(): factory = RequestFactory() @@ -165,7 +331,7 @@ def abc(self, info: Info) -> str: assert data == {"data": {"abc": "ABC"}} -def test_custom_json_encoder(): +def test_json_encoder(): query = "{ hello }" factory = RequestFactory() @@ -173,42 +339,22 @@ def test_custom_json_encoder(): "/graphql/", {"query": query}, content_type="application/json" ) - class MyGraphQLView(BaseGraphQLView): - def encode_json(self, response_data: GraphQLHTTPResponse) -> str: - return "fake_encoder" - - response = MyGraphQLView.as_view(schema=schema)(request) - assert response.content.decode() == "fake_encoder" - - -def test_json_encoder_as_class_works_with_warning(): class CustomEncoder(json.JSONEncoder): def encode(self, o: Any) -> str: - return "this is deprecated" - - query = "{ hello }" - - factory = RequestFactory() - request = factory.post( - "/graphql/", {"query": query}, content_type="application/json" - ) - - with pytest.warns(DeprecationWarning): - response1 = GraphQLView.as_view(schema=schema, json_encoder=CustomEncoder)( - request - ) + # Reverse the result. + return super().encode(o)[::-1] - assert response1.content.decode() == "this is deprecated" + response1 = GraphQLView.as_view(schema=schema, json_encoder=CustomEncoder)(request) + assert response1.content.decode() == '{"data": {"hello": "strawberry"}}'[::-1] - with pytest.warns(DeprecationWarning): + class CustomGraphQLView(GraphQLView): + json_encoder = CustomEncoder - class CustomGraphQLView(GraphQLView): - json_encoder = CustomEncoder + response2 = CustomGraphQLView.as_view(schema=schema)(request) + assert response1.content == response2.content - CustomGraphQLView(schema=schema) - -def test_json_dumps_params_deprecated_via_param(): +def test_json_dumps_params(): query = "{ hello }" factory = RequestFactory() @@ -218,37 +364,13 @@ def test_json_dumps_params_deprecated_via_param(): dumps_params = {"separators": (",", ":")} - with pytest.warns(DeprecationWarning): - response1 = GraphQLView.as_view(schema=schema, json_dumps_params=dumps_params)( - request - ) - assert response1.content.decode() == '{"data":{"hello":"strawberry"}}' - - -def test_json_dumps_params_deprecated_via_property(): - query = "{ hello }" - - factory = RequestFactory() - request = factory.post( - "/graphql/", {"query": query}, content_type="application/json" + response1 = GraphQLView.as_view(schema=schema, json_dumps_params=dumps_params)( + request ) + assert response1.content.decode() == '{"data":{"hello":"strawberry"}}' - dumps_params = {"separators": (",", ":")} - - with pytest.warns(DeprecationWarning): - - class CustomGraphQLView(GraphQLView): - json_dumps_params = dumps_params - - response = CustomGraphQLView.as_view(schema=schema)(request) - assert response.content.decode() == '{"data":{"hello":"strawberry"}}' - - -def test_TemporalHttpResponse() -> None: - resp = TemporalHttpResponse() - assert repr(resp) == '' + class CustomGraphQLView(GraphQLView): + json_dumps_params = dumps_params - # Check that `__repr__` matches Django's output. - resp.status_code = 200 - repr1 = repr(resp).replace("TemporalHttpResponse", "JsonResponse") - assert repr1 == repr(JsonResponse({})) + response2 = CustomGraphQLView.as_view(schema=schema)(request) + assert response1.content == response2.content diff --git a/tests/enums/test_enum.py b/tests/enums/test_enum.py index 53bfe8ab3d..a7e3153aeb 100644 --- a/tests/enums/test_enum.py +++ b/tests/enums/test_enum.py @@ -5,7 +5,6 @@ import strawberry from strawberry.enum import EnumDefinition from strawberry.exceptions import ObjectIsNotAnEnumError -from strawberry.exceptions.not_a_strawberry_enum import NotAStrawberryEnumError def test_basic_enum(): @@ -61,95 +60,10 @@ def flavour_available(self, flavour: IceCreamFlavour) -> bool: assert isinstance(field.arguments[0].type, EnumDefinition) -@pytest.mark.raises_strawberry_exception( - ObjectIsNotAnEnumError, - match="strawberry.enum can only be used with subclasses of Enum. ", -) def test_raises_error_when_using_enum_with_a_not_enum_class(): - @strawberry.enum - class AClass: - hello = "world" - - -def test_can_deprecate_enum_values(): - @strawberry.enum - class IceCreamFlavour(Enum): - VANILLA = strawberry.enum_value("vanilla") - STRAWBERRY = strawberry.enum_value( - "strawberry", deprecation_reason="We ran out" - ) - CHOCOLATE = "chocolate" - - definition = IceCreamFlavour._enum_definition - - assert definition.values[0].name == "VANILLA" - assert definition.values[0].value == "vanilla" - assert definition.values[0].deprecation_reason is None - - assert definition.values[1].name == "STRAWBERRY" - assert definition.values[1].value == "strawberry" - assert definition.values[1].deprecation_reason == "We ran out" - - assert definition.values[2].name == "CHOCOLATE" - assert definition.values[2].value == "chocolate" - assert definition.values[2].deprecation_reason is None - - -def test_can_describe_enum_values(): - @strawberry.enum - class IceCreamFlavour(Enum): - VANILLA = strawberry.enum_value("vanilla") - STRAWBERRY = strawberry.enum_value( - "strawberry", - description="Our favourite", - ) - CHOCOLATE = "chocolate" + expected_error = "strawberry.enum can only be used with subclasses of Enum" + with pytest.raises(ObjectIsNotAnEnumError, match=expected_error): - definition = IceCreamFlavour._enum_definition - - assert definition.values[0].name == "VANILLA" - assert definition.values[0].value == "vanilla" - assert definition.values[0].description is None - - assert definition.values[1].name == "STRAWBERRY" - assert definition.values[1].value == "strawberry" - assert definition.values[1].description == "Our favourite" - - assert definition.values[2].name == "CHOCOLATE" - assert definition.values[2].value == "chocolate" - assert definition.values[2].description is None - - -@pytest.mark.raises_strawberry_exception( - NotAStrawberryEnumError, match='Enum "IceCreamFlavour" is not a Strawberry enum' -) -def test_raises_error_when_using_enum_not_decorated(): - class IceCreamFlavour(Enum): - VANILLA = strawberry.enum_value("vanilla") - STRAWBERRY = strawberry.enum_value( - "strawberry", - description="Our favourite", - ) - CHOCOLATE = "chocolate" - - @strawberry.type - class Query: - flavour: IceCreamFlavour - - strawberry.Schema(query=Query) - - -def test_can_use_enum_values(): - @strawberry.enum - class TestEnum(Enum): - A = "A" - B = strawberry.enum_value("B") - C = strawberry.enum_value("Coconut", deprecation_reason="We ran out") - - assert TestEnum.B.value == "B" - - assert [x.value for x in TestEnum.__members__.values()] == [ - "A", - "B", - "Coconut", - ] + @strawberry.enum + class NormalClass: + hello = "world" diff --git a/tests/exceptions/__init__.py b/tests/exceptions/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/exceptions/test_exception_handler.py b/tests/exceptions/test_exception_handler.py deleted file mode 100644 index 46ba4a9f33..0000000000 --- a/tests/exceptions/test_exception_handler.py +++ /dev/null @@ -1,91 +0,0 @@ -import os -import sys - -from strawberry.exceptions import MissingFieldAnnotationError -from strawberry.exceptions.handler import ( - reset_exception_handler, - setup_exception_handler, - strawberry_exception_handler, -) - - -def test_exception_handler(mocker): - print_mock = mocker.patch("rich.print", autospec=True) - - class Query: - abc: int - - exception = MissingFieldAnnotationError("abc", Query) - - strawberry_exception_handler(MissingFieldAnnotationError, exception, None) - - assert print_mock.call_args == mocker.call(exception) - - -def test_exception_handler_other_exceptions(mocker): - print_mock = mocker.patch("rich.print", autospec=True) - original_exception_mock = mocker.patch( - "strawberry.exceptions.handler.sys.__excepthook__", autospec=True - ) - - exception = ValueError("abc") - - strawberry_exception_handler(ValueError, exception, None) - - assert print_mock.called is False - assert original_exception_mock.call_args == mocker.call(ValueError, exception, None) - - -def test_exception_handler_uses_original_when_rich_is_not_installed(mocker): - original_exception_mock = mocker.patch( - "strawberry.exceptions.handler.sys.__excepthook__", autospec=True - ) - - mocker.patch.dict("sys.modules", {"rich": None}) - - class Query: - abc: int - - exception = MissingFieldAnnotationError("abc", Query) - - strawberry_exception_handler(MissingFieldAnnotationError, exception, None) - - assert original_exception_mock.call_args == mocker.call( - MissingFieldAnnotationError, exception, None - ) - - -def test_exception_handler_uses_original_when_libcst_is_not_installed(mocker): - original_exception_mock = mocker.patch( - "strawberry.exceptions.handler.sys.__excepthook__", autospec=True - ) - - mocker.patch.dict("sys.modules", {"libcst": None}) - - class Query: - abc: int - - exception = MissingFieldAnnotationError("abc", Query) - - strawberry_exception_handler(MissingFieldAnnotationError, exception, None) - - assert original_exception_mock.call_args == mocker.call( - MissingFieldAnnotationError, exception, None - ) - - -def test_setup_install_handler(mocker): - reset_exception_handler() - setup_exception_handler() - - assert sys.excepthook == strawberry_exception_handler - - -def test_setup_does_not_install_handler_when_disabled_via_env(mocker): - reset_exception_handler() - - mocker.patch.dict(os.environ, {"STRAWBERRY_DISABLE_RICH_ERRORS": "true"}) - - setup_exception_handler() - - assert sys.excepthook != strawberry_exception_handler diff --git a/tests/exceptions/test_exception_source.py b/tests/exceptions/test_exception_source.py deleted file mode 100644 index be9235fda1..0000000000 --- a/tests/exceptions/test_exception_source.py +++ /dev/null @@ -1,40 +0,0 @@ -import sys -from pathlib import Path - -import pytest - -from strawberry.exceptions.exception_source import ExceptionSource - -pytestmark = pytest.mark.skipif( - sys.platform == "win32", reason="Test is meant to run on Unix systems" -) - - -def test_returns_relative_path(mocker): - mocker.patch.object(Path, "cwd", return_value="/home/user/project/") - - source = ExceptionSource( - path=Path("/home/user/project/src/main.py"), - code="", - start_line=1, - end_line=1, - error_line=1, - error_column=1, - error_column_end=1, - ) - - assert source.path_relative_to_cwd == Path("src/main.py") - - -def test_returns_relative_path_when_is_already_relative(): - source = ExceptionSource( - path=Path("src/main.py"), - code="", - start_line=1, - end_line=1, - error_line=1, - error_column=1, - error_column_end=1, - ) - - assert source.path_relative_to_cwd == Path("src/main.py") diff --git a/tests/exceptions/test_threading_exception_handler.py b/tests/exceptions/test_threading_exception_handler.py deleted file mode 100644 index 7f969f301d..0000000000 --- a/tests/exceptions/test_threading_exception_handler.py +++ /dev/null @@ -1,114 +0,0 @@ -import os -import sys -import threading - -import pytest - -from strawberry.exceptions import MissingFieldAnnotationError -from strawberry.exceptions.handler import ( - reset_exception_handler, - setup_exception_handler, - strawberry_threading_exception_handler, -) - - -def test_exception_handler(mocker): - print_mock = mocker.patch("rich.print", autospec=True) - - class Query: - abc: int - - exception = MissingFieldAnnotationError("abc", Query) - - strawberry_threading_exception_handler( - (MissingFieldAnnotationError, exception, None, None) - ) - - assert print_mock.call_args == mocker.call(exception) - - -def test_exception_handler_other_exceptions(mocker): - print_mock = mocker.patch("rich.print", autospec=True) - original_exception_mock = mocker.patch( - "strawberry.exceptions.handler.sys.__excepthook__", autospec=True - ) - - exception = ValueError("abc") - - strawberry_threading_exception_handler((ValueError, exception, None, None)) - - assert print_mock.called is False - assert original_exception_mock.call_args == mocker.call(ValueError, exception, None) - - -def test_exception_handler_uses_original_when_rich_is_not_installed(mocker): - original_exception_mock = mocker.patch( - "strawberry.exceptions.handler.sys.__excepthook__", autospec=True - ) - - mocker.patch.dict("sys.modules", {"rich": None}) - - class Query: - abc: int - - exception = MissingFieldAnnotationError("abc", Query) - - strawberry_threading_exception_handler( - (MissingFieldAnnotationError, exception, None, None) - ) - - assert original_exception_mock.call_args == mocker.call( - MissingFieldAnnotationError, exception, None - ) - - -def test_exception_handler_uses_original_when_libcst_is_not_installed(mocker): - original_exception_mock = mocker.patch( - "strawberry.exceptions.handler.sys.__excepthook__", autospec=True - ) - - mocker.patch.dict("sys.modules", {"libcst": None}) - - class Query: - abc: int - - exception = MissingFieldAnnotationError("abc", Query) - - strawberry_threading_exception_handler( - (MissingFieldAnnotationError, exception, None, None) - ) - - assert original_exception_mock.call_args == mocker.call( - MissingFieldAnnotationError, exception, None - ) - - -@pytest.mark.skipif( - sys.version_info < (3, 8), reason="threading.excepthook is only available in 3.8+" -) -def test_setup_install_handler(mocker): - reset_exception_handler() - setup_exception_handler() - - assert threading.excepthook == strawberry_threading_exception_handler - - -@pytest.mark.skipif(sys.version_info >= (3, 8), reason="test for python < 3.8") -def test_setup_install_handler_does_add_attribute(mocker): - reset_exception_handler() - setup_exception_handler() - - assert hasattr(threading, "excepthook") is False - - -@pytest.mark.skipif( - sys.version_info < (3, 8), reason="threading.excepthook is only available in 3.8+" -) -def test_setup_does_not_install_handler_when_disabled_via_env(mocker): - reset_exception_handler() - - mocker.patch.dict(os.environ, {"STRAWBERRY_DISABLE_RICH_ERRORS": "true"}) - - setup_exception_handler() - - assert threading.excepthook != strawberry_threading_exception_handler diff --git a/tests/experimental/pydantic/schema/test_basic.py b/tests/experimental/pydantic/schema/test_basic.py index 2ba2decc10..5c5744b1d3 100644 --- a/tests/experimental/pydantic/schema/test_basic.py +++ b/tests/experimental/pydantic/schema/test_basic.py @@ -467,8 +467,8 @@ def user(self) -> User: } type User { - name: String! age: Int! + name: String! } """ assert str(schema) == textwrap.dedent(expected_schema).strip() diff --git a/tests/experimental/pydantic/schema/test_forward_reference.py b/tests/experimental/pydantic/schema/test_forward_reference.py deleted file mode 100644 index ebc94d4b37..0000000000 --- a/tests/experimental/pydantic/schema/test_forward_reference.py +++ /dev/null @@ -1,50 +0,0 @@ -from __future__ import annotations - -import textwrap -from typing import Optional - -import pydantic - -import strawberry - - -def test_auto_fields(): - global User - - class UserModel(pydantic.BaseModel): - age: int - password: Optional[str] - other: float - - @strawberry.experimental.pydantic.type(UserModel) - class User: - age: strawberry.auto - password: strawberry.auto - - @strawberry.type - class Query: - @strawberry.field - def user(self) -> User: - return User(age=1, password="ABC") - - schema = strawberry.Schema(query=Query) - - expected_schema = """ - type Query { - user: User! - } - - type User { - age: Int! - password: String - } - """ - - assert str(schema) == textwrap.dedent(expected_schema).strip() - - query = "{ user { age } }" - - result = schema.execute_sync(query) - - assert not result.errors - assert result.data["user"]["age"] == 1 diff --git a/tests/experimental/pydantic/schema/test_mutation.py b/tests/experimental/pydantic/schema/test_mutation.py index 5ae48f65a3..e6f42c3a34 100644 --- a/tests/experimental/pydantic/schema/test_mutation.py +++ b/tests/experimental/pydantic/schema/test_mutation.py @@ -25,7 +25,7 @@ class Query: class Mutation: @strawberry.mutation def create_user(self, input: CreateUserInput) -> UserType: - return UserType(name=input.name) + return UserType(input.name) schema = strawberry.Schema(query=Query, mutation=Mutation) @@ -65,7 +65,7 @@ class Mutation: def create_user(self, input: CreateUserInput) -> UserType: data = input.to_pydantic() - return UserType(name=data.name) + return UserType(data.name) schema = strawberry.Schema(query=Query, mutation=Mutation) @@ -117,7 +117,7 @@ class Mutation: def create_user(self, input: CreateUserInput) -> UserType: data = input.to_pydantic() - return UserType(name=data.name) + return UserType(data.name) schema = strawberry.Schema(query=Query, mutation=Mutation) @@ -173,7 +173,7 @@ def create_user(self, input: CreateUserInput) -> Union[UserType, UserError]: args[field] = field_errors return UserError(**args) else: - return UserType(name=data.name) + return UserType(data.name) schema = strawberry.Schema(query=Query, mutation=Mutation) diff --git a/tests/experimental/pydantic/test_basic.py b/tests/experimental/pydantic/test_basic.py index fddf61c5cc..7d86a6fbfb 100644 --- a/tests/experimental/pydantic/test_basic.py +++ b/tests/experimental/pydantic/test_basic.py @@ -1,14 +1,15 @@ import dataclasses from enum import Enum -from typing import Any, List, Optional, Union +from typing import List, Optional, Union -import pydantic import pytest +import pydantic +import sentinel + import strawberry from strawberry.enum import EnumDefinition from strawberry.experimental.pydantic.exceptions import MissingFieldsListError -from strawberry.schema_directive import Location from strawberry.type import StrawberryList, StrawberryOptional from strawberry.types.types import TypeDefinition from strawberry.union import StrawberryUnion @@ -105,8 +106,7 @@ class UserType: def test_auto_fields_other_sentinel(): - class other_sentinel: - pass + other_sentinel = sentinel.create("other_sentinel") class User(pydantic.BaseModel): age: int @@ -128,43 +128,60 @@ class UserType: assert field1.graphql_name is None assert field1.type is int - assert field2.python_name == "password" + assert field2.python_name == "other" assert field2.graphql_name is None - assert isinstance(field2.type, StrawberryOptional) - assert field2.type.of_type is str + assert field2.type is other_sentinel - assert field3.python_name == "other" + assert field3.python_name == "password" assert field3.graphql_name is None - assert field3.type is other_sentinel + assert isinstance(field3.type, StrawberryOptional) + assert field3.type.of_type is str # def test_referencing_other_models_fails_when_not_registered(): # class Group(pydantic.BaseModel): +# name: str # class User(pydantic.BaseModel): +# age: int +# password: Optional[str] +# group: Group # with pytest.raises( # strawberry.experimental.pydantic.UnregisteredTypeException, +# match=("Cannot find a Strawberry Type for (.*) did you forget to register it?"), # ): # @strawberry.experimental.pydantic.type(User) # class UserType: +# age: strawberry.auto +# password: strawberry.auto +# group: strawberry.auto # def test_referencing_other_input_models_fails_when_not_registered(): # class Group(pydantic.BaseModel): +# name: str # class User(pydantic.BaseModel): +# age: int +# password: Optional[str] +# group: Group # @strawberry.experimental.pydantic.type(Group) # class GroupType: +# name: strawberry.auto # with pytest.raises( # strawberry.experimental.pydantic.UnregisteredTypeException, +# match=("Cannot find a Strawberry Type for (.*) did you forget to register it?"), # ): # @strawberry.experimental.pydantic.input(User) # class UserInputType: +# age: strawberry.auto +# password: strawberry.auto +# group: strawberry.auto def test_referencing_other_registered_models(): @@ -269,11 +286,11 @@ class UserType: [field1, field2, field3] = definition.fields - assert field1.python_name == "name" - assert field1.type is str + assert field1.python_name == "age" + assert field1.type is int - assert field2.python_name == "age" - assert field2.type is int + assert field2.python_name == "name" + assert field2.type is str assert field3.python_name == "password" assert isinstance(field3.type, StrawberryOptional) @@ -339,14 +356,13 @@ class UserType: definition: TypeDefinition = UserType._type_definition assert definition.name == "UserType" - [groups_field, friends_field] = definition.fields - - assert groups_field.default is dataclasses.MISSING - assert groups_field.default_factory is dataclasses.MISSING - assert friends_field.default is dataclasses.MISSING + [field1, field2] = definition.fields + assert field1.default is dataclasses.MISSING + assert field2.default is dataclasses.MISSING + assert field1.default_factory is dataclasses.MISSING # check that we really made a copy - assert friends_field.default_factory() is not empty_list + assert field2.default_factory() is not empty_list assert UserType(groups=["groups"]).friends is not empty_list UserType(groups=["groups"]).friends.append("joe") assert empty_list == [] @@ -407,11 +423,11 @@ class UserType: [field1, field2, field3] = definition.fields - assert field1.python_name == "name" - assert field1.type is Name + assert field1.python_name == "age" + assert field1.type is int - assert field2.python_name == "age" - assert field2.type is int + assert field2.python_name == "name" + assert field2.type is Name assert field3.python_name == "password" assert isinstance(field3.type, StrawberryOptional) @@ -685,163 +701,3 @@ class User: assert field2.python_name == "password" assert isinstance(field2.type, StrawberryOptional) assert field2.type.of_type is str - - -def test_deprecated_fields(): - class User(pydantic.BaseModel): - age: int - password: Optional[str] - other: float - - @strawberry.experimental.pydantic.type(User) - class UserType: - age: strawberry.auto = strawberry.field(deprecation_reason="Because") - password: strawberry.auto - - definition: TypeDefinition = UserType._type_definition - assert definition.name == "UserType" - - [field1, field2] = definition.fields - - assert field1.python_name == "age" - assert field1.graphql_name is None - assert field1.type is int - assert field1.deprecation_reason == "Because" - - assert field2.python_name == "password" - assert field2.graphql_name is None - assert isinstance(field2.type, StrawberryOptional) - assert field2.type.of_type is str - - -def test_permission_classes(): - class IsAuthenticated(strawberry.BasePermission): - message = "User is not authenticated" - - def has_permission( - self, source: Any, info: strawberry.types.Info, **kwargs - ) -> bool: - return False - - class User(pydantic.BaseModel): - age: int - password: Optional[str] - other: float - - @strawberry.experimental.pydantic.type(User) - class UserType: - age: strawberry.auto = strawberry.field(permission_classes=[IsAuthenticated]) - password: strawberry.auto - - definition: TypeDefinition = UserType._type_definition - assert definition.name == "UserType" - - [field1, field2] = definition.fields - - assert field1.python_name == "age" - assert field1.graphql_name is None - assert field1.type is int - assert field1.permission_classes == [IsAuthenticated] - - assert field2.python_name == "password" - assert field2.graphql_name is None - assert isinstance(field2.type, StrawberryOptional) - assert field2.type.of_type is str - - -def test_field_directives(): - @strawberry.schema_directive(locations=[Location.FIELD_DEFINITION]) - class Sensitive: - reason: str - - class User(pydantic.BaseModel): - age: int - password: Optional[str] - other: float - - @strawberry.experimental.pydantic.type(User) - class UserType: - age: strawberry.auto = strawberry.field(directives=[Sensitive(reason="GDPR")]) - password: strawberry.auto - - definition: TypeDefinition = UserType._type_definition - assert definition.name == "UserType" - - [field1, field2] = definition.fields - - assert field1.python_name == "age" - assert field1.graphql_name is None - assert field1.type is int - assert field1.directives == [Sensitive(reason="GDPR")] - - assert field2.python_name == "password" - assert field2.graphql_name is None - assert isinstance(field2.type, StrawberryOptional) - assert field2.type.of_type is str - - -def test_alias_fields(): - class User(pydantic.BaseModel): - age: int - - @strawberry.experimental.pydantic.type(User) - class UserType: - age: strawberry.auto = strawberry.field(name="ageAlias") - - definition: TypeDefinition = UserType._type_definition - assert definition.name == "UserType" - - field1 = definition.fields[0] - - assert field1.python_name == "age" - assert field1.graphql_name == "ageAlias" - assert field1.type is int - - -def test_alias_fields_with_use_pydantic_alias(): - class User(pydantic.BaseModel): - age: int - state: str = pydantic.Field(alias="statePydantic") - country: str = pydantic.Field(alias="countryPydantic") - - @strawberry.experimental.pydantic.type(User, use_pydantic_alias=True) - class UserType: - age: strawberry.auto = strawberry.field(name="ageAlias") - state: strawberry.auto = strawberry.field(name="state") - country: strawberry.auto - - definition: TypeDefinition = UserType._type_definition - assert definition.name == "UserType" - - [field1, field2, field3] = definition.fields - - assert field1.python_name == "age" - assert field1.graphql_name == "ageAlias" - - assert field2.python_name == "state" - assert field2.graphql_name == "state" - - assert field3.python_name == "country" - assert field3.graphql_name == "countryPydantic" - - -def test_field_metadata(): - class User(pydantic.BaseModel): - private: bool - public: bool - - @strawberry.experimental.pydantic.type(User) - class UserType: - private: strawberry.auto = strawberry.field(metadata={"admin_only": True}) - public: strawberry.auto - - definition: TypeDefinition = UserType._type_definition - assert definition.name == "UserType" - - [field1, field2] = definition.fields - - assert field1.python_name == "private" - assert field1.metadata["admin_only"] - - assert field2.python_name == "public" - assert not field2.metadata diff --git a/tests/experimental/pydantic/test_conversion.py b/tests/experimental/pydantic/test_conversion.py index 488335295b..ff007ba002 100644 --- a/tests/experimental/pydantic/test_conversion.py +++ b/tests/experimental/pydantic/test_conversion.py @@ -1,22 +1,27 @@ import base64 -import dataclasses import re -import sys from enum import Enum from typing import Any, Dict, List, NewType, Optional, Union, cast import pytest -from pydantic import BaseConfig, BaseModel, Field, ValidationError + +from pydantic import BaseConfig, BaseModel, Field from pydantic.fields import ModelField from pydantic.typing import NoArgAnyCallable import strawberry +from strawberry.arguments import UNSET from strawberry.experimental.pydantic.exceptions import ( AutoFieldsNotInBaseModelError, BothDefaultAndDefaultFactoryDefinedError, ) -from strawberry.experimental.pydantic.utils import get_default_factory_for_field -from strawberry.type import StrawberryList, StrawberryOptional +from strawberry.experimental.pydantic.utils import ( + DataclassCreationFields, + get_default_factory_for_field, + sort_creation_fields, +) +from strawberry.field import StrawberryField +from strawberry.type import StrawberryOptional from strawberry.types.types import TypeDefinition @@ -742,7 +747,7 @@ class UserInput: age: strawberry.auto password: strawberry.auto - data = UserInput(age=1, password=None) + data = UserInput(1, None) user = data.to_pydantic() assert user.age == 1 @@ -787,9 +792,11 @@ class UserInput: assert definition.name == "UserInput" [ - password_field, age_field, - ] = definition.fields + password_field, + ] = ( + definition.fields + ) # fields without a default go first, so the order gets reverse assert age_field.python_name == "age" assert age_field.type is int @@ -818,29 +825,54 @@ class UserType: assert user.password == "abc" -def test_can_convert_pydantic_type_to_strawberry_newtype_list(): - Password = NewType("Password", str) - - class User(BaseModel): - age: int - passwords: List[Password] - - @strawberry.experimental.pydantic.type(User) - class UserType: - age: strawberry.auto - passwords: strawberry.auto - - origin_user = User(age=1, passwords=["hunter2"]) - user = UserType.from_pydantic(origin_user) - - assert user.age == 1 - assert user.passwords == ["hunter2"] +def test_sort_creation_fields(): + has_default = DataclassCreationFields( + name="has_default", + type_annotation=str, + field=StrawberryField( + python_name="has_default", + graphql_name="has_default", + default="default_str", + default_factory=UNSET, + type_annotation=str, + description="description", + ), + ) + has_default_factory = DataclassCreationFields( + name="has_default_factory", + type_annotation=str, + field=StrawberryField( + python_name="has_default_factory", + graphql_name="has_default_factory", + default=UNSET, + default_factory=lambda: "default_factory_str", + type_annotation=str, + description="description", + ), + ) + no_defaults = DataclassCreationFields( + name="no_defaults", + type_annotation=str, + field=StrawberryField( + python_name="no_defaults", + graphql_name="no_defaults", + default=UNSET, + default_factory=UNSET, + type_annotation=str, + description="description", + ), + ) + fields = [has_default, has_default_factory, no_defaults] + # should place items with defaults last + assert sort_creation_fields(fields) == [ + no_defaults, + has_default, + has_default_factory, + ] def test_get_default_factory_for_field(): - def _get_field( - default: Any = dataclasses.MISSING, default_factory: Any = dataclasses.MISSING - ) -> ModelField: + def _get_field(default: Any = UNSET, default_factory: Any = UNSET) -> ModelField: return ModelField( name="a", type_=str, @@ -850,9 +882,10 @@ def _get_field( default_factory=default_factory, ) + # should return UNSET when both defaults are UNSET field = _get_field() - assert get_default_factory_for_field(field) is dataclasses.MISSING + assert get_default_factory_for_field(field) is UNSET def factory_func(): return "strawberry" @@ -914,10 +947,10 @@ def age() -> int: origin_user = UserModel(password="abc", new_age=21) user = User.from_pydantic(origin_user) assert user.password == "abc" - assert User._type_definition.fields[0].name == "age" - assert User._type_definition.fields[0].base_resolver() == 42 - assert User._type_definition.fields[2].name == "new_age" - assert User._type_definition.fields[2].base_resolver() == 84 + assert User._type_definition.fields[0].name == "new_age" + assert User._type_definition.fields[0].base_resolver() == 84 + assert User._type_definition.fields[1].name == "age" + assert User._type_definition.fields[1].base_resolver() == 42 def test_can_convert_both_output_and_input_type(): @@ -1104,93 +1137,3 @@ class UserInput: assert user.age == 1 assert user.password is None assert user.work["Monday"].hours == 1 - - -def test_can_add_missing_arguments_to_pydantic(): - class User(BaseModel): - age: int - password: str - - @strawberry.experimental.pydantic.type(User) - class UserInput: - age: strawberry.auto - - data = UserInput(age=1) - user = data.to_pydantic(password="hunter2") - - assert user.age == 1 - assert user.password == "hunter2" - - -def test_raise_missing_arguments_to_pydantic(): - class User(BaseModel): - age: int - password: str - - @strawberry.experimental.pydantic.type(User) - class UserInput: - age: strawberry.auto - - data = UserInput(age=1) - - with pytest.raises( - ValidationError, - match=("1 validation error for User"), - ): - data.to_pydantic() - - -@pytest.mark.skipif( - sys.version_info < (3, 9), - reason="generic aliases where added in python 3.9", -) -def test_can_convert_generic_alias_fields_to_strawberry(): - class TestModel(BaseModel): - list_1d: list[int] - list_2d: list[list[int]] - - @strawberry.experimental.pydantic.type(TestModel) - class Test: - list_1d: strawberry.auto - list_2d: strawberry.auto - - fields = Test._type_definition.fields - assert isinstance(fields[0].type, StrawberryList) - assert isinstance(fields[1].type, StrawberryList) - - model = TestModel( - list_1d=[1, 2, 3], - list_2d=[[1, 2], [3]], - ) - test = Test.from_pydantic(model) - - assert test.list_1d == [1, 2, 3] - assert test.list_2d == [[1, 2], [3]] - - -@pytest.mark.skipif( - sys.version_info < (3, 10), - reason="union type expressions were added in python 3.10", -) -def test_can_convert_optional_union_type_expression_fields_to_strawberry(): - class TestModel(BaseModel): - optional_list: list[int] | None - optional_str: str | None - - @strawberry.experimental.pydantic.type(TestModel) - class Test: - optional_list: strawberry.auto - optional_str: strawberry.auto - - fields = Test._type_definition.fields - assert isinstance(fields[0].type, StrawberryOptional) - assert isinstance(fields[1].type, StrawberryOptional) - - model = TestModel( - optional_list=[1, 2, 3], - optional_str=None, - ) - test = Test.from_pydantic(model) - - assert test.optional_list == [1, 2, 3] - assert test.optional_str is None diff --git a/tests/experimental/pydantic/test_error_type.py b/tests/experimental/pydantic/test_error_type.py index 72dcfd3203..f850f93ec3 100644 --- a/tests/experimental/pydantic/test_error_type.py +++ b/tests/experimental/pydantic/test_error_type.py @@ -1,8 +1,9 @@ from typing import List, Optional -import pydantic import pytest +import pydantic + import strawberry from strawberry.experimental.pydantic.exceptions import MissingFieldsListError from strawberry.type import StrawberryList, StrawberryOptional diff --git a/tests/experimental/pydantic/test_fields.py b/tests/experimental/pydantic/test_fields.py index 5408cff226..dd83d24847 100644 --- a/tests/experimental/pydantic/test_fields.py +++ b/tests/experimental/pydantic/test_fields.py @@ -1,10 +1,7 @@ -import re -from typing import List -from typing_extensions import Literal +import pytest import pydantic -import pytest -from pydantic import BaseModel, ValidationError, conlist +from typing_extensions import Literal import strawberry from strawberry.type import StrawberryOptional @@ -12,7 +9,7 @@ @pytest.mark.parametrize( - ("pydantic_type", "field_type"), + "pydantic_type, field_type", [ (pydantic.ConstrainedInt, int), (pydantic.PositiveInt, int), @@ -50,7 +47,7 @@ class Type: @pytest.mark.parametrize( - ("pydantic_type", "field_type"), + "pydantic_type, field_type", [(pydantic.NoneStr, str)], ) def test_types_optional(pydantic_type, field_type): @@ -88,23 +85,6 @@ class Type: assert field.type is int -def test_confloat(): - class Model(pydantic.BaseModel): - field: pydantic.confloat(lt=100.5) - - @strawberry.experimental.pydantic.type(Model) - class Type: - field: strawberry.auto - - definition: TypeDefinition = Type._type_definition - assert definition.name == "Type" - - [field] = definition.fields - - assert field.python_name == "field" - assert field.type is float - - def test_constr(): class Model(pydantic.BaseModel): field: pydantic.constr(max_length=100) @@ -122,45 +102,6 @@ class Type: assert field.type is str -def test_constrained_list(): - class User(BaseModel): - friends: conlist(str, min_items=1) - - @strawberry.experimental.pydantic.type(model=User, all_fields=True) - class UserType: - ... - - assert UserType._type_definition.fields[0].name == "friends" - assert UserType._type_definition.fields[0].type_annotation.annotation == List[str] - - data = UserType(friends=[]) - - with pytest.raises( - ValidationError, - match=re.escape( - "ensure this value has at least 1 items " - "(type=value_error.list.min_items; limit_value=1)", - ), - ): - # validation errors should happen when converting to pydantic - data.to_pydantic() - - -def test_constrained_list_nested(): - class User(BaseModel): - friends: conlist(conlist(int, min_items=1), min_items=1) - - @strawberry.experimental.pydantic.type(model=User, all_fields=True) - class UserType: - ... - - assert UserType._type_definition.fields[0].name == "friends" - assert ( - UserType._type_definition.fields[0].type_annotation.annotation - == List[List[int]] - ) - - @pytest.mark.parametrize( "pydantic_type", [ @@ -172,6 +113,8 @@ class UserType: pydantic.Json, pydantic.PaymentCardNumber, pydantic.ByteSize, + # pydantic.ConstrainedList, + # pydantic.ConstrainedSet, # pydantic.JsonWrapper, ], ) diff --git a/tests/fastapi/schema.py b/tests/fastapi/schema.py index 7b375bc185..b11a18b745 100644 --- a/tests/fastapi/schema.py +++ b/tests/fastapi/schema.py @@ -43,11 +43,6 @@ class Query: def hello(self, name: typing.Optional[str] = None) -> str: return f"Hello {name or 'world'}" - @strawberry.field - async def async_hello(self, name: str, delay: float = 0) -> str: - await asyncio.sleep(delay) - return f"Hello {name or 'world'}" - @strawberry.field(permission_classes=[AlwaysFailPermission]) def always_fail(self) -> Optional[str]: return "Hey" @@ -56,18 +51,9 @@ def always_fail(self) -> Optional[str]: def root_name(root) -> str: return type(root).__name__ - @strawberry.field - async def exception(self, message: str) -> str: - raise ValueError(message) - return message - @strawberry.type class Mutation: - @strawberry.mutation - async def hello(self) -> str: - return "strawberry" - @strawberry.mutation async def read_text(self, text_file: Upload) -> str: return (await text_file.read()).decode() @@ -123,7 +109,7 @@ async def exception(self, message: str) -> typing.AsyncGenerator[str, None]: raise ValueError(message) # Without this yield, the method is not recognised as an async generator - yield "Hi" + yield "Hi" # noqa @strawberry.subscription async def flavors(self) -> typing.AsyncGenerator[Flavor, None]: @@ -149,9 +135,5 @@ async def debug(self, info) -> typing.AsyncGenerator[DebugInfo, None]: is_connection_init_timeout_task_done=is_connection_init_timeout_task_done, ) - @strawberry.subscription - async def connection_params(self, info: Info) -> typing.AsyncGenerator[str, None]: - yield info.context["connection_params"]["strawberry"] - schema = strawberry.Schema(Query, mutation=Mutation, subscription=Subscription) diff --git a/tests/fastapi/test_async.py b/tests/fastapi/test_async.py index 36beb2672a..3985536c3c 100644 --- a/tests/fastapi/test_async.py +++ b/tests/fastapi/test_async.py @@ -1,6 +1,7 @@ import typing import pytest + from starlette.testclient import TestClient import strawberry diff --git a/tests/fastapi/test_context.py b/tests/fastapi/test_context.py index 681df1ecc6..25205f67fe 100644 --- a/tests/fastapi/test_context.py +++ b/tests/fastapi/test_context.py @@ -1,32 +1,13 @@ -import asyncio -from typing import AsyncGenerator, Dict +from typing import Dict import pytest -from starlette.websockets import WebSocketDisconnect + +from starlette.testclient import TestClient import strawberry from fastapi import Depends, FastAPI -from fastapi.testclient import TestClient from strawberry.exceptions import InvalidCustomContext from strawberry.fastapi import BaseContext, GraphQLRouter -from strawberry.subscriptions import GRAPHQL_TRANSPORT_WS_PROTOCOL, GRAPHQL_WS_PROTOCOL -from strawberry.subscriptions.protocols.graphql_transport_ws.types import ( - CompleteMessage, - ConnectionAckMessage, - ConnectionInitMessage, - NextMessage, - SubscribeMessage, - SubscribeMessagePayload, -) -from strawberry.subscriptions.protocols.graphql_ws import ( - GQL_COMPLETE, - GQL_CONNECTION_ACK, - GQL_CONNECTION_INIT, - GQL_CONNECTION_TERMINATE, - GQL_DATA, - GQL_START, - GQL_STOP, -) from strawberry.types import Info @@ -37,14 +18,13 @@ def test_base_context(): assert base_context.response is None -def test_with_explicit_class_context_getter(): +def test_with_class_context_getter(): @strawberry.type class Query: @strawberry.field def abc(self, info: Info) -> str: assert info.context.request is not None - assert info.context.strawberry == "explicitly rocks" - assert info.context.connection_params is None + assert info.context.strawberry == "rocks" return "abc" class CustomContext(BaseContext): @@ -52,7 +32,7 @@ def __init__(self, rocks: str): self.strawberry = rocks def custom_context_dependency() -> CustomContext: - return CustomContext(rocks="explicitly rocks") + return CustomContext(rocks="rocks") def get_context(custom_context: CustomContext = Depends(custom_context_dependency)): return custom_context @@ -69,43 +49,12 @@ def get_context(custom_context: CustomContext = Depends(custom_context_dependenc assert response.json() == {"data": {"abc": "abc"}} -def test_with_implicit_class_context_getter(): - @strawberry.type - class Query: - @strawberry.field - def abc(self, info: Info) -> str: - assert info.context.request is not None - assert info.context.strawberry == "implicitly rocks" - assert info.context.connection_params is None - return "abc" - - class CustomContext(BaseContext): - def __init__(self, rocks: str = "implicitly rocks"): - super().__init__() - self.strawberry = rocks - - def get_context(custom_context: CustomContext = Depends()): - return custom_context - - app = FastAPI() - schema = strawberry.Schema(query=Query) - graphql_app = GraphQLRouter(schema=schema, context_getter=get_context) - app.include_router(graphql_app, prefix="/graphql") - - test_client = TestClient(app) - response = test_client.post("/graphql", json={"query": "{ abc }"}) - - assert response.status_code == 200 - assert response.json() == {"data": {"abc": "abc"}} - - def test_with_dict_context_getter(): @strawberry.type class Query: @strawberry.field def abc(self, info: Info) -> str: assert info.context.get("request") is not None - assert "connection_params" not in info.context.keys() assert info.context.get("strawberry") == "rocks" return "abc" @@ -177,131 +126,3 @@ def get_context(value: str = Depends(custom_context_dependency)) -> str: ), ): test_client.post("/graphql", json={"query": "{ abc }"}) - - -def test_class_context_injects_connection_params_over_transport_ws(): - @strawberry.type - class Query: - x: str = "hi" - - @strawberry.type - class Subscription: - @strawberry.subscription - async def connection_params( - self, info: Info, delay: float = 0 - ) -> AsyncGenerator[str, None]: - assert info.context.request is not None - await asyncio.sleep(delay) - yield info.context.connection_params["strawberry"] - - class Context(BaseContext): - strawberry: str - - def __init__(self): - self.strawberry = "rocks" - - def get_context(context: Context = Depends()) -> Context: - return context - - app = FastAPI() - schema = strawberry.Schema(query=Query, subscription=Subscription) - graphql_app = GraphQLRouter(schema=schema, context_getter=get_context) - app.include_router(graphql_app, prefix="/graphql") - test_client = TestClient(app) - - with test_client.websocket_connect( - "/graphql", [GRAPHQL_TRANSPORT_WS_PROTOCOL] - ) as ws: - ws.send_json(ConnectionInitMessage(payload={"strawberry": "rocks"}).as_dict()) - - response = ws.receive_json() - assert response == ConnectionAckMessage().as_dict() - - ws.send_json( - SubscribeMessage( - id="sub1", - payload=SubscribeMessagePayload( - query="subscription { connectionParams }" - ), - ).as_dict() - ) - - response = ws.receive_json() - assert ( - response - == NextMessage( - id="sub1", payload={"data": {"connectionParams": "rocks"}} - ).as_dict() - ) - - ws.send_json(CompleteMessage(id="sub1").as_dict()) - - ws.close() - - -def test_class_context_injects_connection_params_over_ws(): - @strawberry.type - class Query: - x: str = "hi" - - @strawberry.type - class Subscription: - @strawberry.subscription - async def connection_params( - self, info: Info, delay: float = 0 - ) -> AsyncGenerator[str, None]: - assert info.context.request is not None - await asyncio.sleep(delay) - yield info.context.connection_params["strawberry"] - - class Context(BaseContext): - strawberry: str - - def __init__(self): - self.strawberry = "rocks" - - def get_context(context: Context = Depends()) -> Context: - return context - - app = FastAPI() - schema = strawberry.Schema(query=Query, subscription=Subscription) - graphql_app = GraphQLRouter(schema=schema, context_getter=get_context) - app.include_router(graphql_app, prefix="/graphql") - test_client = TestClient(app) - - with test_client.websocket_connect("/graphql", [GRAPHQL_WS_PROTOCOL]) as ws: - ws.send_json( - { - "type": GQL_CONNECTION_INIT, - "id": "demo", - "payload": {"strawberry": "rocks"}, - } - ) - ws.send_json( - { - "type": GQL_START, - "id": "demo", - "payload": { - "query": "subscription { connectionParams }", - }, - } - ) - - response = ws.receive_json() - assert response["type"] == GQL_CONNECTION_ACK - - response = ws.receive_json() - assert response["type"] == GQL_DATA - assert response["id"] == "demo" - assert response["payload"]["data"] == {"connectionParams": "rocks"} - - ws.send_json({"type": GQL_STOP, "id": "demo"}) - response = ws.receive_json() - assert response["type"] == GQL_COMPLETE - assert response["id"] == "demo" - - ws.send_json({"type": GQL_CONNECTION_TERMINATE}) - - # make sure the websocket is disconnected now - with pytest.raises(WebSocketDisconnect): - ws.receive_json() diff --git a/tests/fastapi/test_graphql_transport_ws.py b/tests/fastapi/test_graphql_transport_ws.py index 188a3f65a8..56c811b9c1 100644 --- a/tests/fastapi/test_graphql_transport_ws.py +++ b/tests/fastapi/test_graphql_transport_ws.py @@ -244,56 +244,6 @@ def test_duplicated_operation_ids(test_client): assert data["code"] == 4409 -def test_reused_operation_ids(test_client): - """ - Test that an operation id can be re-used after it has been - previously used for a completed operation - """ - with test_client.websocket_connect( - "/graphql", [GRAPHQL_TRANSPORT_WS_PROTOCOL] - ) as ws: - ws.send_json(ConnectionInitMessage().as_dict()) - - response = ws.receive_json() - assert response == ConnectionAckMessage().as_dict() - - # Use sub1 as an id for an operation - ws.send_json( - SubscribeMessage( - id="sub1", - payload=SubscribeMessagePayload( - query='subscription { echo(message: "Hi") }' - ), - ).as_dict() - ) - - response = ws.receive_json() - assert ( - response - == NextMessage(id="sub1", payload={"data": {"echo": "Hi"}}).as_dict() - ) - - response = ws.receive_json() - assert response == CompleteMessage(id="sub1").as_dict() - - # operation is now complete. Create a new operation using - # the same ID - ws.send_json( - SubscribeMessage( - id="sub1", - payload=SubscribeMessagePayload( - query='subscription { echo(message: "Hi") }' - ), - ).as_dict() - ) - - response = ws.receive_json() - assert ( - response - == NextMessage(id="sub1", payload={"data": {"echo": "Hi"}}).as_dict() - ) - - def test_simple_subscription(test_client): with test_client.websocket_connect( "/graphql", [GRAPHQL_TRANSPORT_WS_PROTOCOL] @@ -339,9 +289,18 @@ def test_subscription_syntax_error(test_client): ).as_dict() ) - data = ws.receive() - assert data["type"] == "websocket.close" - assert data["code"] == 4400 + response = ws.receive_json() + assert response["type"] == ErrorMessage.type + assert response["id"] == "sub1" + assert len(response["payload"]) == 1 + assert response["payload"][0]["path"] is None + assert response["payload"][0]["locations"] == [{"line": 1, "column": 31}] + assert ( + response["payload"][0]["message"] + == "Syntax Error: Expected Name, found ." + ) + + ws.close() def test_subscription_field_errors(test_client): @@ -366,7 +325,7 @@ def test_subscription_field_errors(test_client): assert response["type"] == ErrorMessage.type assert response["id"] == "sub1" assert len(response["payload"]) == 1 - assert response["payload"][0].get("path") is None + assert response["payload"][0]["path"] is None assert response["payload"][0]["locations"] == [{"line": 1, "column": 16}] assert ( response["payload"][0]["message"] @@ -489,395 +448,8 @@ def test_subscription_exceptions(test_client): assert response["type"] == ErrorMessage.type assert response["id"] == "sub1" assert len(response["payload"]) == 1 - assert response["payload"][0].get("path") is None - assert response["payload"][0].get("locations") is None + assert response["payload"][0]["path"] is None + assert response["payload"][0]["locations"] is None assert response["payload"][0]["message"] == "TEST EXC" ws.close() - - -def test_single_result_query_operation(test_client): - with test_client.websocket_connect( - "/graphql", [GRAPHQL_TRANSPORT_WS_PROTOCOL] - ) as ws: - ws.send_json(ConnectionInitMessage().as_dict()) - - response = ws.receive_json() - assert response == ConnectionAckMessage().as_dict() - - ws.send_json( - SubscribeMessage( - id="sub1", - payload=SubscribeMessagePayload(query="query { hello }"), - ).as_dict() - ) - - response = ws.receive_json() - assert ( - response - == NextMessage( - id="sub1", payload={"data": {"hello": "Hello world"}} - ).as_dict() - ) - - response = ws.receive_json() - assert response == CompleteMessage(id="sub1").as_dict() - - -def test_single_result_query_operation_async(test_client): - """ - Test a single result query operation on an - `async` method in the schema, including an artificial - async delay - """ - with test_client.websocket_connect( - "/graphql", [GRAPHQL_TRANSPORT_WS_PROTOCOL] - ) as ws: - ws.send_json(ConnectionInitMessage().as_dict()) - - response = ws.receive_json() - assert response == ConnectionAckMessage().as_dict() - - ws.send_json( - SubscribeMessage( - id="sub1", - payload=SubscribeMessagePayload( - query='query { asyncHello(name: "Dolly", delay:0.01)}' - ), - ).as_dict() - ) - - response = ws.receive_json() - assert ( - response - == NextMessage( - id="sub1", payload={"data": {"asyncHello": "Hello Dolly"}} - ).as_dict() - ) - - response = ws.receive_json() - assert response == CompleteMessage(id="sub1").as_dict() - - -def test_single_result_query_operation_overlapped(test_client): - """ - Test that two single result queries can be in flight at the same time, - just like regular queries. Start two queries with separate ids. The - first query has a delay, so we expect the response to the second - query to be delivered first. - """ - with test_client.websocket_connect( - "/graphql", [GRAPHQL_TRANSPORT_WS_PROTOCOL] - ) as ws: - ws.send_json(ConnectionInitMessage().as_dict()) - - response = ws.receive_json() - assert response == ConnectionAckMessage().as_dict() - - # first query - ws.send_json( - SubscribeMessage( - id="sub1", - payload=SubscribeMessagePayload( - query='query { asyncHello(name: "Dolly", delay:1)}' - ), - ).as_dict() - ) - # second query - ws.send_json( - SubscribeMessage( - id="sub2", - payload=SubscribeMessagePayload( - query='query { asyncHello(name: "Dolly", delay:0)}' - ), - ).as_dict() - ) - - # we expect the response to the second query to arrive first - response = ws.receive_json() - assert ( - response - == NextMessage( - id="sub2", payload={"data": {"asyncHello": "Hello Dolly"}} - ).as_dict() - ) - response = ws.receive_json() - assert response == CompleteMessage(id="sub2").as_dict() - - -def test_single_result_mutation_operation(test_client): - with test_client.websocket_connect( - "/graphql", [GRAPHQL_TRANSPORT_WS_PROTOCOL] - ) as ws: - ws.send_json(ConnectionInitMessage().as_dict()) - - response = ws.receive_json() - assert response == ConnectionAckMessage().as_dict() - - ws.send_json( - SubscribeMessage( - id="sub1", - payload=SubscribeMessagePayload(query="mutation { hello }"), - ).as_dict() - ) - - response = ws.receive_json() - assert ( - response - == NextMessage( - id="sub1", payload={"data": {"hello": "strawberry"}} - ).as_dict() - ) - - response = ws.receive_json() - assert response == CompleteMessage(id="sub1").as_dict() - - -def test_single_result_operation_selection(test_client): - with test_client.websocket_connect( - "/graphql", [GRAPHQL_TRANSPORT_WS_PROTOCOL] - ) as ws: - ws.send_json(ConnectionInitMessage().as_dict()) - - response = ws.receive_json() - assert response == ConnectionAckMessage().as_dict() - - query = """ - query Query1 { - hello - } - query Query2 { - hello(name: "Strawberry") - } - """ - - ws.send_json( - SubscribeMessage( - id="sub1", - payload=SubscribeMessagePayload(query=query, operationName="Query2"), - ).as_dict() - ) - - response = ws.receive_json() - assert ( - response - == NextMessage( - id="sub1", payload={"data": {"hello": "Hello Strawberry"}} - ).as_dict() - ) - - response = ws.receive_json() - assert response == CompleteMessage(id="sub1").as_dict() - - -def test_single_result_invalid_operation_selection(test_client): - with test_client.websocket_connect( - "/graphql", [GRAPHQL_TRANSPORT_WS_PROTOCOL] - ) as ws: - ws.send_json(ConnectionInitMessage().as_dict()) - - response = ws.receive_json() - assert response == ConnectionAckMessage().as_dict() - - query = """ - query Query1 { - hello - } - """ - - ws.send_json( - SubscribeMessage( - id="sub1", - payload=SubscribeMessagePayload(query=query, operationName="Query2"), - ).as_dict() - ) - - data = ws.receive() - assert data["type"] == "websocket.close" - assert data["code"] == 4400 - - -def test_single_result_operation_error(test_client): - with test_client.websocket_connect( - "/graphql", [GRAPHQL_TRANSPORT_WS_PROTOCOL] - ) as ws: - ws.send_json(ConnectionInitMessage().as_dict()) - - response = ws.receive_json() - assert response == ConnectionAckMessage().as_dict() - - ws.send_json( - SubscribeMessage( - id="sub1", - payload=SubscribeMessagePayload( - query="query { alwaysFail }", - ), - ).as_dict() - ) - - response = ws.receive_json() - assert response["type"] == ErrorMessage.type - assert response["id"] == "sub1" - assert len(response["payload"]) == 1 - assert response["payload"][0]["message"] == "You are not authorized" - - -def test_single_result_operation_exception(test_client): - """ - Test that single-result-operations which raise exceptions - behave in the same way as streaming operations - """ - with test_client.websocket_connect( - "/graphql", [GRAPHQL_TRANSPORT_WS_PROTOCOL] - ) as ws: - ws.send_json(ConnectionInitMessage().as_dict()) - - response = ws.receive_json() - assert response == ConnectionAckMessage().as_dict() - - ws.send_json( - SubscribeMessage( - id="sub1", - payload=SubscribeMessagePayload( - query='query { exception(message: "bummer") }', - ), - ).as_dict() - ) - - response = ws.receive_json() - assert response["type"] == ErrorMessage.type - assert response["id"] == "sub1" - assert len(response["payload"]) == 1 - assert response["payload"][0].get("path") == ["exception"] - assert response["payload"][0]["message"] == "bummer" - - -def test_single_result_duplicate_ids_sub(test_client): - """ - Test that single-result-operations and streaming operations - share the same ID namespace. Start a regular subscription, - then issue a single-result operation with same ID and expect an - error due to already existing ID - """ - with test_client.websocket_connect( - "/graphql", [GRAPHQL_TRANSPORT_WS_PROTOCOL] - ) as ws: - ws.send_json(ConnectionInitMessage().as_dict()) - - response = ws.receive_json() - assert response == ConnectionAckMessage().as_dict() - - # regular subscription - ws.send_json( - SubscribeMessage( - id="sub1", - payload=SubscribeMessagePayload( - query='subscription { echo(message: "Hi", delay: 5) }' - ), - ).as_dict() - ) - # single result subscription with duplicate id - ws.send_json( - SubscribeMessage( - id="sub1", - payload=SubscribeMessagePayload( - query="query { hello }", - ), - ).as_dict() - ) - - data = ws.receive() - assert data["type"] == "websocket.close" - assert data["code"] == 4409 - - -def test_single_result_duplicate_ids_query(test_client): - """ - Test that single-result-operations don't allow duplicate - IDs for two asynchronous queries. Issue one async query - with delay, then another with same id. Expect error. - """ - with test_client.websocket_connect( - "/graphql", [GRAPHQL_TRANSPORT_WS_PROTOCOL] - ) as ws: - ws.send_json(ConnectionInitMessage().as_dict()) - - response = ws.receive_json() - assert response == ConnectionAckMessage().as_dict() - - # single result subscription 1 - ws.send_json( - SubscribeMessage( - id="sub1", - payload=SubscribeMessagePayload( - query='query { asyncHello(name: "Hi", delay: 5) }' - ), - ).as_dict() - ) - # single result subscription with duplicate id - ws.send_json( - SubscribeMessage( - id="sub1", - payload=SubscribeMessagePayload( - query="query { hello }", - ), - ).as_dict() - ) - - # We expect the remote to close the socket due to duplicate ID in use - data = ws.receive() - assert data["type"] == "websocket.close" - assert data["code"] == 4409 - - -def test_injects_connection_params(test_client): - with test_client.websocket_connect( - "/graphql", [GRAPHQL_TRANSPORT_WS_PROTOCOL] - ) as ws: - ws.send_json(ConnectionInitMessage(payload={"strawberry": "rocks"}).as_dict()) - - response = ws.receive_json() - assert response == ConnectionAckMessage().as_dict() - - ws.send_json( - SubscribeMessage( - id="sub1", - payload=SubscribeMessagePayload( - query="subscription { connectionParams }" - ), - ).as_dict() - ) - - response = ws.receive_json() - assert ( - response - == NextMessage( - id="sub1", payload={"data": {"connectionParams": "rocks"}} - ).as_dict() - ) - - ws.send_json(CompleteMessage(id="sub1").as_dict()) - - ws.close() - - -def test_rejects_connection_params_not_dict(test_client): - with test_client.websocket_connect( - "/graphql", [GRAPHQL_TRANSPORT_WS_PROTOCOL] - ) as ws: - ws.send_json(ConnectionInitMessage(payload="gonna fail").as_dict()) - - data = ws.receive() - assert data["type"] == "websocket.close" - assert data["code"] == 4400 - - -def test_rejects_connection_params_not_unset(test_client): - with test_client.websocket_connect( - "/graphql", [GRAPHQL_TRANSPORT_WS_PROTOCOL] - ) as ws: - ws.send_json(ConnectionInitMessage(payload=None).as_dict()) - - data = ws.receive() - assert data["type"] == "websocket.close" - assert data["code"] == 4400 diff --git a/tests/fastapi/test_graphql_ws.py b/tests/fastapi/test_graphql_ws.py index cff994c0cf..94827ff259 100644 --- a/tests/fastapi/test_graphql_ws.py +++ b/tests/fastapi/test_graphql_ws.py @@ -1,11 +1,11 @@ import pytest + from starlette.websockets import WebSocketDisconnect from strawberry.subscriptions import GRAPHQL_WS_PROTOCOL from strawberry.subscriptions.protocols.graphql_ws import ( GQL_COMPLETE, GQL_CONNECTION_ACK, - GQL_CONNECTION_ERROR, GQL_CONNECTION_INIT, GQL_CONNECTION_KEEP_ALIVE, GQL_CONNECTION_TERMINATE, @@ -236,7 +236,9 @@ def test_subscription_exceptions(test_client): assert response["type"] == GQL_DATA assert response["id"] == "demo" assert response["payload"]["data"] is None - assert response["payload"]["errors"] == [{"message": "TEST EXC"}] + assert response["payload"]["errors"] == [ + {"locations": None, "message": "TEST EXC", "path": None} + ] ws.send_json({"type": GQL_STOP, "id": "demo"}) response = ws.receive_json() @@ -269,6 +271,7 @@ def test_subscription_field_error(test_client): assert response["id"] == "invalid-field" assert response["payload"] == { "locations": [{"line": 1, "column": 16}], + "path": None, "message": ( "The subscription field 'notASubscriptionField' is not defined." ), @@ -300,6 +303,7 @@ def test_subscription_syntax_error(test_client): assert response["id"] == "syntax-error" assert response["payload"] == { "locations": [{"line": 1, "column": 24}], + "path": None, "message": "Syntax Error: Expected Name, found .", } @@ -517,60 +521,3 @@ def test_task_cancellation_separation(test_client): response = ws1.receive_json() assert response["type"] == GQL_COMPLETE assert response["id"] == "debug1" - - -def test_injects_connection_params(test_client): - with test_client.websocket_connect("/graphql", [GRAPHQL_WS_PROTOCOL]) as ws: - ws.send_json( - { - "type": GQL_CONNECTION_INIT, - "id": "demo", - "payload": {"strawberry": "rocks"}, - } - ) - ws.send_json( - { - "type": GQL_START, - "id": "demo", - "payload": { - "query": "subscription { connectionParams }", - }, - } - ) - - response = ws.receive_json() - assert response["type"] == GQL_CONNECTION_ACK - - response = ws.receive_json() - assert response["type"] == GQL_DATA - assert response["id"] == "demo" - assert response["payload"]["data"] == {"connectionParams": "rocks"} - - ws.send_json({"type": GQL_STOP, "id": "demo"}) - response = ws.receive_json() - assert response["type"] == GQL_COMPLETE - assert response["id"] == "demo" - - ws.send_json({"type": GQL_CONNECTION_TERMINATE}) - - # make sure the websocket is disconnected now - with pytest.raises(WebSocketDisconnect): - ws.receive_json() - - -def test_rejects_connection_params(test_client): - with test_client.websocket_connect("/graphql", [GRAPHQL_WS_PROTOCOL]) as ws: - ws.send_json( - { - "type": GQL_CONNECTION_INIT, - "id": "demo", - "payload": "gonna fail", - } - ) - - response = ws.receive_json() - assert response["type"] == GQL_CONNECTION_ERROR - - # make sure the websocket is disconnected now - with pytest.raises(WebSocketDisconnect): - ws.receive_json() diff --git a/tests/fastapi/test_http.py b/tests/fastapi/test_http.py new file mode 100644 index 0000000000..53aa8d4960 --- /dev/null +++ b/tests/fastapi/test_http.py @@ -0,0 +1,22 @@ +import pytest + +from fastapi import status + + +def test_returns_error_when_missing_query(test_client): + response = test_client.post("/graphql", json={}) + + assert response.status_code == status.HTTP_400_BAD_REQUEST + + +def test_returns_error_when_not_sending_wrong_content_type(test_client): + response = test_client.post("/graphql", data="Example") + + assert response.status_code == status.HTTP_415_UNSUPPORTED_MEDIA_TYPE + + +@pytest.mark.parametrize("method", ("PUT", "DELETE")) +def test_returns_error_when_method_is_not_allowed(method, test_client): + response = test_client.request(method, "/graphql", json={}) + + assert response.status_code == status.HTTP_405_METHOD_NOT_ALLOWED diff --git a/tests/fastapi/test_query.py b/tests/fastapi/test_query.py new file mode 100644 index 0000000000..3e22d06ee2 --- /dev/null +++ b/tests/fastapi/test_query.py @@ -0,0 +1,135 @@ +import strawberry +from fastapi import FastAPI +from fastapi.testclient import TestClient +from strawberry.fastapi import GraphQLRouter +from strawberry.types import ExecutionResult, Info +from tests.fastapi.app import create_app + + +def test_simple_query(test_client): + response = test_client.post("/graphql", json={"query": "{ hello }"}) + + assert response.json() == {"data": {"hello": "Hello world"}} + + +def test_fails_when_request_body_has_invalid_json(test_client): + response = test_client.post( + "/graphql", + data='{"qeury": "{__typena"', + headers={"content-type": "application/json"}, + ) + assert response.status_code == 400 + + +def test_returns_errors(test_client): + response = test_client.post("/graphql", json={"query": "{ donut }"}) + + assert response.json() == { + "data": None, + "errors": [ + { + "locations": [{"column": 3, "line": 1}], + "message": "Cannot query field 'donut' on type 'Query'.", + "path": None, + } + ], + } + + +def test_can_pass_variables(test_client): + response = test_client.post( + "/graphql", + json={ + "query": "query Hello($name: String!) { hello(name: $name) }", + "variables": {"name": "James"}, + }, + ) + + assert response.json() == {"data": {"hello": "Hello James"}} + + +def test_returns_errors_and_data(test_client): + response = test_client.post("/graphql", json={"query": "{ hello, alwaysFail }"}) + + assert response.status_code == 200 + assert response.json() == { + "data": {"hello": "Hello world", "alwaysFail": None}, + "errors": [ + { + "locations": [{"column": 10, "line": 1}], + "message": "You are not authorized", + "path": ["alwaysFail"], + } + ], + } + + +def test_root_value(test_client): + response = test_client.post("/graphql", json={"query": "{ rootName }"}) + + assert response.json() == {"data": {"rootName": "Request"}} + + +def test_can_set_background_task(): + task_complete = False + + def task(): + nonlocal task_complete + task_complete = True + + @strawberry.type + class Query: + @strawberry.field + def something(self, info: Info) -> str: + tasks = info.context["background_tasks"] + tasks.add_task(task) + return "foo" + + schema = strawberry.Schema(query=Query) + app = create_app(schema=schema) + + test_client = TestClient(app) + response = test_client.post("/graphql", json={"query": "{ something }"}) + + assert response.json() == {"data": {"something": "foo"}} + assert task_complete + + +def test_custom_context(): + @strawberry.type + class Query: + @strawberry.field + def custom_context_value(self, info: Info) -> str: + return info.context["custom_value"] + + schema = strawberry.Schema(query=Query) + app = create_app(schema=schema) + + test_client = TestClient(app) + response = test_client.post("/graphql", json={"query": "{ customContextValue }"}) + + assert response.status_code == 200 + assert response.json() == {"data": {"customContextValue": "Hi!"}} + + +def test_custom_process_result(): + class CustomGraphQL(GraphQLRouter): + async def process_result(self, request, result: ExecutionResult): + return {} + + @strawberry.type + class Query: + @strawberry.field + def abc(self) -> str: + return "ABC" + + app = FastAPI() + schema = strawberry.Schema(query=Query) + graphql_app = CustomGraphQL(schema) + app.include_router(graphql_app, prefix="/graphql") + + test_client = TestClient(app) + response = test_client.post("/graphql", json={"query": "{ abc }"}) + + assert response.status_code == 200 + assert response.json() == {} diff --git a/tests/fastapi/test_response_headers.py b/tests/fastapi/test_response_headers.py index c169d95140..6d1c702354 100644 --- a/tests/fastapi/test_response_headers.py +++ b/tests/fastapi/test_response_headers.py @@ -6,7 +6,6 @@ from strawberry.types import Info -# TODO: move this to common tests def test_set_response_headers(): @strawberry.type class Query: diff --git a/tests/fastapi/test_response_status.py b/tests/fastapi/test_response_status.py index 2b28d5c6cf..173b9a840f 100644 --- a/tests/fastapi/test_response_status.py +++ b/tests/fastapi/test_response_status.py @@ -7,7 +7,6 @@ from strawberry.types import Info -# TODO: move this to common tests def test_set_custom_http_response_status(): @strawberry.type class Query: diff --git a/tests/fastapi/test_router.py b/tests/fastapi/test_router.py index 787eb0b915..78e1d0856f 100644 --- a/tests/fastapi/test_router.py +++ b/tests/fastapi/test_router.py @@ -1,4 +1,5 @@ import pytest + from starlette.testclient import TestClient import strawberry diff --git a/tests/fastapi/test_upload.py b/tests/fastapi/test_upload.py new file mode 100644 index 0000000000..396971aaf7 --- /dev/null +++ b/tests/fastapi/test_upload.py @@ -0,0 +1,85 @@ +import json +from io import BytesIO + +from fastapi import status + + +def test_single_file_upload(test_client): + f = BytesIO(b"strawberry") + + query = """mutation($textFile: Upload!) { + readText(textFile: $textFile) + }""" + + response = test_client.post( + "/graphql", + data={ + "operations": json.dumps({"query": query, "variables": {"textFile": None}}), + "map": json.dumps({"textFile": ["variables.textFile"]}), + }, + files={"textFile": f}, + ) + + assert response.status_code == status.HTTP_200_OK + data = response.json() + + assert not data.get("errors") + assert data["data"]["readText"] == "strawberry" + + +def test_file_list_upload(test_client): + query = "mutation($files: [Upload!]!) { readFiles(files: $files) }" + operations = json.dumps({"query": query, "variables": {"files": [None, None]}}) + file_map = json.dumps( + {"file1": ["variables.files.0"], "file2": ["variables.files.1"]} + ) + + file1 = BytesIO(b"strawberry1") + file2 = BytesIO(b"strawberry2") + + response = test_client.post( + "/graphql", + data={ + "operations": operations, + "map": file_map, + }, + files={"file1": file1, "file2": file2}, + ) + + assert response.status_code == status.HTTP_200_OK + data = response.json() + + assert not data.get("errors") + assert len(data["data"]["readFiles"]) == 2 + assert data["data"]["readFiles"][0] == "strawberry1" + assert data["data"]["readFiles"][1] == "strawberry2" + + +def test_nested_file_list(test_client): + query = "mutation($folder: FolderInput!) { readFolder(folder: $folder) }" + operations = json.dumps( + {"query": query, "variables": {"folder": {"files": [None, None]}}} + ) + file_map = json.dumps( + {"file1": ["variables.folder.files.0"], "file2": ["variables.folder.files.1"]} + ) + + file1 = BytesIO(b"strawberry1") + file2 = BytesIO(b"strawberry2") + + response = test_client.post( + "/graphql", + data={ + "operations": operations, + "map": file_map, + }, + files={"file1": file1, "file2": file2}, + ) + + assert response.status_code == status.HTTP_200_OK + data = response.json() + + assert not data.get("errors") + assert len(data["data"]["readFolder"]) == 2 + assert data["data"]["readFolder"][0] == "strawberry1" + assert data["data"]["readFolder"][1] == "strawberry2" diff --git a/tests/starlite/test_websockets.py b/tests/fastapi/test_view.py similarity index 79% rename from tests/starlite/test_websockets.py rename to tests/fastapi/test_view.py index b9326a4fff..d8aeb9a753 100644 --- a/tests/starlite/test_websockets.py +++ b/tests/fastapi/test_view.py @@ -1,9 +1,25 @@ import pytest -from starlite.exceptions import WebSocketDisconnect -from starlite.testing import TestClient +from starlette import status +from starlette.testclient import TestClient +from starlette.websockets import WebSocketDisconnect + from strawberry.subscriptions import GRAPHQL_TRANSPORT_WS_PROTOCOL, GRAPHQL_WS_PROTOCOL -from tests.starlite.app import create_app +from tests.fastapi.app import create_app + + +def test_renders_graphiql(test_client): + response = test_client.get("/graphql") + + assert response.status_code == status.HTTP_200_OK + + assert "Strawberry GraphiQL" in response.text + + +def test_renders_graphiql_disabled(test_client_no_graphiql): + response = test_client_no_graphiql.get("/graphql") + + assert response.status_code == status.HTTP_404_NOT_FOUND def test_turning_off_graphql_ws(): diff --git a/tests/fastapi/test_websockets.py b/tests/fastapi/test_websockets.py deleted file mode 100644 index 0d9aabd9fe..0000000000 --- a/tests/fastapi/test_websockets.py +++ /dev/null @@ -1,99 +0,0 @@ -import pytest -from starlette.testclient import TestClient -from starlette.websockets import WebSocketDisconnect - -import strawberry -from fastapi import FastAPI -from strawberry.fastapi.router import GraphQLRouter -from strawberry.subscriptions import GRAPHQL_TRANSPORT_WS_PROTOCOL, GRAPHQL_WS_PROTOCOL -from tests.fastapi.app import create_app - - -def test_turning_off_graphql_ws(): - app = create_app(subscription_protocols=[GRAPHQL_TRANSPORT_WS_PROTOCOL]) - test_client = TestClient(app) - - with pytest.raises(WebSocketDisconnect) as exc: - with test_client.websocket_connect("/graphql", [GRAPHQL_WS_PROTOCOL]): - pass - - assert exc.value.code == 4406 - - -def test_turning_off_graphql_transport_ws(): - app = create_app(subscription_protocols=[GRAPHQL_WS_PROTOCOL]) - test_client = TestClient(app) - - with pytest.raises(WebSocketDisconnect) as exc: - with test_client.websocket_connect("/graphql", [GRAPHQL_TRANSPORT_WS_PROTOCOL]): - pass - - assert exc.value.code == 4406 - - -def test_turning_off_all_ws_protocols(): - app = create_app(subscription_protocols=[]) - test_client = TestClient(app) - - with pytest.raises(WebSocketDisconnect) as exc: - with test_client.websocket_connect("/graphql", [GRAPHQL_TRANSPORT_WS_PROTOCOL]): - pass - - assert exc.value.code == 4406 - - with pytest.raises(WebSocketDisconnect) as exc: - with test_client.websocket_connect("/graphql", [GRAPHQL_WS_PROTOCOL]): - pass - - assert exc.value.code == 4406 - - -def test_unsupported_ws_protocol(): - app = create_app(subscription_protocols=[]) - test_client = TestClient(app) - - with pytest.raises(WebSocketDisconnect) as exc: - with test_client.websocket_connect("/graphql", ["imaginary-protocol"]): - pass - - assert exc.value.code == 4406 - - -def test_clients_can_prefer_protocols(): - app = create_app( - subscription_protocols=[GRAPHQL_WS_PROTOCOL, GRAPHQL_TRANSPORT_WS_PROTOCOL] - ) - test_client = TestClient(app) - - with test_client.websocket_connect( - "/graphql", [GRAPHQL_TRANSPORT_WS_PROTOCOL, GRAPHQL_WS_PROTOCOL] - ) as ws: - assert ws.accepted_subprotocol == GRAPHQL_TRANSPORT_WS_PROTOCOL - - with test_client.websocket_connect( - "/graphql", [GRAPHQL_WS_PROTOCOL, GRAPHQL_TRANSPORT_WS_PROTOCOL] - ) as ws: - assert ws.accepted_subprotocol == GRAPHQL_WS_PROTOCOL - - -def test_with_custom_encode_json(): - @strawberry.type - class Query: - @strawberry.field - def abc(self) -> str: - return "abc" - - class MyRouter(GraphQLRouter): - def encode_json(self, data): - return '"custom"' - - app = FastAPI() - schema = strawberry.Schema(query=Query) - graphql_app = MyRouter(schema=schema) - app.include_router(graphql_app, prefix="/graphql") - - test_client = TestClient(app) - response = test_client.post("/graphql", json={"query": "{ abc }"}) - - assert response.status_code == 200 - assert response.json() == "custom" diff --git a/tests/federation/printer/__init__.py b/tests/federation/printer/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/federation/printer/test_additional_directives.py b/tests/federation/printer/test_additional_directives.py deleted file mode 100644 index 5fedbb734e..0000000000 --- a/tests/federation/printer/test_additional_directives.py +++ /dev/null @@ -1,82 +0,0 @@ -# type: ignore - -import textwrap - -import strawberry -from strawberry.schema.config import StrawberryConfig -from strawberry.schema_directive import Location - - -def test_additional_schema_directives_printed_correctly_object(): - @strawberry.schema_directive(locations=[Location.OBJECT]) - class CacheControl: - max_age: int - - @strawberry.federation.type( - keys=["id"], shareable=True, extend=True, directives=[CacheControl(max_age=42)] - ) - class FederatedType: - id: strawberry.ID - - @strawberry.type - class Query: - federatedType: FederatedType - - expected_type = """ - directive @CacheControl(max_age: Int!) on OBJECT - - extend type FederatedType @CacheControl(max_age: 42) @key(fields: "id") @shareable { - id: ID! - } - - type Query { - federatedType: FederatedType! - } - """ - - schema = strawberry.Schema( - query=Query, config=StrawberryConfig(auto_camel_case=False) - ) - assert schema.as_str() == textwrap.dedent(expected_type).strip() - - -def test_additional_schema_directives_printed_in_order_object(): - @strawberry.schema_directive(locations=[Location.OBJECT]) - class CacheControl0: - max_age: int - - @strawberry.schema_directive(locations=[Location.OBJECT]) - class CacheControl1: - min_age: int - - @strawberry.federation.type( - keys=["id"], - shareable=True, - extend=True, - directives=[CacheControl0(max_age=42), CacheControl1(min_age=42)], - ) - class FederatedType: - id: strawberry.ID - - @strawberry.type - class Query: - federatedType: FederatedType - - expected_type = """ - directive @CacheControl0(max_age: Int!) on OBJECT - - directive @CacheControl1(min_age: Int!) on OBJECT - - extend type FederatedType @CacheControl0(max_age: 42) @CacheControl1(min_age: 42) @key(fields: "id") @shareable { - id: ID! - } - - type Query { - federatedType: FederatedType! - } - """ - - schema = strawberry.Schema( - query=Query, config=StrawberryConfig(auto_camel_case=False) - ) - assert schema.as_str() == textwrap.dedent(expected_type).strip() diff --git a/tests/federation/printer/test_entities.py b/tests/federation/printer/test_entities.py deleted file mode 100644 index af3d85b681..0000000000 --- a/tests/federation/printer/test_entities.py +++ /dev/null @@ -1,135 +0,0 @@ -# type: ignore - -import textwrap -from typing import List - -import strawberry - - -def test_entities_type_when_no_type_has_keys(): - global Review - - @strawberry.federation.type - class User: - username: str - - @strawberry.federation.type(extend=True) - class Product: - upc: str = strawberry.federation.field(external=True) - reviews: List["Review"] - - @strawberry.federation.type - class Review: - body: str - author: User - product: Product - - @strawberry.federation.type - class Query: - @strawberry.field - def top_products(self, first: int) -> List[Product]: - return [] - - schema = strawberry.federation.Schema(query=Query, enable_federation_2=True) - - expected = """ - schema @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@external"]) { - query: Query - } - - extend type Product { - upc: String! @external - reviews: [Review!]! - } - - type Query { - _service: _Service! - topProducts(first: Int!): [Product!]! - } - - type Review { - body: String! - author: User! - product: Product! - } - - type User { - username: String! - } - - scalar _Any - - type _Service { - sdl: String! - } - """ - - assert schema.as_str() == textwrap.dedent(expected).strip() - - del Review - - -def test_entities_type_when_one_type_has_keys(): - global Review - - @strawberry.federation.type - class User: - username: str - - @strawberry.federation.type(keys=["upc"], extend=True) - class Product: - upc: str = strawberry.federation.field(external=True) - reviews: List["Review"] - - @strawberry.federation.type - class Review: - body: str - author: User - product: Product - - @strawberry.federation.type - class Query: - @strawberry.field - def top_products(self, first: int) -> List[Product]: - return [] - - schema = strawberry.federation.Schema(query=Query, enable_federation_2=True) - - expected = """ - schema @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@external", "@key"]) { - query: Query - } - - extend type Product @key(fields: "upc") { - upc: String! @external - reviews: [Review!]! - } - - type Query { - _entities(representations: [_Any!]!): [_Entity]! - _service: _Service! - topProducts(first: Int!): [Product!]! - } - - type Review { - body: String! - author: User! - product: Product! - } - - type User { - username: String! - } - - scalar _Any - - union _Entity = Product - - type _Service { - sdl: String! - } - """ - - assert schema.as_str() == textwrap.dedent(expected).strip() - - del Review diff --git a/tests/federation/printer/test_inaccessible.py b/tests/federation/printer/test_inaccessible.py deleted file mode 100644 index 444b44aa41..0000000000 --- a/tests/federation/printer/test_inaccessible.py +++ /dev/null @@ -1,288 +0,0 @@ -import textwrap -from enum import Enum -from typing import List -from typing_extensions import Annotated - -import strawberry - - -def test_field_inaccessible_printed_correctly(): - @strawberry.federation.interface(inaccessible=True) - class AnInterface: - id: strawberry.ID - - @strawberry.interface - class SomeInterface: - id: strawberry.ID - a_field: str = strawberry.federation.field(inaccessible=True) - - @strawberry.federation.type(keys=["upc"], extend=True) - class Product(SomeInterface): - upc: str = strawberry.federation.field(external=True, inaccessible=True) - - @strawberry.federation.input(inaccessible=True) - class AnInput: - id: strawberry.ID = strawberry.federation.field(inaccessible=True) - - @strawberry.federation.type(inaccessible=True) - class AnInaccessibleType: - id: strawberry.ID - - @strawberry.federation.type - class Query: - @strawberry.field - def top_products( - self, - first: Annotated[int, strawberry.federation.argument(inaccessible=True)], - ) -> List[Product]: - return [] - - schema = strawberry.federation.Schema( - query=Query, - types=[AnInterface, AnInput, AnInaccessibleType], - enable_federation_2=True, - ) - - expected = """ - schema @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@external", "@inaccessible", "@key"]) { - query: Query - } - - type AnInaccessibleType @inaccessible { - id: ID! - } - - input AnInput @inaccessible { - id: ID! @inaccessible - } - - interface AnInterface @inaccessible { - id: ID! - } - - extend type Product implements SomeInterface @key(fields: "upc") { - id: ID! - aField: String! @inaccessible - upc: String! @external @inaccessible - } - - type Query { - _entities(representations: [_Any!]!): [_Entity]! - _service: _Service! - topProducts(first: Int! @inaccessible): [Product!]! - } - - interface SomeInterface { - id: ID! - aField: String! @inaccessible - } - - scalar _Any - - union _Entity = Product - - type _Service { - sdl: String! - } - """ - - assert schema.as_str() == textwrap.dedent(expected).strip() - - -def test_inaccessible_on_mutation(): - @strawberry.type - class Query: - hello: str - - @strawberry.type - class Mutation: - @strawberry.federation.mutation(inaccessible=True) - def hello(self) -> str: - return "Hello" - - schema = strawberry.federation.Schema( - query=Query, - mutation=Mutation, - enable_federation_2=True, - ) - - expected = """ - schema @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@inaccessible"]) { - query: Query - mutation: Mutation - } - - type Mutation { - hello: String! @inaccessible - } - - type Query { - _service: _Service! - hello: String! - } - - scalar _Any - - type _Service { - sdl: String! - } - """ - - assert schema.as_str() == textwrap.dedent(expected).strip() - - -def test_inaccessible_on_scalar(): - SomeScalar = strawberry.federation.scalar(str, name="SomeScalar", inaccessible=True) - - @strawberry.type - class Query: - hello: SomeScalar - - schema = strawberry.federation.Schema( - query=Query, - enable_federation_2=True, - ) - - expected = """ - schema @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@inaccessible"]) { - query: Query - } - - type Query { - _service: _Service! - hello: SomeScalar! - } - - scalar SomeScalar @inaccessible - - scalar _Any - - type _Service { - sdl: String! - } - """ - - assert schema.as_str() == textwrap.dedent(expected).strip() - - -def test_inaccessible_on_enum(): - @strawberry.federation.enum(inaccessible=True) - class SomeEnum(Enum): - A = "A" - - @strawberry.type - class Query: - hello: SomeEnum - - schema = strawberry.federation.Schema( - query=Query, - enable_federation_2=True, - ) - - expected = """ - schema @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@inaccessible"]) { - query: Query - } - - type Query { - _service: _Service! - hello: SomeEnum! - } - - enum SomeEnum @inaccessible { - A - } - - scalar _Any - - type _Service { - sdl: String! - } - """ - - assert schema.as_str() == textwrap.dedent(expected).strip() - - -def test_inaccessible_on_enum_value(): - @strawberry.enum - class SomeEnum(Enum): - A = strawberry.federation.enum_value("A", inaccessible=True) - - @strawberry.type - class Query: - hello: SomeEnum - - schema = strawberry.federation.Schema( - query=Query, - enable_federation_2=True, - ) - - expected = """ - schema @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@inaccessible"]) { - query: Query - } - - type Query { - _service: _Service! - hello: SomeEnum! - } - - enum SomeEnum { - A @inaccessible - } - - scalar _Any - - type _Service { - sdl: String! - } - """ - - assert schema.as_str() == textwrap.dedent(expected).strip() - - -def test_field_tag_printed_correctly_on_union(): - @strawberry.type - class A: - a: str - - @strawberry.type - class B: - b: str - - Union = strawberry.federation.union("Union", (A, B), inaccessible=True) - - @strawberry.federation.type - class Query: - hello: Union - - schema = strawberry.federation.Schema(query=Query, enable_federation_2=True) - - expected = """ - schema @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@inaccessible"]) { - query: Query - } - - type A { - a: String! - } - - type B { - b: String! - } - - type Query { - _service: _Service! - hello: Union! - } - - union Union @inaccessible = A | B - - scalar _Any - - type _Service { - sdl: String! - } - """ - - assert schema.as_str() == textwrap.dedent(expected).strip() diff --git a/tests/federation/printer/test_interface.py b/tests/federation/printer/test_interface.py deleted file mode 100644 index 91ad34636e..0000000000 --- a/tests/federation/printer/test_interface.py +++ /dev/null @@ -1,53 +0,0 @@ -import textwrap -from typing import List - -import strawberry - - -def test_entities_extending_interface(): - @strawberry.interface - class SomeInterface: - id: strawberry.ID - - @strawberry.federation.type(keys=["upc"], extend=True) - class Product(SomeInterface): - upc: str = strawberry.federation.field(external=True) - - @strawberry.federation.type - class Query: - @strawberry.field - def top_products(self, first: int) -> List[Product]: - return [] - - schema = strawberry.federation.Schema(query=Query, enable_federation_2=True) - - expected = """ - schema @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@external", "@key"]) { - query: Query - } - - extend type Product implements SomeInterface @key(fields: "upc") { - id: ID! - upc: String! @external - } - - type Query { - _entities(representations: [_Any!]!): [_Entity]! - _service: _Service! - topProducts(first: Int!): [Product!]! - } - - interface SomeInterface { - id: ID! - } - - scalar _Any - - union _Entity = Product - - type _Service { - sdl: String! - } - """ - - assert schema.as_str() == textwrap.dedent(expected).strip() diff --git a/tests/federation/printer/test_keys.py b/tests/federation/printer/test_keys.py deleted file mode 100644 index 11fd17a1e0..0000000000 --- a/tests/federation/printer/test_keys.py +++ /dev/null @@ -1,135 +0,0 @@ -# type: ignore - -import textwrap -from typing import List - -import strawberry -from strawberry.federation.schema_directives import Key - - -def test_keys_federation_1(): - global Review - - @strawberry.federation.type - class User: - username: str - - @strawberry.federation.type(keys=[Key(fields="upc", resolvable=True)], extend=True) - class Product: - upc: str = strawberry.federation.field(external=True) - reviews: List["Review"] - - @strawberry.federation.type(keys=["body"]) - class Review: - body: str - author: User - product: Product - - @strawberry.federation.type - class Query: - @strawberry.field - def top_products(self, first: int) -> List[Product]: - return [] - - schema = strawberry.federation.Schema(query=Query, enable_federation_2=False) - - expected = """ - extend type Product @key(fields: "upc") { - upc: String! @external - reviews: [Review!]! - } - - type Query { - _entities(representations: [_Any!]!): [_Entity]! - _service: _Service! - topProducts(first: Int!): [Product!]! - } - - type Review @key(fields: "body") { - body: String! - author: User! - product: Product! - } - - type User { - username: String! - } - - scalar _Any - - union _Entity = Product | Review - - type _Service { - sdl: String! - } - """ - - assert schema.as_str() == textwrap.dedent(expected).strip() - - del Review - - -def test_keys_federation_2(): - global Review - - @strawberry.federation.type - class User: - username: str - - @strawberry.federation.type(keys=[Key(fields="upc", resolvable=True)], extend=True) - class Product: - upc: str = strawberry.federation.field(external=True) - reviews: List["Review"] - - @strawberry.federation.type(keys=["body"]) - class Review: - body: str - author: User - product: Product - - @strawberry.federation.type - class Query: - @strawberry.field - def top_products(self, first: int) -> List[Product]: - return [] - - schema = strawberry.federation.Schema(query=Query, enable_federation_2=True) - - expected = """ - schema @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@external", "@key"]) { - query: Query - } - - extend type Product @key(fields: "upc", resolvable: true) { - upc: String! @external - reviews: [Review!]! - } - - type Query { - _entities(representations: [_Any!]!): [_Entity]! - _service: _Service! - topProducts(first: Int!): [Product!]! - } - - type Review @key(fields: "body") { - body: String! - author: User! - product: Product! - } - - type User { - username: String! - } - - scalar _Any - - union _Entity = Product | Review - - type _Service { - sdl: String! - } - """ - - assert schema.as_str() == textwrap.dedent(expected).strip() - - del Review diff --git a/tests/federation/printer/test_link.py b/tests/federation/printer/test_link.py deleted file mode 100644 index a5b99af853..0000000000 --- a/tests/federation/printer/test_link.py +++ /dev/null @@ -1,332 +0,0 @@ -import textwrap - -import strawberry -from strawberry.federation.schema_directives import Link - - -def test_link_directive(): - @strawberry.type - class Query: - hello: str - - schema = strawberry.federation.Schema( - query=Query, - schema_directives=[ - Link( - url="https://specs.apollo.dev/link/v1.0", - ) - ], - ) - - expected = """ - schema @link(url: "https://specs.apollo.dev/link/v1.0") { - query: Query - } - - type Query { - _service: _Service! - hello: String! - } - - scalar _Any - - type _Service { - sdl: String! - } - """ - - assert schema.as_str() == textwrap.dedent(expected).strip() - - -def test_link_directive_imports(): - @strawberry.type - class Query: - hello: str - - schema = strawberry.federation.Schema( - query=Query, - schema_directives=[ - Link( - url="https://specs.apollo.dev/federation/v2.0", - import_=[ - "@key", - "@requires", - "@provides", - "@external", - {"name": "@tag", "as": "@mytag"}, - "@extends", - "@shareable", - "@inaccessible", - "@override", - ], - ) - ], - ) - - expected = """ - schema @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@key", "@requires", "@provides", "@external", {name: "@tag", as: "@mytag"}, "@extends", "@shareable", "@inaccessible", "@override"]) { - query: Query - } - - type Query { - _service: _Service! - hello: String! - } - - scalar _Any - - type _Service { - sdl: String! - } - """ - - assert schema.as_str() == textwrap.dedent(expected).strip() - - -def test_adds_link_directive_automatically(): - @strawberry.federation.type(keys=["id"]) - class User: - id: strawberry.ID - - @strawberry.type - class Query: - user: User - - schema = strawberry.federation.Schema(query=Query, enable_federation_2=True) - - expected = """ - schema @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@key"]) { - query: Query - } - - type Query { - _entities(representations: [_Any!]!): [_Entity]! - _service: _Service! - user: User! - } - - type User @key(fields: "id") { - id: ID! - } - - scalar _Any - - union _Entity = User - - type _Service { - sdl: String! - } - """ - - assert schema.as_str() == textwrap.dedent(expected).strip() - - -def test_adds_link_directive_from_interface(): - @strawberry.federation.interface(keys=["id"]) - class SomeInterface: - id: strawberry.ID - - @strawberry.type - class User: - id: strawberry.ID - - @strawberry.type - class Query: - user: User - - schema = strawberry.federation.Schema( - query=Query, types=[SomeInterface], enable_federation_2=True - ) - - expected = """ - schema @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@key"]) { - query: Query - } - - type Query { - _service: _Service! - user: User! - } - - interface SomeInterface @key(fields: "id") { - id: ID! - } - - type User { - id: ID! - } - - scalar _Any - - type _Service { - sdl: String! - } - """ - - assert schema.as_str() == textwrap.dedent(expected).strip() - - -def test_adds_link_directive_from_input_types(): - @strawberry.federation.input(inaccessible=True) - class SomeInput: - id: strawberry.ID - - @strawberry.type - class User: - id: strawberry.ID - - @strawberry.type - class Query: - user: User - - schema = strawberry.federation.Schema( - query=Query, types=[SomeInput], enable_federation_2=True - ) - - expected = """ - schema @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@inaccessible"]) { - query: Query - } - - type Query { - _service: _Service! - user: User! - } - - input SomeInput @inaccessible { - id: ID! - } - - type User { - id: ID! - } - - scalar _Any - - type _Service { - sdl: String! - } - """ - - assert schema.as_str() == textwrap.dedent(expected).strip() - - -def test_adds_link_directive_automatically_from_field(): - @strawberry.federation.type(keys=["id"]) - class User: - id: strawberry.ID - age: int = strawberry.federation.field(tags=["private"]) - - @strawberry.type - class Query: - user: User - - schema = strawberry.federation.Schema(query=Query, enable_federation_2=True) - - expected = """ - schema @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@key", "@tag"]) { - query: Query - } - - type Query { - _entities(representations: [_Any!]!): [_Entity]! - _service: _Service! - user: User! - } - - type User @key(fields: "id") { - id: ID! - age: Int! @tag(name: "private") - } - - scalar _Any - - union _Entity = User - - type _Service { - sdl: String! - } - """ - - assert schema.as_str() == textwrap.dedent(expected).strip() - - -def test_does_not_add_directive_link_if_federation_two_is_not_enabled(): - @strawberry.federation.type(keys=["id"]) - class User: - id: strawberry.ID - - @strawberry.type - class Query: - user: User - - schema = strawberry.federation.Schema(query=Query, enable_federation_2=False) - - expected = """ - type Query { - _entities(representations: [_Any!]!): [_Entity]! - _service: _Service! - user: User! - } - - type User @key(fields: "id") { - id: ID! - } - - scalar _Any - - union _Entity = User - - type _Service { - sdl: String! - } - """ - - assert schema.as_str() == textwrap.dedent(expected).strip() - - -def test_adds_link_directive_automatically_from_scalar(): - # TODO: Federation scalar - @strawberry.scalar - class X: - pass - - @strawberry.federation.type(keys=["id"]) - class User: - id: strawberry.ID - age: X - - @strawberry.type - class Query: - user: User - - schema = strawberry.federation.Schema(query=Query, enable_federation_2=True) - - expected = """ - schema @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@key"]) { - query: Query - } - - type Query { - _entities(representations: [_Any!]!): [_Entity]! - _service: _Service! - user: User! - } - - type User @key(fields: "id") { - id: ID! - age: X! - } - - scalar X - - scalar _Any - - union _Entity = User - - type _Service { - sdl: String! - } - """ - - assert schema.as_str() == textwrap.dedent(expected).strip() diff --git a/tests/federation/printer/test_override.py b/tests/federation/printer/test_override.py deleted file mode 100644 index 9d8699827c..0000000000 --- a/tests/federation/printer/test_override.py +++ /dev/null @@ -1,55 +0,0 @@ -# type: ignore - -import textwrap -from typing import List - -import strawberry - - -def test_field_override_printed_correctly(): - @strawberry.interface - class SomeInterface: - id: strawberry.ID - - @strawberry.federation.type(keys=["upc"], extend=True) - class Product(SomeInterface): - upc: str = strawberry.federation.field(external=True, override="mySubGraph") - - @strawberry.federation.type - class Query: - @strawberry.field - def top_products(self, first: int) -> List[Product]: - return [] - - schema = strawberry.federation.Schema(query=Query, enable_federation_2=True) - - expected = """ - schema @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@external", "@key", "@override"]) { - query: Query - } - - extend type Product implements SomeInterface @key(fields: "upc") { - id: ID! - upc: String! @external @override(from: "mySubGraph") - } - - type Query { - _entities(representations: [_Any!]!): [_Entity]! - _service: _Service! - topProducts(first: Int!): [Product!]! - } - - interface SomeInterface { - id: ID! - } - - scalar _Any - - union _Entity = Product - - type _Service { - sdl: String! - } - """ - - assert schema.as_str() == textwrap.dedent(expected).strip() diff --git a/tests/federation/printer/test_provides.py b/tests/federation/printer/test_provides.py deleted file mode 100644 index c182362814..0000000000 --- a/tests/federation/printer/test_provides.py +++ /dev/null @@ -1,151 +0,0 @@ -# type: ignore - -import textwrap -from typing import List - -import strawberry -from strawberry.schema.config import StrawberryConfig - - -def test_field_provides_are_printed_correctly_camel_case_on(): - global Review - - @strawberry.federation.type - class User: - username: str - - @strawberry.federation.type(keys=["upc"], extend=True) - class Product: - upc: str = strawberry.federation.field(external=True) - the_name: str = strawberry.federation.field(external=True) - reviews: List["Review"] - - @strawberry.federation.type - class Review: - body: str - author: User - product: Product = strawberry.federation.field(provides=["name"]) - - @strawberry.federation.type - class Query: - @strawberry.field - def top_products(self, first: int) -> List[Product]: - return [] - - schema = strawberry.federation.Schema( - query=Query, - config=StrawberryConfig(auto_camel_case=True), - enable_federation_2=True, - ) - - expected = """ - schema @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@external", "@key", "@provides"]) { - query: Query - } - - extend type Product @key(fields: "upc") { - upc: String! @external - theName: String! @external - reviews: [Review!]! - } - - type Query { - _entities(representations: [_Any!]!): [_Entity]! - _service: _Service! - topProducts(first: Int!): [Product!]! - } - - type Review { - body: String! - author: User! - product: Product! @provides(fields: "name") - } - - type User { - username: String! - } - - scalar _Any - - union _Entity = Product - - type _Service { - sdl: String! - } - """ - - assert schema.as_str() == textwrap.dedent(expected).strip() - - del Review - - -def test_field_provides_are_printed_correctly_camel_case_off(): - global Review - - @strawberry.federation.type - class User: - username: str - - @strawberry.federation.type(keys=["upc"], extend=True) - class Product: - upc: str = strawberry.federation.field(external=True) - the_name: str = strawberry.federation.field(external=True) - reviews: List["Review"] - - @strawberry.federation.type - class Review: - body: str - author: User - product: Product = strawberry.federation.field(provides=["name"]) - - @strawberry.federation.type - class Query: - @strawberry.field - def top_products(self, first: int) -> List[Product]: - return [] - - schema = strawberry.federation.Schema( - query=Query, - config=StrawberryConfig(auto_camel_case=False), - enable_federation_2=True, - ) - - expected = """ - schema @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@external", "@key", "@provides"]) { - query: Query - } - - extend type Product @key(fields: "upc") { - upc: String! @external - the_name: String! @external - reviews: [Review!]! - } - - type Query { - _entities(representations: [_Any!]!): [_Entity]! - _service: _Service! - top_products(first: Int!): [Product!]! - } - - type Review { - body: String! - author: User! - product: Product! @provides(fields: "name") - } - - type User { - username: String! - } - - scalar _Any - - union _Entity = Product - - type _Service { - sdl: String! - } - """ - - assert schema.as_str() == textwrap.dedent(expected).strip() - - del Review diff --git a/tests/federation/printer/test_requires.py b/tests/federation/printer/test_requires.py deleted file mode 100644 index 71701810dc..0000000000 --- a/tests/federation/printer/test_requires.py +++ /dev/null @@ -1,81 +0,0 @@ -# type: ignore - -import textwrap -from typing import List - -import strawberry - - -def test_fields_requires_are_printed_correctly(): - global Review - - @strawberry.federation.type - class User: - username: str - - @strawberry.federation.type(keys=["upc"], extend=True) - class Product: - upc: str = strawberry.federation.field(external=True) - field1: str = strawberry.federation.field(external=True) - field2: str = strawberry.federation.field(external=True) - field3: str = strawberry.federation.field(external=True) - - @strawberry.federation.field(requires=["field1", "field2", "field3"]) - def reviews(self) -> List["Review"]: - return [] - - @strawberry.federation.type - class Review: - body: str - author: User - product: Product - - @strawberry.federation.type - class Query: - @strawberry.field - def top_products(self, first: int) -> List[Product]: - return [] - - schema = strawberry.federation.Schema(query=Query, enable_federation_2=True) - - expected = """ - schema @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@external", "@key", "@requires"]) { - query: Query - } - - extend type Product @key(fields: "upc") { - upc: String! @external - field1: String! @external - field2: String! @external - field3: String! @external - reviews: [Review!]! @requires(fields: "field1 field2 field3") - } - - type Query { - _entities(representations: [_Any!]!): [_Entity]! - _service: _Service! - topProducts(first: Int!): [Product!]! - } - - type Review { - body: String! - author: User! - product: Product! - } - - type User { - username: String! - } - - scalar _Any - - union _Entity = Product - - type _Service { - sdl: String! - } - """ - - assert schema.as_str() == textwrap.dedent(expected).strip() - - del Review diff --git a/tests/federation/printer/test_shareable.py b/tests/federation/printer/test_shareable.py deleted file mode 100644 index fc46998050..0000000000 --- a/tests/federation/printer/test_shareable.py +++ /dev/null @@ -1,55 +0,0 @@ -# type: ignore - -import textwrap -from typing import List - -import strawberry - - -def test_field_shareable_printed_correctly(): - @strawberry.interface - class SomeInterface: - id: strawberry.ID - - @strawberry.federation.type(keys=["upc"], extend=True, shareable=True) - class Product(SomeInterface): - upc: str = strawberry.federation.field(external=True, shareable=True) - - @strawberry.federation.type - class Query: - @strawberry.field - def top_products(self, first: int) -> List[Product]: - return [] - - schema = strawberry.federation.Schema(query=Query, enable_federation_2=True) - - expected = """ - schema @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@external", "@key", "@shareable"]) { - query: Query - } - - extend type Product implements SomeInterface @key(fields: "upc") @shareable { - id: ID! - upc: String! @external @shareable - } - - type Query { - _entities(representations: [_Any!]!): [_Entity]! - _service: _Service! - topProducts(first: Int!): [Product!]! - } - - interface SomeInterface { - id: ID! - } - - scalar _Any - - union _Entity = Product - - type _Service { - sdl: String! - } - """ - - assert schema.as_str() == textwrap.dedent(expected).strip() diff --git a/tests/federation/printer/test_tag.py b/tests/federation/printer/test_tag.py deleted file mode 100644 index c15e127ff2..0000000000 --- a/tests/federation/printer/test_tag.py +++ /dev/null @@ -1,243 +0,0 @@ -import textwrap -from enum import Enum -from typing import List -from typing_extensions import Annotated - -import strawberry - - -def test_field_tag_printed_correctly(): - @strawberry.federation.interface(tags=["myTag", "anotherTag"]) - class SomeInterface: - id: strawberry.ID - - @strawberry.federation.type(tags=["myTag", "anotherTag"]) - class Product(SomeInterface): - upc: str = strawberry.federation.field( - external=True, tags=["myTag", "anotherTag"] - ) - - @strawberry.federation.type - class Query: - @strawberry.field - def top_products( - self, first: Annotated[int, strawberry.federation.argument(tags=["myTag"])] - ) -> List[Product]: - return [] - - schema = strawberry.federation.Schema(query=Query, enable_federation_2=True) - - expected = """ - schema @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@external", "@tag"]) { - query: Query - } - - type Product implements SomeInterface @tag(name: "myTag") @tag(name: "anotherTag") { - id: ID! - upc: String! @external @tag(name: "myTag") @tag(name: "anotherTag") - } - - type Query { - _service: _Service! - topProducts(first: Int! @tag(name: "myTag")): [Product!]! - } - - interface SomeInterface @tag(name: "myTag") @tag(name: "anotherTag") { - id: ID! - } - - scalar _Any - - type _Service { - sdl: String! - } - """ - - assert schema.as_str() == textwrap.dedent(expected).strip() - - -def test_field_tag_printed_correctly_on_scalar(): - @strawberry.federation.scalar(tags=["myTag", "anotherTag"]) - class SomeScalar(str): - ... - - @strawberry.federation.type - class Query: - hello: SomeScalar - - schema = strawberry.federation.Schema(query=Query, enable_federation_2=True) - - expected = """ - schema @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@tag"]) { - query: Query - } - - type Query { - _service: _Service! - hello: SomeScalar! - } - - scalar SomeScalar @tag(name: "myTag") @tag(name: "anotherTag") - - scalar _Any - - type _Service { - sdl: String! - } - """ - - assert schema.as_str() == textwrap.dedent(expected).strip() - - -def test_field_tag_printed_correctly_on_enum(): - @strawberry.federation.enum(tags=["myTag", "anotherTag"]) - class SomeEnum(Enum): - A = "A" - - @strawberry.federation.type - class Query: - hello: SomeEnum - - schema = strawberry.federation.Schema(query=Query, enable_federation_2=True) - - expected = """ - schema @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@tag"]) { - query: Query - } - - type Query { - _service: _Service! - hello: SomeEnum! - } - - enum SomeEnum @tag(name: "myTag") @tag(name: "anotherTag") { - A - } - - scalar _Any - - type _Service { - sdl: String! - } - """ - - assert schema.as_str() == textwrap.dedent(expected).strip() - - -def test_field_tag_printed_correctly_on_enum_value(): - @strawberry.enum - class SomeEnum(Enum): - A = strawberry.federation.enum_value("A", tags=["myTag", "anotherTag"]) - - @strawberry.federation.type - class Query: - hello: SomeEnum - - schema = strawberry.federation.Schema(query=Query, enable_federation_2=True) - - expected = """ - schema @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@tag"]) { - query: Query - } - - type Query { - _service: _Service! - hello: SomeEnum! - } - - enum SomeEnum { - A @tag(name: "myTag") @tag(name: "anotherTag") - } - - scalar _Any - - type _Service { - sdl: String! - } - """ - - assert schema.as_str() == textwrap.dedent(expected).strip() - - -def test_field_tag_printed_correctly_on_union(): - @strawberry.type - class A: - a: str - - @strawberry.type - class B: - b: str - - Union = strawberry.federation.union("Union", (A, B), tags=["myTag", "anotherTag"]) - - @strawberry.federation.type - class Query: - hello: Union - - schema = strawberry.federation.Schema(query=Query, enable_federation_2=True) - - expected = """ - schema @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@tag"]) { - query: Query - } - - type A { - a: String! - } - - type B { - b: String! - } - - type Query { - _service: _Service! - hello: Union! - } - - union Union @tag(name: "myTag") @tag(name: "anotherTag") = A | B - - scalar _Any - - type _Service { - sdl: String! - } - """ - - assert schema.as_str() == textwrap.dedent(expected).strip() - - -def test_tag_printed_correctly_on_inputs(): - @strawberry.federation.input(tags=["myTag", "anotherTag"]) - class Input: - a: str = strawberry.federation.field(tags=["myTag", "anotherTag"]) - - @strawberry.federation.type - class Query: - hello: str - - schema = strawberry.federation.Schema( - query=Query, types=[Input], enable_federation_2=True - ) - - expected = """ - schema @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@tag"]) { - query: Query - } - - input Input @tag(name: "myTag") @tag(name: "anotherTag") { - a: String! @tag(name: "myTag") @tag(name: "anotherTag") - } - - type Query { - _service: _Service! - hello: String! - } - - scalar _Any - - type _Service { - sdl: String! - } - """ - - assert schema.as_str() == textwrap.dedent(expected).strip() diff --git a/tests/federation/test_entities.py b/tests/federation/test_entities.py index c01bb2893f..9ca521f8fa 100644 --- a/tests/federation/test_entities.py +++ b/tests/federation/test_entities.py @@ -10,7 +10,7 @@ class Product: @classmethod def resolve_reference(cls, upc): - return Product(upc=upc) + return Product(upc) @strawberry.federation.type(extend=True) class Query: @@ -18,7 +18,7 @@ class Query: def top_products(self, first: int) -> typing.List[Product]: return [] - schema = strawberry.federation.Schema(query=Query, enable_federation_2=True) + schema = strawberry.federation.Schema(query=Query) query = """ query ($representations: [_Any!]!) { @@ -50,7 +50,7 @@ class Product: @classmethod def resolve_reference(cls, info, upc): - return Product(upc=upc, info=info) + return Product(upc, info) @strawberry.federation.type(extend=True) class Query: @@ -58,7 +58,7 @@ class Query: def top_products(self, first: int) -> typing.List[Product]: return [] - schema = strawberry.federation.Schema(query=Query, enable_federation_2=True) + schema = strawberry.federation.Schema(query=Query) query = """ query ($representations: [_Any!]!) { @@ -84,215 +84,3 @@ def top_products(self, first: int) -> typing.List[Product]: "GraphQLResolveInfo(field_name='_entities', field_nodes=[FieldNode" in result.data["_entities"][0]["info"] ) - - -def test_does_not_need_custom_resolve_reference_for_basic_things(): - @strawberry.federation.type(keys=["upc"]) - class Product: - upc: str - - @strawberry.federation.type(extend=True) - class Query: - @strawberry.field - def top_products(self, first: int) -> typing.List[Product]: - return [] - - schema = strawberry.federation.Schema(query=Query, enable_federation_2=True) - - query = """ - query ($representations: [_Any!]!) { - _entities(representations: $representations) { - ... on Product { - upc - } - } - } - """ - - result = schema.execute_sync( - query, - variable_values={ - "representations": [{"__typename": "Product", "upc": "B00005N5PF"}] - }, - ) - - assert not result.errors - - assert result.data == {"_entities": [{"upc": "B00005N5PF"}]} - - -def test_does_not_need_custom_resolve_reference_nested(): - @strawberry.federation.type(keys=["id"]) - class Something: - id: str - - @strawberry.federation.type(keys=["upc"]) - class Product: - upc: str - something: Something - - @strawberry.federation.type(extend=True) - class Query: - @strawberry.field - def top_products(self, first: int) -> typing.List[Product]: - return [] - - schema = strawberry.federation.Schema(query=Query, enable_federation_2=True) - - query = """ - query ($representations: [_Any!]!) { - _entities(representations: $representations) { - ... on Product { - upc - something { - id - } - } - } - } - """ - - result = schema.execute_sync( - query, - variable_values={ - "representations": [ - {"__typename": "Product", "upc": "B00005N5PF", "something": {"id": "1"}} - ] - }, - ) - - assert not result.errors - - assert result.data == { - "_entities": [{"upc": "B00005N5PF", "something": {"id": "1"}}] - } - - -def test_fails_properly_when_wrong_data_is_passed(): - @strawberry.federation.type(keys=["id"]) - class Something: - id: str - - @strawberry.federation.type(keys=["upc"]) - class Product: - upc: str - something: Something - - @strawberry.federation.type(extend=True) - class Query: - @strawberry.field - def top_products(self, first: int) -> typing.List[Product]: - return [] - - schema = strawberry.federation.Schema(query=Query, enable_federation_2=True) - - query = """ - query ($representations: [_Any!]!) { - _entities(representations: $representations) { - ... on Product { - upc - something { - id - } - } - } - } - """ - - result = schema.execute_sync( - query, - variable_values={ - "representations": [ - { - "__typename": "Product", - "upc": "B00005N5PF", - "not_something": {"id": "1"}, - } - ] - }, - ) - - assert result.errors - - assert result.errors[0].message.startswith("Unable to resolve reference for") - - -async def test_can_use_async_resolve_reference(): - @strawberry.federation.type(keys=["upc"]) - class Product: - upc: str - - @classmethod - async def resolve_reference(cls, upc: str): - return Product(upc=upc) - - @strawberry.federation.type(extend=True) - class Query: - @strawberry.field - def top_products(self, first: int) -> typing.List[Product]: - return [] - - schema = strawberry.federation.Schema(query=Query, enable_federation_2=True) - - query = """ - query ($representations: [_Any!]!) { - _entities(representations: $representations) { - ... on Product { - upc - } - } - } - """ - - result = await schema.execute( - query, - variable_values={ - "representations": [{"__typename": "Product", "upc": "B00005N5PF"}] - }, - ) - - assert not result.errors - - assert result.data == {"_entities": [{"upc": "B00005N5PF"}]} - - -async def test_can_use_async_resolve_reference_multiple_representations(): - @strawberry.federation.type(keys=["upc"]) - class Product: - upc: str - - @classmethod - async def resolve_reference(cls, upc: str): - return Product(upc=upc) - - @strawberry.federation.type(extend=True) - class Query: - @strawberry.field - def top_products(self, first: int) -> typing.List[Product]: - return [] - - schema = strawberry.federation.Schema(query=Query, enable_federation_2=True) - - query = """ - query ($representations: [_Any!]!) { - _entities(representations: $representations) { - ... on Product { - upc - } - } - } - """ - - result = await schema.execute( - query, - variable_values={ - "representations": [ - {"__typename": "Product", "upc": "B00005N5PF"}, - {"__typename": "Product", "upc": "B00005N5PG"}, - ] - }, - ) - - assert not result.errors - - assert result.data == {"_entities": [{"upc": "B00005N5PF"}, {"upc": "B00005N5PG"}]} diff --git a/tests/federation/test_printer.py b/tests/federation/test_printer.py new file mode 100644 index 0000000000..39aadc9bed --- /dev/null +++ b/tests/federation/test_printer.py @@ -0,0 +1,379 @@ +# type: ignore + +import textwrap +from typing import List + +import strawberry +from strawberry.schema.config import StrawberryConfig + + +def test_entities_type_when_no_type_has_keys(): + global Review + + @strawberry.federation.type + class User: + username: str + + @strawberry.federation.type(keys=["upc"], extend=True) + class Product: + upc: str = strawberry.federation.field(external=True) + reviews: List["Review"] + + @strawberry.federation.type + class Review: + body: str + author: User + product: Product + + @strawberry.federation.type + class Query: + @strawberry.field + def top_products(self, first: int) -> List[Product]: + return [] + + schema = strawberry.federation.Schema(query=Query) + + expected = """ + extend type Product @key(fields: "upc") { + upc: String! @external + reviews: [Review!]! + } + + type Query { + _service: _Service! + _entities(representations: [_Any!]!): [_Entity]! + topProducts(first: Int!): [Product!]! + } + + type Review { + body: String! + author: User! + product: Product! + } + + type User { + username: String! + } + + scalar _Any + + union _Entity = Product + + type _Service { + sdl: String! + } + """ + + assert schema.as_str() == textwrap.dedent(expected).strip() + + del Review + + +def test_entities_extending_interface(): + @strawberry.interface + class SomeInterface: + id: strawberry.ID + + @strawberry.federation.type(keys=["upc"], extend=True) + class Product(SomeInterface): + upc: str = strawberry.federation.field(external=True) + + @strawberry.federation.type + class Query: + @strawberry.field + def top_products(self, first: int) -> List[Product]: + return [] + + schema = strawberry.federation.Schema(query=Query) + + expected = """ + extend type Product implements SomeInterface @key(fields: "upc") { + id: ID! + upc: String! @external + } + + type Query { + _service: _Service! + _entities(representations: [_Any!]!): [_Entity]! + topProducts(first: Int!): [Product!]! + } + + interface SomeInterface { + id: ID! + } + + scalar _Any + + union _Entity = Product + + type _Service { + sdl: String! + } + """ + + assert schema.as_str() == textwrap.dedent(expected).strip() + + +def test_fields_requires_are_printed_correctly(): + global Review + + @strawberry.federation.type + class User: + username: str + + @strawberry.federation.type(keys=["upc"], extend=True) + class Product: + upc: str = strawberry.federation.field(external=True) + field1: str = strawberry.federation.field(external=True) + field2: str = strawberry.federation.field(external=True) + field3: str = strawberry.federation.field(external=True) + + @strawberry.federation.field(requires=["field1", "field2", "field3"]) + def reviews(self) -> List["Review"]: + return [] + + @strawberry.federation.type + class Review: + body: str + author: User + product: Product + + @strawberry.federation.type + class Query: + @strawberry.field + def top_products(self, first: int) -> List[Product]: + return [] + + schema = strawberry.federation.Schema(query=Query) + + expected = """ + extend type Product @key(fields: "upc") { + upc: String! @external + field1: String! @external + field2: String! @external + field3: String! @external + reviews: [Review!]! @requires(fields: "field1 field2 field3") + } + + type Query { + _service: _Service! + _entities(representations: [_Any!]!): [_Entity]! + topProducts(first: Int!): [Product!]! + } + + type Review { + body: String! + author: User! + product: Product! + } + + type User { + username: String! + } + + scalar _Any + + union _Entity = Product + + type _Service { + sdl: String! + } + """ + + assert schema.as_str() == textwrap.dedent(expected).strip() + + del Review + + +def test_field_provides_are_printed_correctly_camel_case_on(): + global Review + + @strawberry.federation.type + class User: + username: str + + @strawberry.federation.type(keys=["upc"], extend=True) + class Product: + upc: str = strawberry.federation.field(external=True) + the_name: str = strawberry.federation.field(external=True) + reviews: List["Review"] + + @strawberry.federation.type + class Review: + body: str + author: User + product: Product = strawberry.federation.field(provides=["name"]) + + @strawberry.federation.type + class Query: + @strawberry.field + def top_products(self, first: int) -> List[Product]: + return [] + + schema = strawberry.federation.Schema( + query=Query, config=StrawberryConfig(auto_camel_case=True) + ) + + expected = """ + extend type Product @key(fields: "upc") { + upc: String! @external + theName: String! @external + reviews: [Review!]! + } + + type Query { + _service: _Service! + _entities(representations: [_Any!]!): [_Entity]! + topProducts(first: Int!): [Product!]! + } + + type Review { + body: String! + author: User! + product: Product! @provides(fields: "name") + } + + type User { + username: String! + } + + scalar _Any + + union _Entity = Product + + type _Service { + sdl: String! + } + """ + + assert schema.as_str() == textwrap.dedent(expected).strip() + + del Review + + +def test_field_provides_are_printed_correctly_camel_case_off(): + global Review + + @strawberry.federation.type + class User: + username: str + + @strawberry.federation.type(keys=["upc"], extend=True) + class Product: + upc: str = strawberry.federation.field(external=True) + the_name: str = strawberry.federation.field(external=True) + reviews: List["Review"] + + @strawberry.federation.type + class Review: + body: str + author: User + product: Product = strawberry.federation.field(provides=["name"]) + + @strawberry.federation.type + class Query: + @strawberry.field + def top_products(self, first: int) -> List[Product]: + return [] + + schema = strawberry.federation.Schema( + query=Query, config=StrawberryConfig(auto_camel_case=False) + ) + + expected = """ + extend type Product @key(fields: "upc") { + upc: String! @external + the_name: String! @external + reviews: [Review!]! + } + + type Query { + _service: _Service! + _entities(representations: [_Any!]!): [_Entity]! + top_products(first: Int!): [Product!]! + } + + type Review { + body: String! + author: User! + product: Product! @provides(fields: "name") + } + + type User { + username: String! + } + + scalar _Any + + union _Entity = Product + + type _Service { + sdl: String! + } + """ + + assert schema.as_str() == textwrap.dedent(expected).strip() + + del Review + + +def test_multiple_keys(): + global Review + + @strawberry.federation.type + class User: + username: str + + @strawberry.federation.type(keys=["upc"], extend=True) + class Product: + upc: str = strawberry.federation.field(external=True) + reviews: List["Review"] + + @strawberry.federation.type(keys=["body"]) + class Review: + body: str + author: User + product: Product + + @strawberry.federation.type + class Query: + @strawberry.field + def top_products(self, first: int) -> List[Product]: + return [] + + schema = strawberry.federation.Schema(query=Query) + + expected = """ + extend type Product @key(fields: "upc") { + upc: String! @external + reviews: [Review!]! + } + + type Query { + _service: _Service! + _entities(representations: [_Any!]!): [_Entity]! + topProducts(first: Int!): [Product!]! + } + + type Review @key(fields: "body") { + body: String! + author: User! + product: Product! + } + + type User { + username: String! + } + + scalar _Any + + union _Entity = Product | Review + + type _Service { + sdl: String! + } + """ + + assert schema.as_str() == textwrap.dedent(expected).strip() + + del Review diff --git a/tests/federation/test_schema.py b/tests/federation/test_schema.py index bda66c97c1..97d1dd0ed9 100644 --- a/tests/federation/test_schema.py +++ b/tests/federation/test_schema.py @@ -18,7 +18,7 @@ class Query: def top_products(self, first: int) -> List[Product]: return [] - schema = strawberry.federation.Schema(query=Query, enable_federation_2=True) + schema = strawberry.federation.Schema(query=Query) query = """ query { @@ -52,7 +52,7 @@ class Query: def top_products(self, first: int) -> List[Product]: return [] - schema = strawberry.federation.Schema(query=Query, enable_federation_2=True) + schema = strawberry.federation.Schema(query=Query) query = """ query { @@ -85,7 +85,7 @@ class Query: def top_products(self, first: int) -> List[Example]: return [] - schema = strawberry.federation.Schema(query=Query, enable_federation_2=True) + schema = strawberry.federation.Schema(query=Query) query = """ query { @@ -113,7 +113,7 @@ class Query: def top_products(self, first: int) -> List[Product]: return [] - schema = strawberry.federation.Schema(query=Query, enable_federation_2=True) + schema = strawberry.federation.Schema(query=Query) query = """ query { @@ -162,9 +162,9 @@ class ListOfProducts(Generic[T]): class Query: @strawberry.field def top_products(self, first: int) -> ListOfProducts[Product]: - return ListOfProducts(products=[]) + return ListOfProducts([]) - schema = strawberry.federation.Schema(query=Query, enable_federation_2=True) + schema = strawberry.federation.Schema(query=Query) query = """ query { @@ -200,66 +200,3 @@ def top_products(self, first: int) -> ListOfProducts[Product]: """ assert result.data == {"_service": {"sdl": textwrap.dedent(sdl).strip()}} - - -def test_input_types(): - @strawberry.federation.input(inaccessible=True) - class ExampleInput: - upc: str - - @strawberry.federation.type(extend=True) - class Query: - @strawberry.field - def top_products(self, example: ExampleInput) -> List[str]: - return [] - - schema = strawberry.federation.Schema(query=Query, enable_federation_2=True) - - query = """ - query { - __type(name: "ExampleInput") { - kind - } - } - """ - - result = schema.execute_sync(query) - - assert not result.errors - - assert result.data == {"__type": {"kind": "INPUT_OBJECT"}} - - -def test_can_create_schema_without_query(): - @strawberry.federation.type() - class Product: - upc: str - name: Optional[str] - price: Optional[int] - weight: Optional[int] - - schema = strawberry.federation.Schema(types=[Product], enable_federation_2=True) - - assert ( - str(schema) - == textwrap.dedent( - """ - type Product { - upc: String! - name: String - price: Int - weight: Int - } - - type Query { - _service: _Service! - } - - scalar _Any - - type _Service { - sdl: String! - } - """ - ).strip() - ) diff --git a/tests/federation/test_types.py b/tests/federation/test_types.py deleted file mode 100644 index bc35a6a913..0000000000 --- a/tests/federation/test_types.py +++ /dev/null @@ -1,34 +0,0 @@ -import strawberry - - -def test_type(): - @strawberry.federation.type(keys=["id"]) - class Location: - id: strawberry.ID - - assert Location(id=strawberry.ID("1")).id == "1" - - -def test_type_and_override(): - @strawberry.federation.type(keys=["id"]) - class Location: - id: strawberry.ID - address: str = strawberry.federation.field(override="start") - - location = Location(id=strawberry.ID("1"), address="ABC") - - assert location.id == "1" - assert location.address == "ABC" - - -def test_type_and_override_with_resolver(): - @strawberry.federation.type(keys=["id"]) - class Location: - id: strawberry.ID - address: str = strawberry.federation.field( - override="start", resolver=lambda: "ABC" - ) - - location = Location(id=strawberry.ID("1")) - - assert location.id == "1" diff --git a/tests/fields/test_arguments.py b/tests/fields/test_arguments.py index 37a45f1345..0b49d3bc57 100644 --- a/tests/fields/test_arguments.py +++ b/tests/fields/test_arguments.py @@ -1,15 +1,13 @@ import sys from typing import List, Optional -from typing_extensions import Annotated import pytest +from typing_extensions import Annotated + import strawberry -from strawberry import UNSET -from strawberry.exceptions import ( - InvalidArgumentTypeError, - MultipleStrawberryArgumentsError, -) +from strawberry.arguments import UNSET +from strawberry.exceptions import InvalidFieldArgument, MultipleStrawberryArgumentsError from strawberry.type import StrawberryList, StrawberryOptional @@ -257,7 +255,7 @@ class Query: def name( # type: ignore argument: Annotated[ str, - strawberry.argument(description="This is a description"), + strawberry.argument(description="This is a description"), # noqa: F722 ] ) -> str: return "Name" @@ -281,7 +279,7 @@ class Query: def name( # type: ignore argument: Annotated[ Optional[str], - strawberry.argument(description="This is a description"), + strawberry.argument(description="This is a description"), # noqa: F722 ] ) -> str: return "Name" @@ -303,12 +301,11 @@ def test_annotated_argument_with_default_value(): @strawberry.type class Query: @strawberry.field - def name( - self, + def name( # type: ignore argument: Annotated[ str, - strawberry.argument(description="This is a description"), - ] = "Patrick", + strawberry.argument(description="This is a description"), # noqa: F722 + ] = "Patrick" ) -> str: return "Name" @@ -329,12 +326,11 @@ def test_annotated_argument_with_rename(): @strawberry.type class Query: @strawberry.field - def name( - self, + def name( # type: ignore arg: Annotated[ str, - strawberry.argument(name="argument"), - ] = "Patrick", + strawberry.argument(name="argument"), # noqa: F722 + ] = "Patrick" ) -> str: return "Name" @@ -358,12 +354,12 @@ def test_multiple_annotated_arguments_exception(): with pytest.raises(MultipleStrawberryArgumentsError) as error: @strawberry.field - def name( + def name( # type: ignore argument: Annotated[ str, - strawberry.argument(description="This is a description"), - strawberry.argument(description="Another description"), - ], + strawberry.argument(description="This is a description"), # noqa: F722 + strawberry.argument(description="Another description"), # noqa: F722 + ] ) -> str: return "Name" @@ -378,7 +374,9 @@ def test_annotated_with_other_information(): @strawberry.type class Query: @strawberry.field - def name(self, argument: Annotated[str, "Some other info"]) -> str: + def name( # type: ignore + argument: Annotated[str, "Some other info"] # noqa: F722 + ) -> str: return "Name" definition = Query._type_definition @@ -403,12 +401,11 @@ def test_annotated_python_39(): @strawberry.type class Query: @strawberry.field - def name( - self, + def name( # type: ignore argument: Annotated[ str, - strawberry.argument(description="This is a description"), - ], + strawberry.argument(description="This is a description"), # noqa: F722 + ] ) -> str: return "Name" @@ -425,71 +422,54 @@ def name( assert argument.type is str -@pytest.mark.raises_strawberry_exception( - InvalidArgumentTypeError, - 'Argument "word" on field "add_word" cannot be of type "Union"', -) def test_union_as_an_argument_type(): - @strawberry.type - class Noun: - text: str + error_message = 'Argument "word" on field "add_word" cannot be of type "Union"' + with pytest.raises(InvalidFieldArgument, match=error_message): - @strawberry.type - class Verb: - text: str + @strawberry.type + class Noun: + text: str - Word = strawberry.union("Word", types=(Noun, Verb)) + @strawberry.type + class Verb: + text: str - @strawberry.field - def add_word(word: Word) -> bool: - return True + Word = strawberry.union("Word", types=(Noun, Verb)) + + @strawberry.field + def add_word(word: Word) -> bool: + return True -@pytest.mark.raises_strawberry_exception( - InvalidArgumentTypeError, - 'Argument "adjective" on field "add_adjective" cannot be of type "Interface"', -) def test_interface_as_an_argument_type(): - @strawberry.interface - class Adjective: - text: str + error_message = ( + 'Argument "adjective" on field "add_adjective" cannot be of type "Interface"' + ) + with pytest.raises(InvalidFieldArgument, match=error_message): - @strawberry.field - def add_adjective(adjective: Adjective) -> bool: - return True + @strawberry.interface + class Adjective: + text: str + + @strawberry.field + def add_adjective(adjective: Adjective) -> bool: + return True -@pytest.mark.raises_strawberry_exception( - InvalidArgumentTypeError, - ( +def test_resolver_with_invalid_field_argument_type(): + error_message = ( 'Argument "adjective" on field "add_adjective_resolver" cannot be ' 'of type "Interface"' - ), -) -def test_resolver_with_invalid_field_argument_type(): - @strawberry.interface - class Adjective: - text: str - - def add_adjective_resolver(adjective: Adjective) -> bool: - return True - - @strawberry.type - class Mutation: - add_adjective: bool = strawberry.field(resolver=add_adjective_resolver) - + ) + with pytest.raises(InvalidFieldArgument, match=error_message): -def test_unset_deprecation_warning(): - with pytest.deprecated_call(): - from strawberry.arguments import UNSET # noqa: F401 - with pytest.deprecated_call(): - from strawberry.arguments import is_unset # noqa: F401 + @strawberry.interface + class Adjective: + text: str + def add_adjective_resolver(adjective: Adjective) -> bool: + return True -def test_deprecated_unset(): - with pytest.deprecated_call(): - from strawberry.unset import is_unset - assert is_unset(UNSET) - assert not is_unset(None) - assert not is_unset(False) - assert not is_unset("hello world") + @strawberry.type + class Mutation: + add_adjective: bool = strawberry.field(resolver=add_adjective_resolver) diff --git a/tests/fields/test_resolvers.py b/tests/fields/test_resolvers.py index a183a675fa..3b4f7974fe 100644 --- a/tests/fields/test_resolvers.py +++ b/tests/fields/test_resolvers.py @@ -1,6 +1,6 @@ import dataclasses -import types -from typing import ClassVar, List, no_type_check +import re +from typing import ClassVar import pytest @@ -10,13 +10,7 @@ MissingFieldAnnotationError, MissingReturnAnnotationError, ) -from strawberry.scalars import JSON -from strawberry.types.fields.resolver import ( - Signature, - StrawberryResolver, - UncallableResolverError, -) -from strawberry.types.info import Info +from strawberry.types.fields.resolver import StrawberryResolver, UncallableResolverError def test_resolver_as_argument(): @@ -102,182 +96,92 @@ def val(cls) -> str: assert Query().val() == "thingy" -@pytest.mark.raises_strawberry_exception( - MissingReturnAnnotationError, - match='Return annotation missing for field "hello", did you forget to add it?', -) def test_raises_error_when_return_annotation_missing(): - @strawberry.type - class Query: - @strawberry.field - def hello(self): - return "I'm a resolver" + with pytest.raises(MissingReturnAnnotationError) as e: + @strawberry.type + class Query: + @strawberry.field + def hello(self): + return "I'm a resolver" -@pytest.mark.raises_strawberry_exception( - MissingReturnAnnotationError, - match='Return annotation missing for field "hello", did you forget to add it?', -) -def test_raises_error_when_return_annotation_missing_async_function(): - @strawberry.type - class Query: - @strawberry.field - async def hello(self): - return "I'm a resolver" + assert e.value.args == ( + 'Return annotation missing for field "hello", did you forget to add it?', + ) + with pytest.raises(MissingReturnAnnotationError) as e: -@pytest.mark.raises_strawberry_exception( - MissingReturnAnnotationError, - match='Return annotation missing for field "goodbye", did you forget to add it?', -) -def test_raises_error_when_return_annotation_missing_resolver(): - @strawberry.type - class Query2: - def adios(self): - return -1 + @strawberry.type + class Query2: + def adios(self): + return -1 - goodbye = strawberry.field(resolver=adios) + goodbye = strawberry.field(resolver=adios) + # TODO: Maybe we should say that the resolver needs the annotation? -@pytest.mark.raises_strawberry_exception( - MissingArgumentsAnnotationsError, - match=( - 'Missing annotation for argument "query" in field "hello", ' - "did you forget to add it?" - ), -) -def test_raises_error_when_argument_annotation_missing(): - @strawberry.field - def hello(self, query) -> str: - return "I'm a resolver" + assert e.value.args == ( + 'Return annotation missing for field "goodbye", did you forget to add it?', + ) -@pytest.mark.raises_strawberry_exception( - MissingArgumentsAnnotationsError, - match=( - 'Missing annotation for arguments "query" and "limit" ' - 'in field "hello", did you forget to add it?' - ), -) -def test_raises_error_when_argument_annotation_missing_multiple_fields(): - @strawberry.field - def hello(self, query, limit) -> str: - return "I'm a resolver" +def test_raises_error_when_argument_annotation_missing(): + with pytest.raises(MissingArgumentsAnnotationsError) as e: + @strawberry.field + def hello(self, query) -> str: + return "I'm a resolver" -@pytest.mark.raises_strawberry_exception( - MissingArgumentsAnnotationsError, - match=( - 'Missing annotation for argument "query" ' - 'in field "hello", did you forget to add it?' - ), -) -def test_raises_error_when_argument_annotation_missing_multiple_lines(): - @strawberry.field - def hello( - self, - query, - ) -> str: - return "I'm a resolver" + assert e.value.args == ( + 'Missing annotation for argument "query" in field "hello", ' + "did you forget to add it?", + ) + with pytest.raises(MissingArgumentsAnnotationsError) as e: -@pytest.mark.raises_strawberry_exception( - MissingArgumentsAnnotationsError, - match=( - 'Missing annotation for argument "query" ' - 'in field "hello", did you forget to add it?' - ), -) -def test_raises_error_when_argument_annotation_missing_default_value(): - @strawberry.field - def hello( - self, - query="this is a default value", - ) -> str: - return "I'm a resolver" + @strawberry.field + def hello2(self, query, limit) -> str: + return "I'm a resolver" + + assert e.value.args == ( + 'Missing annotation for arguments "limit" and "query" ' + 'in field "hello2", did you forget to add it?', + ) -@pytest.mark.raises_strawberry_exception( - MissingFieldAnnotationError, - match=( - 'Unable to determine the type of field "missing". ' - "Either annotate it directly, or provide a typed resolver " - "using @strawberry.field." - ), -) def test_raises_error_when_missing_annotation_and_resolver(): - @strawberry.type - class Query: - missing = strawberry.field(name="annotation") + with pytest.raises(MissingFieldAnnotationError) as e: + @strawberry.type + class Query: # noqa: F841 + missing = strawberry.field(name="annotation") -@pytest.mark.raises_strawberry_exception( - MissingFieldAnnotationError, - match=( + [message] = e.value.args + assert message == ( 'Unable to determine the type of field "missing". Either annotate it ' "directly, or provide a typed resolver using @strawberry.field." - ), -) + ) + + def test_raises_error_when_missing_type(): """Test to make sure that if somehow a non-StrawberryField field is added to the cls without annotations it raises an exception. This would occur if someone manually uses dataclasses.field""" + with pytest.raises(MissingFieldAnnotationError) as e: - @strawberry.type - class Query: - missing = dataclasses.field() - + @strawberry.type + class Query: # noqa: F841 + missing = dataclasses.field() -@pytest.mark.raises_strawberry_exception( - MissingFieldAnnotationError, - match=( + [message] = e.value.args + assert message == ( 'Unable to determine the type of field "missing". Either annotate it ' "directly, or provide a typed resolver using @strawberry.field." - ), -) -def test_raises_error_when_missing_type_on_dynamic_class(): - # this test if for making sure the code that finds the exception source - # doesn't crash with dynamic code - - namespace = {"missing": dataclasses.field()} - - strawberry.type(types.new_class("Query", (), {}, lambda ns: ns.update(namespace))) - - -@pytest.mark.raises_strawberry_exception( - MissingFieldAnnotationError, - match=( - 'Unable to determine the type of field "banana". Either annotate it ' - "directly, or provide a typed resolver using @strawberry.field." - ), -) -def test_raises_error_when_missing_type_on_longish_class(): - @strawberry.type - class Query: - field_1: str = strawberry.field(name="field_1") - field_2: str = strawberry.field(name="field_2") - field_3: str = strawberry.field(name="field_3") - field_4: str = strawberry.field(name="field_4") - field_5: str = strawberry.field(name="field_5") - field_6: str = strawberry.field(name="field_6") - field_7: str = strawberry.field(name="field_7") - field_8: str = strawberry.field(name="field_8") - field_9: str = strawberry.field(name="field_9") - banana = strawberry.field(name="banana") - field_10: str = strawberry.field(name="field_10") - field_11: str = strawberry.field(name="field_11") - field_12: str = strawberry.field(name="field_12") - field_13: str = strawberry.field(name="field_13") - field_14: str = strawberry.field(name="field_14") - field_15: str = strawberry.field(name="field_15") - field_16: str = strawberry.field(name="field_16") - field_17: str = strawberry.field(name="field_17") - field_18: str = strawberry.field(name="field_18") - field_19: str = strawberry.field(name="field_19") + ) def test_raises_error_calling_uncallable_resolver(): - @classmethod # type: ignore + @classmethod def class_func(cls) -> int: ... @@ -285,10 +189,12 @@ def class_func(cls) -> int: # to a class at this point resolver = StrawberryResolver(class_func) - with pytest.raises( - UncallableResolverError, - match="Attempted to call resolver (.*) with uncallable function (.*)", - ): + expected_error_message = re.escape( + f"Attempted to call resolver {resolver} with uncallable function " + f"{class_func}" + ) + + with pytest.raises(UncallableResolverError, match=expected_error_message): resolver() @@ -329,8 +235,8 @@ class Query: name: str = strawberry.field(resolver=get_name) name_2: str = strawberry.field(resolver=get_name) - assert Query(a=1) == Query(a=1) - assert Query(a=1) != Query(a=2) + assert Query(1) == Query(1) + assert Query(1) != Query(2) def test_eq_fields(): @@ -339,8 +245,8 @@ class Query: a: int name: str = strawberry.field(name="name") - assert Query(a=1, name="name") == Query(a=1, name="name") - assert Query(a=1, name="name") != Query(a=1, name="not a name") + assert Query(1, "name") == Query(1, "name") + assert Query(1, "name") != Query(1, "not a name") def test_with_resolver_fields(): @@ -352,73 +258,5 @@ class Query: def name(self) -> str: return "A" - assert Query(a=1) == Query(a=1) - assert Query(a=1) != Query(a=2) - - -def test_resolver_annotations(): - """Ensure only non-reserved annotations are returned.""" - - def resolver_annotated_info( - self, root, foo: str, bar: float, info: str, strawberry_info: Info - ) -> str: - return "Hello world" - - resolver = StrawberryResolver(resolver_annotated_info) - - expected_annotations = {"foo": str, "bar": float, "info": str, "return": str} - assert resolver.annotations == expected_annotations - - # Sanity-check to ensure StrawberryArguments return the same annotations - assert { - **{ - arg.python_name: arg.type_annotation.resolve() # type: ignore - for arg in resolver.arguments - }, - "return": str, - } - - -@no_type_check -def test_resolver_with_unhashable_default(): - @strawberry.type - class Query: - @strawberry.field - def field(self, x: List[str] = ["foo"], y: JSON = {"foo": 42}) -> str: - return f"{x} {y}" - - schema = strawberry.Schema(Query) - result = schema.execute_sync("query { field }") - assert result.data == {"field": "['foo'] {'foo': 42}"} - assert not result.errors - - -@no_type_check -def test_parameter_hash_collision(): - """Ensure support for hashable defaults does not introduce collision.""" - - def foo(x: str = "foo"): - pass - - def bar(x: str = "bar"): - pass - - foo_signature = Signature.from_callable(foo, follow_wrapped=True) - bar_signature = Signature.from_callable(bar, follow_wrapped=True) - - foo_param = foo_signature.parameters["x"] - bar_param = bar_signature.parameters["x"] - - # Ensure __eq__ still functions properly - assert foo_param != bar_param - - # Ensure collision does not occur in hash-map and hash-tables. Colisions are - # prevented by Python invoking __eq__ when two items have the same hash. - parameters_map = { - foo_param: "foo", - bar_param: "bar", - } - parameters_set = {foo_param, bar_param} - - assert len(parameters_map) == 2 - assert len(parameters_set) == 2 + assert Query(1) == Query(1) + assert Query(1) != Query(2) diff --git a/strawberry/codegen/plugins/__init__.py b/tests/flask/__init__.py similarity index 100% rename from strawberry/codegen/plugins/__init__.py rename to tests/flask/__init__.py diff --git a/tests/flask/app.py b/tests/flask/app.py new file mode 100644 index 0000000000..0a0e263dd5 --- /dev/null +++ b/tests/flask/app.py @@ -0,0 +1,51 @@ +import typing + +import strawberry +from flask import Flask +from strawberry.file_uploads import Upload +from strawberry.flask.views import GraphQLView as BaseGraphQLView + + +def create_app(**kwargs): + @strawberry.input + class FolderInput: + files: typing.List[Upload] + + @strawberry.type + class Query: + hello: str = "strawberry" + + @strawberry.type + class Mutation: + @strawberry.mutation + def read_text(self, text_file: Upload) -> str: + return text_file.read().decode() + + @strawberry.mutation + def read_files(self, files: typing.List[Upload]) -> typing.List[str]: + contents = [] + for file in files: + contents.append(file.read().decode()) + return contents + + @strawberry.mutation + def read_folder(self, folder: FolderInput) -> typing.List[str]: + contents = [] + for file in folder.files: + contents.append(file.read().decode()) + return contents + + schema = strawberry.Schema(query=Query, mutation=Mutation) + + class GraphQLView(BaseGraphQLView): + def get_root_value(self): + return Query() + + app = Flask(__name__) + app.debug = True + + app.add_url_rule( + "/graphql", + view_func=GraphQLView.as_view("graphql_view", schema=schema, **kwargs), + ) + return app diff --git a/tests/flask/conftest.py b/tests/flask/conftest.py new file mode 100644 index 0000000000..928ddc9bbc --- /dev/null +++ b/tests/flask/conftest.py @@ -0,0 +1,9 @@ +import pytest + +from .app import create_app + + +@pytest.fixture +def flask_client(): + with create_app().test_client() as client: + yield client diff --git a/tests/flask/test_upload.py b/tests/flask/test_upload.py new file mode 100644 index 0000000000..0574bc2123 --- /dev/null +++ b/tests/flask/test_upload.py @@ -0,0 +1,89 @@ +import json +from io import BytesIO + + +def test_upload(flask_client): + f = (BytesIO(b"strawberry"), "textFile.txt") + + query = """mutation($textFile: Upload!) { + readText(textFile: $textFile) + }""" + + response = flask_client.post( + "/graphql", + data={ + "operations": json.dumps({"query": query, "variables": {"textFile": None}}), + "map": json.dumps({"textFile": ["variables.textFile"]}), + "textFile": f, + }, + content_type="multipart/form-data", + ) + + assert response.status_code == 200 + + data = json.loads(response.data.decode()) + + assert not data.get("errors") + assert data["data"]["readText"] == "strawberry" + + +def test_file_list_upload(flask_client): + query = "mutation($files: [Upload!]!) { readFiles(files: $files) }" + operations = json.dumps({"query": query, "variables": {"files": [None, None]}}) + file_map = json.dumps( + {"file1": ["variables.files.0"], "file2": ["variables.files.1"]} + ) + + file1 = (BytesIO(b"strawberry1"), "file1.txt") + file2 = (BytesIO(b"strawberry2"), "file2.txt") + + response = flask_client.post( + "/graphql", + data={ + "operations": operations, + "map": file_map, + "file1": file1, + "file2": file2, + }, + content_type="multipart/form-data", + ) + + assert response.status_code == 200 + data = json.loads(response.data.decode()) + + assert not data.get("errors") + assert len(data["data"]["readFiles"]) == 2 + assert data["data"]["readFiles"][0] == "strawberry1" + assert data["data"]["readFiles"][1] == "strawberry2" + + +def test_nested_file_list(flask_client): + query = "mutation($folder: FolderInput!) { readFolder(folder: $folder) }" + operations = json.dumps( + {"query": query, "variables": {"folder": {"files": [None, None]}}} + ) + file_map = json.dumps( + {"file1": ["variables.folder.files.0"], "file2": ["variables.folder.files.1"]} + ) + + file1 = (BytesIO(b"strawberry1"), "file1.txt") + file2 = (BytesIO(b"strawberry2"), "file2.txt") + + response = flask_client.post( + "/graphql", + data={ + "operations": operations, + "map": file_map, + "file1": file1, + "file2": file2, + }, + content_type="multipart/form-data", + ) + + assert response.status_code == 200 + data = json.loads(response.data.decode()) + + assert not data.get("errors") + assert len(data["data"]["readFolder"]) == 2 + assert data["data"]["readFolder"][0] == "strawberry1" + assert data["data"]["readFolder"][1] == "strawberry2" diff --git a/tests/flask/test_view.py b/tests/flask/test_view.py new file mode 100644 index 0000000000..8adfdcc9dc --- /dev/null +++ b/tests/flask/test_view.py @@ -0,0 +1,116 @@ +import json + +import strawberry +from flask import Flask, request +from strawberry.flask.views import GraphQLView as BaseGraphQLView +from strawberry.types import ExecutionResult, Info + +from .app import create_app + + +def test_graphql_query(flask_client): + query = { + "query": """ + query { + hello + } + """ + } + + response = flask_client.get("/graphql", json=query) + data = json.loads(response.data.decode()) + + assert response.status_code == 200 + assert data["data"]["hello"] == "strawberry" + + +def test_fails_when_request_body_has_invalid_json(flask_client): + response = flask_client.post( + "/graphql", + data='{"qeury": "{__typena"', + headers={"content-type": "application/json"}, + ) + assert response.status_code == 400 + + +def test_graphiql_view(flask_client): + flask_client.environ_base["HTTP_ACCEPT"] = "text/html" + response = flask_client.get("/graphql") + body = response.data.decode() + + assert "GraphiQL" in body + + +def test_graphiql_disabled_view(): + app = create_app(graphiql=False) + + with app.test_client() as client: + client.environ_base["HTTP_ACCEPT"] = "text/html" + response = client.get("/graphql") + + assert response.status_code == 404 + + +def test_custom_context(): + class CustomGraphQLView(BaseGraphQLView): + def get_context(self): + return { + "request": request, + "custom_value": "Hi!", + } + + @strawberry.type + class Query: + @strawberry.field + def custom_context_value(self, info: Info) -> str: + return info.context["custom_value"] + + schema = strawberry.Schema(query=Query) + + app = Flask(__name__) + app.debug = True + + app.add_url_rule( + "/graphql", + view_func=CustomGraphQLView.as_view("graphql_view", schema=schema), + ) + + with app.test_client() as client: + query = "{ customContextValue }" + + response = client.get("/graphql", json={"query": query}) + data = json.loads(response.data.decode()) + + assert response.status_code == 200 + assert data["data"] == {"customContextValue": "Hi!"} + + +def test_custom_process_result(): + class CustomGraphQLView(BaseGraphQLView): + def process_result(self, result: ExecutionResult): + return {} + + @strawberry.type + class Query: + @strawberry.field + def abc(self) -> str: + return "ABC" + + schema = strawberry.Schema(query=Query) + + app = Flask(__name__) + app.debug = True + + app.add_url_rule( + "/graphql", + view_func=CustomGraphQLView.as_view("graphql_view", schema=schema), + ) + + with app.test_client() as client: + query = "{ abc }" + + response = client.get("/graphql", json={"query": query}) + data = json.loads(response.data.decode()) + + assert response.status_code == 200 + assert data == {} diff --git a/tests/http/__init__.py b/tests/http/__init__.py deleted file mode 100644 index a86b4f3f2c..0000000000 --- a/tests/http/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -# Starlite is only available on python 3.8+ -try: - - IS_STARLITE_INSTALLED: bool = False -except ModuleNotFoundError: - IS_STARLITE_INSTALLED: bool = True - -__all__ = ["IS_STARLITE_INSTALLED"] diff --git a/tests/http/clients/__init__.py b/tests/http/clients/__init__.py deleted file mode 100644 index 5150fdd265..0000000000 --- a/tests/http/clients/__init__.py +++ /dev/null @@ -1,167 +0,0 @@ -import abc -import json -from dataclasses import dataclass -from io import BytesIO -from typing import Callable, Dict, List, Optional -from typing_extensions import Literal - -from strawberry.http import GraphQLHTTPResponse -from strawberry.types import ExecutionResult - -JSON = Dict[str, object] -ResultOverrideFunction = Optional[Callable[[ExecutionResult], GraphQLHTTPResponse]] - - -@dataclass -class Response: - status_code: int - data: bytes - # TODO: headers - - @property - def text(self) -> str: - return self.data.decode() - - @property - def json(self) -> JSON: - return json.loads(self.data) - - -class HttpClient(abc.ABC): - @abc.abstractmethod - def __init__( - self, - graphiql: bool = True, - allow_queries_via_get: bool = True, - result_override: ResultOverrideFunction = None, - ): - ... - - @abc.abstractmethod - async def _graphql_request( - self, - method: Literal["get", "post"], - query: Optional[str] = None, - variables: Optional[Dict[str, object]] = None, - files: Optional[Dict[str, BytesIO]] = None, - headers: Optional[Dict[str, str]] = None, - **kwargs, - ) -> Response: - ... - - @abc.abstractmethod - async def request( - self, - url: str, - method: Literal["get", "post", "patch", "put", "delete"], - headers: Optional[Dict[str, str]] = None, - ) -> Response: - ... - - @abc.abstractmethod - async def get( - self, - url: str, - headers: Optional[Dict[str, str]] = None, - ) -> Response: - ... - - @abc.abstractmethod - async def post( - self, - url: str, - data: Optional[bytes] = None, - json: Optional[JSON] = None, - headers: Optional[Dict[str, str]] = None, - ) -> Response: - ... - - async def query( - self, - query: Optional[str] = None, - method: Literal["get", "post"] = "post", - variables: Optional[Dict[str, object]] = None, - files: Optional[Dict[str, BytesIO]] = None, - headers: Optional[Dict[str, str]] = None, - ) -> Response: - return await self._graphql_request( - method, query=query, headers=headers, variables=variables, files=files - ) - - def _get_headers( - self, - method: Literal["get", "post"], - headers: Optional[Dict[str, str]], - files: Optional[Dict[str, BytesIO]], - ) -> Dict[str, str]: - addition_headers = {} - - content_type = None - - if method == "post" and not files: - content_type = "application/json" - - addition_headers = {"Content-Type": content_type} if content_type else {} - - return addition_headers if headers is None else {**addition_headers, **headers} - - def _build_body( - self, - query: Optional[str] = None, - variables: Optional[Dict[str, object]] = None, - files: Optional[Dict[str, BytesIO]] = None, - method: Literal["get", "post"] = "post", - ) -> Optional[Dict[str, object]]: - if query is None: - assert files is None - assert variables is None - - return None - - body: Dict[str, object] = {"query": query} - - if variables: - body["variables"] = variables - - if files: - assert variables is not None - - file_map = self._build_multipart_file_map(variables, files) - - body = { - "operations": json.dumps(body), - "map": json.dumps(file_map), - } - - if method == "get" and variables: - body["variables"] = json.dumps(variables) - - return body - - @staticmethod - def _build_multipart_file_map( - variables: Dict[str, object], files: Dict[str, BytesIO] - ) -> Dict[str, List[str]]: - # TODO: remove code duplication - - files_map: Dict[str, List[str]] = {} - for key, values in variables.items(): - if isinstance(values, dict): - folder_key = list(values.keys())[0] - key += f".{folder_key}" - # the list of file is inside the folder keyword - values = values[folder_key] - - # If the variable is an array of files we must number the keys - if isinstance(values, list): - # copying `files` as when we map a file we must discard from the dict - _kwargs = files.copy() - for index, _ in enumerate(values): - k = list(_kwargs.keys())[0] - _kwargs.pop(k) - files_map.setdefault(k, []) - files_map[k].append(f"variables.{key}.{index}") - else: - files_map[key] = [f"variables.{key}"] - - return files_map diff --git a/tests/http/clients/aiohttp.py b/tests/http/clients/aiohttp.py deleted file mode 100644 index 884d11e619..0000000000 --- a/tests/http/clients/aiohttp.py +++ /dev/null @@ -1,131 +0,0 @@ -from __future__ import annotations - -import json -from io import BytesIO -from typing import Dict, Optional -from typing_extensions import Literal - -from aiohttp import web -from aiohttp.test_utils import TestClient, TestServer -from strawberry.aiohttp.views import GraphQLView as BaseGraphQLView -from strawberry.http import GraphQLHTTPResponse -from strawberry.types import ExecutionResult - -from ..context import get_context -from ..schema import Query, schema -from . import JSON, HttpClient, Response, ResultOverrideFunction - - -class GraphQLView(BaseGraphQLView): - result_override: ResultOverrideFunction = None - - async def get_context( - self, request: web.Request, response: web.StreamResponse - ) -> object: - context = await super().get_context(request, response) - - return get_context(context) - - async def get_root_value(self, request: web.Request): - return Query() - - async def process_result( - self, request: web.Request, result: ExecutionResult - ) -> GraphQLHTTPResponse: - if self.result_override: - return self.result_override(result) - - return await super().process_result(request, result) - - -class AioHttpClient(HttpClient): - def __init__( - self, - graphiql: bool = True, - allow_queries_via_get: bool = True, - result_override: ResultOverrideFunction = None, - ): - view = GraphQLView( - schema=schema, - graphiql=graphiql, - allow_queries_via_get=allow_queries_via_get, - ) - view.result_override = result_override - - self.app = web.Application() - self.app.router.add_route( - "*", - "/graphql", - view, - ) - - async def _graphql_request( - self, - method: Literal["get", "post"], - query: Optional[str] = None, - variables: Optional[Dict[str, object]] = None, - files: Optional[Dict[str, BytesIO]] = None, - headers: Optional[Dict[str, str]] = None, - **kwargs, - ) -> Response: - async with TestClient(TestServer(self.app)) as client: - body = self._build_body( - query=query, variables=variables, files=files, method=method - ) - - if body and files: - body.update(files) - - if method == "get": - kwargs["params"] = body - else: - kwargs["data"] = body if files else json.dumps(body) - - response = await getattr(client, method)( - "/graphql", - headers=self._get_headers(method=method, headers=headers, files=files), - **kwargs, - ) - - return Response( - status_code=response.status, - data=(await response.text()).encode(), - ) - - async def request( - self, - url: str, - method: Literal["get", "post", "patch", "put", "delete"], - headers: Optional[Dict[str, str]] = None, - ) -> Response: - async with TestClient(TestServer(self.app)) as client: - response = await getattr(client, method)(url, headers=headers) - - return Response( - status_code=response.status, - data=(await response.text()).encode(), - ) - - async def get( - self, - url: str, - headers: Optional[Dict[str, str]] = None, - ) -> Response: - return await self.request(url, "get", headers=headers) - - async def post( - self, - url: str, - data: Optional[bytes] = None, - json: Optional[JSON] = None, - headers: Optional[Dict[str, str]] = None, - ) -> Response: - async with TestClient(TestServer(self.app)) as client: - response = await client.post( - "/graphql", headers=headers, data=data, json=json - ) - - return Response( - status_code=response.status, - data=(await response.text()).encode(), - ) diff --git a/tests/http/clients/asgi.py b/tests/http/clients/asgi.py deleted file mode 100644 index adabf21562..0000000000 --- a/tests/http/clients/asgi.py +++ /dev/null @@ -1,129 +0,0 @@ -from __future__ import annotations - -import json -from io import BytesIO -from typing import Dict, Optional, Union -from typing_extensions import Literal - -from starlette.requests import Request -from starlette.responses import Response as StarletteResponse -from starlette.testclient import TestClient -from starlette.websockets import WebSocket - -from strawberry.asgi import GraphQL as BaseGraphQLView -from strawberry.http import GraphQLHTTPResponse -from strawberry.types import ExecutionResult - -from ..context import get_context -from ..schema import Query, schema -from . import JSON, HttpClient, Response, ResultOverrideFunction - - -class GraphQLView(BaseGraphQLView): - result_override: ResultOverrideFunction = None - - async def get_root_value(self, request: Union[WebSocket, Request]) -> Query: - return Query() - - async def get_context( - self, - request: Union[Request, WebSocket], - response: Optional[StarletteResponse] = None, - ) -> object: - context = await super().get_context(request, response) - - return get_context(context) - - async def process_result( - self, request: Request, result: ExecutionResult - ) -> GraphQLHTTPResponse: - if self.result_override: - return self.result_override(result) - - return await super().process_result(request, result) - - -class AsgiHttpClient(HttpClient): - def __init__( - self, - graphiql: bool = True, - allow_queries_via_get: bool = True, - result_override: ResultOverrideFunction = None, - ): - view = GraphQLView( - schema, - graphiql=graphiql, - allow_queries_via_get=allow_queries_via_get, - ) - view.result_override = result_override - - self.client = TestClient(view) - - async def _graphql_request( - self, - method: Literal["get", "post"], - query: Optional[str] = None, - variables: Optional[Dict[str, object]] = None, - files: Optional[Dict[str, BytesIO]] = None, - headers: Optional[Dict[str, str]] = None, - **kwargs, - ) -> Response: - body = self._build_body( - query=query, variables=variables, files=files, method=method - ) - - if method == "get": - kwargs["params"] = body - elif body: - if files: - kwargs["data"] = body - else: - kwargs["content"] = json.dumps(body) - - if files is not None: - kwargs["files"] = files - - response = getattr(self.client, method)( - "/graphql", - headers=self._get_headers(method=method, headers=headers, files=files), - **kwargs, - ) - - return Response( - status_code=response.status_code, - data=response.content, - ) - - async def request( - self, - url: str, - method: Literal["get", "post", "patch", "put", "delete"], - headers: Optional[Dict[str, str]] = None, - ) -> Response: - response = getattr(self.client, method)(url, headers=headers) - - return Response( - status_code=response.status_code, - data=response.content, - ) - - async def get( - self, - url: str, - headers: Optional[Dict[str, str]] = None, - ) -> Response: - return await self.request(url, "get", headers=headers) - - async def post( - self, - url: str, - data: Optional[bytes] = None, - json: Optional[JSON] = None, - headers: Optional[Dict[str, str]] = None, - ) -> Response: - response = self.client.post(url, headers=headers, content=data, json=json) - - return Response( - status_code=response.status_code, - data=response.content, - ) diff --git a/tests/http/clients/async_django.py b/tests/http/clients/async_django.py deleted file mode 100644 index f191244295..0000000000 --- a/tests/http/clients/async_django.py +++ /dev/null @@ -1,53 +0,0 @@ -from __future__ import annotations - -from django.core.exceptions import BadRequest, SuspiciousOperation -from django.http import Http404, HttpRequest, HttpResponse -from django.test.client import RequestFactory - -from strawberry.django.views import AsyncGraphQLView as BaseAsyncGraphQLView -from strawberry.http import GraphQLHTTPResponse -from strawberry.types import ExecutionResult - -from ..context import get_context -from ..schema import Query, schema -from . import Response, ResultOverrideFunction -from .django import DjangoHttpClient - - -class AsyncGraphQLView(BaseAsyncGraphQLView): - result_override: ResultOverrideFunction = None - - async def get_root_value(self, request: HttpRequest): - return Query() - - async def get_context(self, request: HttpRequest, response: HttpResponse) -> object: - context = {"request": request, "response": response} - - return get_context(context) - - async def process_result( - self, request: HttpRequest, result: ExecutionResult - ) -> GraphQLHTTPResponse: - if self.result_override: - return self.result_override(result) - - return await super().process_result(request, result) - - -class AsyncDjangoHttpClient(DjangoHttpClient): - async def _do_request(self, request: RequestFactory) -> Response: - view = AsyncGraphQLView.as_view( - schema=schema, - graphiql=self.graphiql, - allow_queries_via_get=self.allow_queries_via_get, - result_override=self.result_override, - ) - - try: - response = await view(request) - except Http404: - return Response(status_code=404, data=b"Not found") - except (BadRequest, SuspiciousOperation) as e: - return Response(status_code=400, data=e.args[0].encode()) - else: - return Response(status_code=response.status_code, data=response.content) diff --git a/tests/http/clients/async_flask.py b/tests/http/clients/async_flask.py deleted file mode 100644 index 2ac4fc7d2b..0000000000 --- a/tests/http/clients/async_flask.py +++ /dev/null @@ -1,65 +0,0 @@ -from __future__ import annotations - -from typing import Dict - -from flask import Flask -from flask import Response as FlaskResponse -from strawberry.flask.views import AsyncGraphQLView as BaseAsyncGraphQLView -from strawberry.http import GraphQLHTTPResponse -from strawberry.types import ExecutionResult - -from ..context import get_context -from ..schema import Query, schema -from . import ResultOverrideFunction -from .flask import FlaskHttpClient - - -class GraphQLView(BaseAsyncGraphQLView): - # this allows to test our code path for checking the request type - # TODO: we might want to remove our check since it is done by flask - # already - methods = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD"] - - result_override: ResultOverrideFunction = None - - def __init__(self, *args, **kwargs): - self.result_override = kwargs.pop("result_override") - super().__init__(*args, **kwargs) - - async def get_root_value(self): - return Query() - - async def get_context(self, response: FlaskResponse) -> Dict[str, object]: - context = await super().get_context(response) - - return get_context(context) - - async def process_result(self, result: ExecutionResult) -> GraphQLHTTPResponse: - if self.result_override: - return self.result_override(result) - - return await super().process_result(result) - - -class AsyncFlaskHttpClient(FlaskHttpClient): - def __init__( - self, - graphiql: bool = True, - allow_queries_via_get: bool = True, - result_override: ResultOverrideFunction = None, - ): - self.app = Flask(__name__) - self.app.debug = True - - view = GraphQLView.as_view( - "graphql_view", - schema=schema, - graphiql=graphiql, - allow_queries_via_get=allow_queries_via_get, - result_override=result_override, - ) - - self.app.add_url_rule( - "/graphql", - view_func=view, - ) diff --git a/tests/http/clients/chalice.py b/tests/http/clients/chalice.py deleted file mode 100644 index bd5933329e..0000000000 --- a/tests/http/clients/chalice.py +++ /dev/null @@ -1,142 +0,0 @@ -from __future__ import annotations - -import urllib.parse -from io import BytesIO -from json import dumps -from typing import Dict, Optional, Union -from typing_extensions import Literal - -from chalice.app import Chalice -from chalice.app import Request as ChaliceRequest -from chalice.test import Client -from strawberry.chalice.views import GraphQLView as BaseGraphQLView -from strawberry.http import GraphQLHTTPResponse -from strawberry.http.temporal_response import TemporalResponse -from strawberry.types import ExecutionResult - -from ..context import get_context -from ..schema import Query, schema -from . import JSON, HttpClient, Response, ResultOverrideFunction - - -class GraphQLView(BaseGraphQLView): - result_override: ResultOverrideFunction = None - - def get_root_value(self, request: ChaliceRequest) -> Query: - return Query() - - def get_context( - self, request: ChaliceRequest, response: TemporalResponse - ) -> object: - context = super().get_context(request, response) - - return get_context(context) - - def process_result( - self, request: ChaliceRequest, result: ExecutionResult - ) -> GraphQLHTTPResponse: - if self.result_override: - return self.result_override(result) - - return super().process_result(request, result) - - -class ChaliceHttpClient(HttpClient): - def __init__( - self, - graphiql: bool = True, - allow_queries_via_get: bool = True, - result_override: ResultOverrideFunction = None, - ): - self.app = Chalice(app_name="TheStackBadger") - - view = GraphQLView( - schema=schema, - graphiql=graphiql, - allow_queries_via_get=allow_queries_via_get, - ) - view.result_override = result_override - - @self.app.route( - "/graphql", methods=["GET", "POST"], content_types=["application/json"] - ) - def handle_graphql(): - return view.execute_request(self.app.current_request) - - async def _graphql_request( - self, - method: Literal["get", "post"], - query: Optional[str] = None, - variables: Optional[Dict[str, object]] = None, - files: Optional[Dict[str, BytesIO]] = None, - headers: Optional[Dict[str, str]] = None, - **kwargs, - ) -> Response: - body = self._build_body( - query=query, variables=variables, files=files, method=method - ) - - data: Union[Dict[str, object], str, None] = None - - if body and files: - body.update({name: (file, name) for name, file in files.items()}) - - url = "/graphql" - - if method == "get": - body_encoded = urllib.parse.urlencode(body or {}) - url = f"{url}?{body_encoded}" - else: - if body: - data = body if files else dumps(body) - kwargs["body"] = data - - with Client(self.app) as client: - response = getattr(client.http, method)( - url, - headers=self._get_headers(method=method, headers=headers, files=files), - **kwargs, - ) - - return Response( - status_code=response.status_code, - data=response.body, - ) - - async def request( - self, - url: str, - method: Literal["get", "post", "patch", "put", "delete"], - headers: Optional[Dict[str, str]] = None, - ) -> Response: - with Client(self.app) as client: - response = getattr(client.http, method)(url, headers=headers) - - return Response( - status_code=response.status_code, - data=response.body, - ) - - async def get( - self, - url: str, - headers: Optional[Dict[str, str]] = None, - ) -> Response: - return await self.request(url, "get", headers=headers) - - async def post( - self, - url: str, - data: Optional[bytes] = None, - json: Optional[JSON] = None, - headers: Optional[Dict[str, str]] = None, - ) -> Response: - body = data or dumps(json) - - with Client(self.app) as client: - response = client.http.post(url, headers=headers, body=body) - - return Response( - status_code=response.status_code, - data=response.body, - ) diff --git a/tests/http/clients/django.py b/tests/http/clients/django.py deleted file mode 100644 index 47ef42673a..0000000000 --- a/tests/http/clients/django.py +++ /dev/null @@ -1,170 +0,0 @@ -from __future__ import annotations - -from io import BytesIO -from json import dumps -from typing import Dict, Optional, Union -from typing_extensions import Literal - -from django.core.exceptions import BadRequest, SuspiciousOperation -from django.core.files.uploadedfile import SimpleUploadedFile -from django.http import Http404, HttpRequest, HttpResponse -from django.test.client import RequestFactory - -from strawberry.django.views import GraphQLView as BaseGraphQLView -from strawberry.http import GraphQLHTTPResponse -from strawberry.types import ExecutionResult - -from ..context import get_context -from ..schema import Query, schema -from . import JSON, HttpClient, Response, ResultOverrideFunction - - -class GraphQLView(BaseGraphQLView): - result_override: ResultOverrideFunction = None - - def get_root_value(self, request): - return Query() - - def get_context(self, request: HttpRequest, response: HttpResponse) -> object: - context = {"request": request, "response": response} - - return get_context(context) - - def process_result( - self, request: HttpRequest, result: ExecutionResult - ) -> GraphQLHTTPResponse: - if self.result_override: - return self.result_override(result) - - return super().process_result(request, result) - - -class DjangoHttpClient(HttpClient): - def __init__( - self, - graphiql: bool = True, - allow_queries_via_get: bool = True, - result_override: ResultOverrideFunction = None, - ): - self.graphiql = graphiql - self.allow_queries_via_get = allow_queries_via_get - self.result_override = result_override - - def _get_header_name(self, key: str) -> str: - return f"HTTP_{key.upper().replace('-', '_')}" - - def _get_headers( - self, - method: Literal["get", "post"], - headers: Optional[Dict[str, str]], - files: Optional[Dict[str, BytesIO]], - ) -> Dict[str, str]: - headers = headers or {} - headers = {self._get_header_name(key): value for key, value in headers.items()} - - return super()._get_headers(method=method, headers=headers, files=files) - - async def _do_request(self, request: RequestFactory) -> Response: - view = GraphQLView.as_view( - schema=schema, - graphiql=self.graphiql, - allow_queries_via_get=self.allow_queries_via_get, - result_override=self.result_override, - ) - - try: - response = view(request) - except Http404: - return Response(status_code=404, data=b"Not found") - except (BadRequest, SuspiciousOperation) as e: - return Response(status_code=400, data=e.args[0].encode()) - else: - return Response(status_code=response.status_code, data=response.content) - - async def _graphql_request( - self, - method: Literal["get", "post"], - query: Optional[str] = None, - variables: Optional[Dict[str, object]] = None, - files: Optional[Dict[str, BytesIO]] = None, - headers: Optional[Dict[str, str]] = None, - **kwargs, - ) -> Response: - headers = self._get_headers(method=method, headers=headers, files=files) - additional_arguments = {**kwargs, **headers} - - body = self._build_body( - query=query, variables=variables, files=files, method=method - ) - - data: Union[Dict[str, object], str, None] = None - - if body and files: - files = { - name: SimpleUploadedFile(name, file.read()) - for name, file in files.items() - } - body.update(files) - else: - additional_arguments["content_type"] = "application/json" - - if body: - data = body if files or method == "get" else dumps(body) - - factory = RequestFactory() - request = getattr(factory, method)( - "/graphql", - data=data, - **additional_arguments, - ) - - return await self._do_request(request) - - async def request( - self, - url: str, - method: Literal["get", "post", "patch", "put", "delete"], - headers: Optional[Dict[str, str]] = None, - ) -> Response: - headers = self._get_headers( - method=method, # type: ignore - headers=headers, - files=None, - ) - - factory = RequestFactory() - request = getattr(factory, method)(url, **headers) - - return await self._do_request(request) - - async def get( - self, - url: str, - headers: Optional[Dict[str, str]] = None, - ) -> Response: - return await self.request(url, "get", headers=headers) - - async def post( - self, - url: str, - data: Optional[bytes] = None, - json: Optional[JSON] = None, - headers: Optional[Dict[str, str]] = None, - ) -> Response: - headers = self._get_headers(method="post", headers=headers, files=None) - - additional_arguments = {**headers} - - body = data or dumps(json) - - if headers.get("HTTP_CONTENT_TYPE"): - additional_arguments["content_type"] = headers["HTTP_CONTENT_TYPE"] - - factory = RequestFactory() - request = factory.post( - url, - data=body, - **additional_arguments, - ) - - return await self._do_request(request) diff --git a/tests/http/clients/fastapi.py b/tests/http/clients/fastapi.py deleted file mode 100644 index 747f9d886a..0000000000 --- a/tests/http/clients/fastapi.py +++ /dev/null @@ -1,139 +0,0 @@ -from __future__ import annotations - -import json -from io import BytesIO -from typing import Dict, Optional -from typing_extensions import Literal - -from fastapi import BackgroundTasks, Depends, FastAPI, Request, WebSocket -from fastapi.testclient import TestClient -from strawberry.fastapi import GraphQLRouter as BaseGraphQLRouter -from strawberry.http import GraphQLHTTPResponse -from strawberry.types import ExecutionResult - -from ..context import get_context -from ..schema import Query, schema -from . import JSON, HttpClient, Response, ResultOverrideFunction - - -def custom_context_dependency() -> str: - return "Hi!" - - -async def fastapi_get_context( - background_tasks: BackgroundTasks, - request: Request = None, - ws: WebSocket = None, - custom_value=Depends(custom_context_dependency), -): - return get_context( - { - "request": request or ws, - "background_tasks": background_tasks, - } - ) - - -async def get_root_value(request: Request = None, ws: WebSocket = None): - return Query() - - -class GraphQLRouter(BaseGraphQLRouter): - result_override: ResultOverrideFunction = None - - async def process_result( - self, request: Request, result: ExecutionResult - ) -> GraphQLHTTPResponse: - if self.result_override: - return self.result_override(result) - - return await super().process_result(request, result) - - -class FastAPIHttpClient(HttpClient): - def __init__( - self, - graphiql: bool = True, - allow_queries_via_get: bool = True, - result_override: ResultOverrideFunction = None, - ): - self.app = FastAPI() - - graphql_app = GraphQLRouter( - schema, - graphiql=graphiql, - context_getter=fastapi_get_context, - root_value_getter=get_root_value, - allow_queries_via_get=allow_queries_via_get, - ) - graphql_app.result_override = result_override - self.app.include_router(graphql_app, prefix="/graphql") - - self.client = TestClient(self.app) - - async def _graphql_request( - self, - method: Literal["get", "post"], - query: Optional[str] = None, - variables: Optional[Dict[str, object]] = None, - files: Optional[Dict[str, BytesIO]] = None, - headers: Optional[Dict[str, str]] = None, - **kwargs, - ) -> Response: - body = self._build_body( - query=query, variables=variables, files=files, method=method - ) - - if body: - if method == "get": - kwargs["params"] = body - else: - if files: - kwargs["data"] = body - else: - kwargs["content"] = json.dumps(body) - - if files: - kwargs["files"] = files - - response = getattr(self.client, method)( - "/graphql", - headers=self._get_headers(method=method, headers=headers, files=files), - **kwargs, - ) - - return Response(status_code=response.status_code, data=response.content) - - async def request( - self, - url: str, - method: Literal["get", "post", "patch", "put", "delete"], - headers: Optional[Dict[str, str]] = None, - ) -> Response: - response = getattr(self.client, method)(url, headers=headers) - - return Response( - status_code=response.status_code, - data=response.content, - ) - - async def get( - self, - url: str, - headers: Optional[Dict[str, str]] = None, - ) -> Response: - return await self.request(url, "get", headers=headers) - - async def post( - self, - url: str, - data: Optional[bytes] = None, - json: Optional[JSON] = None, - headers: Optional[Dict[str, str]] = None, - ) -> Response: - response = self.client.post(url, headers=headers, content=data, json=json) - - return Response( - status_code=response.status_code, - data=response.content, - ) diff --git a/tests/http/clients/flask.py b/tests/http/clients/flask.py deleted file mode 100644 index f34c6bd618..0000000000 --- a/tests/http/clients/flask.py +++ /dev/null @@ -1,148 +0,0 @@ -from __future__ import annotations - -import asyncio -import contextvars -import functools -import json -import urllib.parse -from io import BytesIO -from typing import Dict, Optional, Union -from typing_extensions import Literal - -from flask import Flask -from flask import Response as FlaskResponse -from strawberry.flask.views import GraphQLView as BaseGraphQLView -from strawberry.http import GraphQLHTTPResponse -from strawberry.types import ExecutionResult - -from ..context import get_context -from ..schema import Query, schema -from . import JSON, HttpClient, Response, ResultOverrideFunction - - -class GraphQLView(BaseGraphQLView): - # this allows to test our code path for checking the request type - # TODO: we might want to remove our check since it is done by flask - # already - methods = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD"] - - result_override: ResultOverrideFunction = None - - def __init__(self, *args, **kwargs): - self.result_override = kwargs.pop("result_override") - super().__init__(*args, **kwargs) - - def get_root_value(self): - return Query() - - def get_context(self, response: FlaskResponse) -> Dict[str, object]: - context = super().get_context(response) - - return get_context(context) - - def process_result(self, result: ExecutionResult) -> GraphQLHTTPResponse: - if self.result_override: - return self.result_override(result) - - return super().process_result(result) - - -class FlaskHttpClient(HttpClient): - def __init__( - self, - graphiql: bool = True, - allow_queries_via_get: bool = True, - result_override: ResultOverrideFunction = None, - ): - self.app = Flask(__name__) - self.app.debug = True - - view = GraphQLView.as_view( - "graphql_view", - schema=schema, - graphiql=graphiql, - allow_queries_via_get=allow_queries_via_get, - result_override=result_override, - ) - - self.app.add_url_rule( - "/graphql", - view_func=view, - ) - - async def _graphql_request( - self, - method: Literal["get", "post"], - query: Optional[str] = None, - variables: Optional[Dict[str, object]] = None, - files: Optional[Dict[str, BytesIO]] = None, - headers: Optional[Dict[str, str]] = None, - **kwargs, - ) -> Response: - body = self._build_body( - query=query, variables=variables, files=files, method=method - ) - - data: Union[Dict[str, object], str, None] = None - - if body and files: - body.update({name: (file, name) for name, file in files.items()}) - - url = "/graphql" - - if method == "get": - body_encoded = urllib.parse.urlencode(body or {}) - url = f"{url}?{body_encoded}" - else: - if body: - data = body if files else json.dumps(body) - kwargs["data"] = data - - headers = self._get_headers(method=method, headers=headers, files=files) - - return await self.request(url, method, headers=headers, **kwargs) - - def _do_request( - self, - url: str, - method: Literal["get", "post", "patch", "put", "delete"], - headers: Optional[Dict[str, str]] = None, - **kwargs, - ): - with self.app.test_client() as client: - response = getattr(client, method)(url, headers=headers, **kwargs) - - return Response( - status_code=response.status_code, - data=response.data, - ) - - async def request( - self, - url: str, - method: Literal["get", "post", "patch", "put", "delete"], - headers: Optional[Dict[str, str]] = None, - **kwargs, - ) -> Response: - loop = asyncio.get_running_loop() - ctx = contextvars.copy_context() - func_call = functools.partial( - ctx.run, self._do_request, url=url, method=method, headers=headers, **kwargs - ) - return await loop.run_in_executor(None, func_call) # type: ignore - - async def get( - self, - url: str, - headers: Optional[Dict[str, str]] = None, - ) -> Response: - return await self.request(url, "get", headers=headers) - - async def post( - self, - url: str, - data: Optional[bytes] = None, - json: Optional[JSON] = None, - headers: Optional[Dict[str, str]] = None, - ) -> Response: - return await self.request(url, "post", headers=headers, data=data, json=json) diff --git a/tests/http/clients/sanic.py b/tests/http/clients/sanic.py deleted file mode 100644 index 2d312193cd..0000000000 --- a/tests/http/clients/sanic.py +++ /dev/null @@ -1,133 +0,0 @@ -from __future__ import annotations - -from io import BytesIO -from json import dumps -from random import randint -from typing import Dict, Optional -from typing_extensions import Literal - -from sanic import Sanic -from sanic.request import Request as SanicRequest -from strawberry.http import GraphQLHTTPResponse -from strawberry.http.temporal_response import TemporalResponse -from strawberry.sanic.views import GraphQLView as BaseGraphQLView -from strawberry.types import ExecutionResult - -from ..context import get_context -from ..schema import Query, schema -from . import JSON, HttpClient, Response, ResultOverrideFunction - - -class GraphQLView(BaseGraphQLView): - result_override: ResultOverrideFunction = None - - def __init__(self, *args, **kwargs): - self.result_override = kwargs.pop("result_override") - super().__init__(*args, **kwargs) - - def get_root_value(self): - return Query() - - async def get_context( - self, request: SanicRequest, response: TemporalResponse - ) -> object: - context = {"request": request, "response": response} - - return get_context(context) - - async def process_result( - self, request: SanicRequest, result: ExecutionResult - ) -> GraphQLHTTPResponse: - if self.result_override: - return self.result_override(result) - - return await super().process_result(request, result) - - -class SanicHttpClient(HttpClient): - def __init__( - self, - graphiql: bool = True, - allow_queries_via_get: bool = True, - result_override: ResultOverrideFunction = None, - ): - self.app = Sanic( - f"test_{int(randint(0, 1000))}", - ) - view = GraphQLView.as_view( - schema=schema, - graphiql=graphiql, - allow_queries_via_get=allow_queries_via_get, - result_override=result_override, - ) - self.app.add_route( - view, - "/graphql", - ) - - async def _graphql_request( - self, - method: Literal["get", "post"], - query: Optional[str] = None, - variables: Optional[Dict[str, object]] = None, - files: Optional[Dict[str, BytesIO]] = None, - headers: Optional[Dict[str, str]] = None, - **kwargs, - ) -> Response: - body = self._build_body( - query=query, variables=variables, files=files, method=method - ) - - if body: - if method == "get": - kwargs["params"] = body - else: - if files: - kwargs["data"] = body - else: - kwargs["content"] = dumps(body) - - request, response = await self.app.asgi_client.request( - method, - "/graphql", - headers=self._get_headers(method=method, headers=headers, files=files), - files=files, - **kwargs, - ) - - return Response(status_code=response.status_code, data=response.content) - - async def request( - self, - url: str, - method: Literal["get", "post", "patch", "put", "delete"], - headers: Optional[Dict[str, str]] = None, - ) -> Response: - request, response = await self.app.asgi_client.request( - method, - url, - headers=headers, - ) - - return Response(status_code=response.status_code, data=response.content) - - async def get( - self, - url: str, - headers: Optional[Dict[str, str]] = None, - ) -> Response: - return await self.request(url, "get", headers=headers) - - async def post( - self, - url: str, - data: Optional[bytes] = None, - json: Optional[JSON] = None, - headers: Optional[Dict[str, str]] = None, - ) -> Response: - body = data or dumps(json) - request, response = await self.app.asgi_client.request( - "post", url, content=body, headers=headers - ) - - return Response(status_code=response.status_code, data=response.content) diff --git a/tests/http/clients/starlite.py b/tests/http/clients/starlite.py deleted file mode 100644 index 3e9f8cc397..0000000000 --- a/tests/http/clients/starlite.py +++ /dev/null @@ -1,125 +0,0 @@ -from __future__ import annotations - -import json -from io import BytesIO -from typing import Dict, Optional -from typing_extensions import Literal - -from starlite import Request, Starlite -from starlite.testing import TestClient -from strawberry.http import GraphQLHTTPResponse -from strawberry.starlite import make_graphql_controller -from strawberry.types import ExecutionResult - -from ..context import get_context -from ..schema import Query, schema -from . import JSON, HttpClient, Response, ResultOverrideFunction - - -def custom_context_dependency() -> str: - return "Hi!" - - -async def starlite_get_context(request: Request = None): - return get_context({"request": request}) - - -async def get_root_value(request: Request = None): - return Query() - - -class StarliteHttpClient(HttpClient): - def __init__( - self, - graphiql: bool = True, - allow_queries_via_get: bool = True, - result_override: ResultOverrideFunction = None, - ): - BaseGraphQLController = make_graphql_controller( - schema=schema, - path="/graphql", - graphiql=graphiql, - context_getter=starlite_get_context, - root_value_getter=get_root_value, - allow_queries_via_get=allow_queries_via_get, - ) - - class GraphQLController(BaseGraphQLController): - async def process_result( - self, result: ExecutionResult - ) -> GraphQLHTTPResponse: - if result_override: - return result_override(result) - - return await super().process_result(result) - - self.app = Starlite(route_handlers=[GraphQLController]) - - self.client = TestClient(self.app) - - async def _graphql_request( - self, - method: Literal["get", "post"], - query: Optional[str] = None, - variables: Optional[Dict[str, object]] = None, - files: Optional[Dict[str, BytesIO]] = None, - headers: Optional[Dict[str, str]] = None, - **kwargs, - ) -> Response: - body = self._build_body( - query=query, variables=variables, files=files, method=method - ) - - if body: - if method == "get": - kwargs["params"] = body - else: - if files: - kwargs["data"] = body - else: - kwargs["content"] = json.dumps(body) - - if files: - kwargs["files"] = files - - response = getattr(self.client, method)( - "/graphql", - headers=self._get_headers(method=method, headers=headers, files=files), - **kwargs, - ) - - return Response(status_code=response.status_code, data=response.content) - - async def request( - self, - url: str, - method: Literal["get", "post", "patch", "put", "delete"], - headers: Optional[Dict[str, str]] = None, - ) -> Response: - response = getattr(self.client, method)(url, headers=headers) - - return Response( - status_code=response.status_code, - data=response.content, - ) - - async def get( - self, - url: str, - headers: Optional[Dict[str, str]] = None, - ) -> Response: - return await self.request(url, "get", headers=headers) - - async def post( - self, - url: str, - data: Optional[bytes] = None, - json: Optional[JSON] = None, - headers: Optional[Dict[str, str]] = None, - ) -> Response: - response = self.client.post(url, headers=headers, content=data, json=json) - - return Response( - status_code=response.status_code, - data=response.content, - ) diff --git a/tests/http/conftest.py b/tests/http/conftest.py deleted file mode 100644 index 5372b6cf81..0000000000 --- a/tests/http/conftest.py +++ /dev/null @@ -1,49 +0,0 @@ -from typing import TYPE_CHECKING - -import pytest - -from . import IS_STARLITE_INSTALLED -from .clients import HttpClient -from .clients.aiohttp import AioHttpClient -from .clients.asgi import AsgiHttpClient -from .clients.async_django import AsyncDjangoHttpClient -from .clients.async_flask import AsyncFlaskHttpClient -from .clients.chalice import ChaliceHttpClient -from .clients.django import DjangoHttpClient -from .clients.fastapi import FastAPIHttpClient -from .clients.flask import FlaskHttpClient -from .clients.sanic import SanicHttpClient - -if TYPE_CHECKING: - from typing import Any, Dict, Type - - -_clients_dict: "Dict[Type[HttpClient], Any]" = { - AioHttpClient: pytest.param(AioHttpClient, marks=pytest.mark.aiohttp), - AsgiHttpClient: pytest.param(AsgiHttpClient, marks=pytest.mark.asgi), - AsyncDjangoHttpClient: pytest.param( - AsyncDjangoHttpClient, marks=pytest.mark.django - ), - AsyncFlaskHttpClient: pytest.param(AsyncFlaskHttpClient, marks=pytest.mark.flask), - ChaliceHttpClient: pytest.param(ChaliceHttpClient, marks=pytest.mark.chalice), - DjangoHttpClient: pytest.param(DjangoHttpClient, marks=pytest.mark.django), - FastAPIHttpClient: pytest.param(FastAPIHttpClient, marks=pytest.mark.fastapi), - FlaskHttpClient: pytest.param(FlaskHttpClient, marks=pytest.mark.flask), - SanicHttpClient: pytest.param(SanicHttpClient, marks=pytest.mark.sanic), -} - - -def pytest_generate_tests(metafunc: pytest.Metafunc) -> None: - if "http_client_class" in metafunc.fixturenames and IS_STARLITE_INSTALLED: - from .clients.starlite import StarliteHttpClient - - _clients_dict[StarliteHttpClient] = pytest.param( - StarliteHttpClient, marks=pytest.mark.starlite - ) - - metafunc.parametrize("http_client_class", _clients_dict.values()) - - -@pytest.fixture() -def http_client(http_client_class) -> HttpClient: - return http_client_class() diff --git a/tests/http/context.py b/tests/http/context.py deleted file mode 100644 index 99985b2434..0000000000 --- a/tests/http/context.py +++ /dev/null @@ -1,7 +0,0 @@ -from typing import Dict - - -def get_context(context: object) -> Dict[str, object]: - assert isinstance(context, dict) - - return {**context, "custom_value": "a value from context"} diff --git a/tests/http/schema.py b/tests/http/schema.py deleted file mode 100644 index 4c2d6897d3..0000000000 --- a/tests/http/schema.py +++ /dev/null @@ -1,91 +0,0 @@ -from typing import IO, List, Optional - -from starlette.datastructures import UploadFile as StarletteUploadFile - -import strawberry -from strawberry.file_uploads import Upload -from strawberry.permission import BasePermission -from strawberry.types import Info - -from . import IS_STARLITE_INSTALLED - - -def _read_file(text_file: Upload) -> str: - # allow to keep this function synchronous, starlette's files have - # async methods for reading - text_file: IO - if isinstance(text_file, StarletteUploadFile): - text_file = text_file.file._file # type: ignore - if IS_STARLITE_INSTALLED: - from starlite import UploadFile as StarliteUploadFile - - if isinstance(text_file, StarliteUploadFile): - text_file = text_file.file - - content = text_file.read() - - return content.decode() - - -@strawberry.input -class FolderInput: - files: List[Upload] - - -class AlwaysFailPermission(BasePermission): - message = "You are not authorized" - - def has_permission(self, source: object, info: Info, **kwargs) -> bool: - return False - - -@strawberry.type -class Query: - @strawberry.field - def hello(self, name: Optional[str] = None) -> str: - return f"Hello {name or 'world'}" - - @strawberry.field(permission_classes=[AlwaysFailPermission]) - def always_fail(self) -> Optional[str]: - return "Hey" - - @strawberry.field - def root_name(self) -> str: - return type(self).__name__ - - @strawberry.field - def value_from_context(self, info: Info) -> str: - return info.context["custom_value"] - - @strawberry.field - def returns_401(self, info: Info) -> str: - response = info.context["response"] - - if hasattr(response, "set_status"): - response.set_status(401) - else: - response.status_code = 401 - - return "hey" - - -@strawberry.type -class Mutation: - @strawberry.mutation - def hello(self) -> str: - return "strawberry" - - @strawberry.mutation - def read_text(self, text_file: Upload) -> str: - return _read_file(text_file) - - @strawberry.mutation - def read_files(self, files: List[Upload]) -> List[str]: - return [_read_file(file) for file in files] - - @strawberry.mutation - def read_folder(self, folder: FolderInput) -> List[str]: - return [_read_file(file) for file in folder.files] - - -schema = strawberry.Schema(query=Query, mutation=Mutation) diff --git a/tests/http/test_graphiql.py b/tests/http/test_graphiql.py deleted file mode 100644 index 25be6150c3..0000000000 --- a/tests/http/test_graphiql.py +++ /dev/null @@ -1,32 +0,0 @@ -from typing import Type - -import pytest - -from .clients import HttpClient - - -@pytest.mark.parametrize("header_value", ["text/html", "*/*"]) -async def test_renders_graphiql(header_value: str, http_client_class: Type[HttpClient]): - http_client = http_client_class() - response = await http_client.get("/graphql", headers={"Accept": header_value}) - - assert response.status_code == 200 - - assert "Strawberry GraphiQL" in response.text - - -async def test_does_not_render_graphiql_if_missing_accept( - http_client_class: Type[HttpClient], -): - http_client = http_client_class() - response = await http_client.get("/graphql", headers={"Accept": "text/xml"}) - - # TODO: django returns a 400, but we should be returning a 404 - assert response.status_code in (404, 400) - - -async def test_renders_graphiql_disabled(http_client_class: Type[HttpClient]): - http_client = http_client_class(graphiql=False) - response = await http_client.get("/graphql", headers={"Accept": "text/html"}) - - assert response.status_code == 404 diff --git a/tests/http/test_http.py b/tests/http/test_http.py deleted file mode 100644 index 0c316e3a7c..0000000000 --- a/tests/http/test_http.py +++ /dev/null @@ -1,13 +0,0 @@ -import pytest - -from .clients import HttpClient - - -@pytest.mark.parametrize("method", ["delete", "head", "put", "patch"]) -async def test_does_only_allow_get_and_post( - method: str, - http_client: HttpClient, -): - response = await http_client.request(url="/graphql", method=method) # type: ignore - - assert response.status_code == 405 diff --git a/tests/http/test_mutation.py b/tests/http/test_mutation.py deleted file mode 100644 index 920e606406..0000000000 --- a/tests/http/test_mutation.py +++ /dev/null @@ -1,14 +0,0 @@ -from .clients import HttpClient - - -async def test_mutation(http_client: HttpClient): - response = await http_client.query( - query="mutation { hello }", - headers={ - "Content-Type": "application/json", - }, - ) - data = response.json["data"] - - assert response.status_code == 200 - assert data["hello"] == "strawberry" diff --git a/tests/http/test_process_result.py b/tests/http/test_process_result.py deleted file mode 100644 index 6ecb71b4e6..0000000000 --- a/tests/http/test_process_result.py +++ /dev/null @@ -1,33 +0,0 @@ -from typing_extensions import Literal - -import pytest - -from strawberry.http import GraphQLHTTPResponse -from strawberry.types import ExecutionResult - -from .clients import HttpClient - - -def process_result(result: ExecutionResult) -> GraphQLHTTPResponse: - if result.data: - return { - "data": {key.upper(): result for key, result in result.data.items()}, - } - - return {} - - -@pytest.fixture() -def http_client(http_client_class) -> HttpClient: - return http_client_class(result_override=process_result) - - -@pytest.mark.parametrize("method", ["get", "post"]) -async def test_custom_process_result( - method: Literal["get", "post"], http_client: HttpClient -): - response = await http_client.query( - method=method, - query="{ hello }", - ) - assert response.json["data"] == {"HELLO": "Hello world"} diff --git a/tests/http/test_query.py b/tests/http/test_query.py deleted file mode 100644 index d9010a4b7b..0000000000 --- a/tests/http/test_query.py +++ /dev/null @@ -1,161 +0,0 @@ -from typing_extensions import Literal - -import pytest - -from .clients import HttpClient - - -@pytest.mark.parametrize("method", ["get", "post"]) -async def test_graphql_query(method: Literal["get", "post"], http_client: HttpClient): - response = await http_client.query( - method=method, - query="{ hello }", - ) - data = response.json["data"] - - assert response.status_code == 200 - assert data["hello"] == "Hello world" - - -@pytest.mark.parametrize("method", ["get", "post"]) -async def test_graphql_can_pass_variables( - method: Literal["get", "post"], http_client: HttpClient -): - response = await http_client.query( - method=method, - query="query hello($name: String!) { hello(name: $name) }", - variables={"name": "Jake"}, - ) - data = response.json["data"] - - assert response.status_code == 200 - assert data["hello"] == "Hello Jake" - - -@pytest.mark.parametrize("method", ["get", "post"]) -async def test_root_value(method: Literal["get", "post"], http_client: HttpClient): - response = await http_client.query( - method=method, - query="{ rootName }", - ) - data = response.json["data"] - - assert response.status_code == 200 - assert data["rootName"] == "Query" - - -@pytest.mark.parametrize("method", ["get", "post"]) -async def test_passing_invalid_query( - method: Literal["get", "post"], http_client: HttpClient -): - response = await http_client.query( - method=method, - query="{ h", - ) - - assert response.status_code == 200 - assert response.json["errors"] == [ - { - "message": "Syntax Error: Expected Name, found .", - "locations": [{"line": 1, "column": 4}], - } - ] - - -@pytest.mark.parametrize("method", ["get", "post"]) -async def test_returns_errors(method: Literal["get", "post"], http_client: HttpClient): - response = await http_client.query( - method=method, - query="{ maya }", - ) - - assert response.status_code == 200 - assert response.json["errors"] == [ - { - "message": "Cannot query field 'maya' on type 'Query'.", - "locations": [{"line": 1, "column": 3}], - } - ] - - -@pytest.mark.parametrize("method", ["get", "post"]) -async def test_returns_errors_and_data( - method: Literal["get", "post"], http_client: HttpClient -): - response = await http_client.query( - method=method, - query="{ hello, alwaysFail }", - ) - - assert response.status_code == 200 - data = response.json["data"] - errors = response.json["errors"] - - assert errors == [ - { - "locations": [{"column": 10, "line": 1}], - "message": "You are not authorized", - "path": ["alwaysFail"], - } - ] - assert data == {"hello": "Hello world", "alwaysFail": None} - - -async def test_passing_invalid_json_post(http_client: HttpClient): - response = await http_client.post( - url="/graphql", - data=b"{ h", - headers={"Content-Type": "application/json"}, - ) - - assert response.status_code == 400 - assert "Unable to parse request body as JSON" in response.text - - -async def test_passing_invalid_json_get(http_client: HttpClient): - response = await http_client.get( - url="/graphql?query={ hello }&variables='{'", - ) - - assert response.status_code == 400 - assert "Unable to parse request body as JSON" in response.text - - -async def test_missing_query(http_client: HttpClient): - response = await http_client.post( - url="/graphql", - json={}, - headers={"Content-Type": "application/json"}, - ) - - assert response.status_code == 400 - # TODO: consolidate this - assert ( - "No GraphQL query found in the request" in response.text - or "No valid query was provided for the request" in response.text - ) - - -@pytest.mark.parametrize("method", ["get", "post"]) -async def test_query_context(method: Literal["get", "post"], http_client: HttpClient): - response = await http_client.query( - method=method, - query="{ valueFromContext }", - ) - data = response.json["data"] - - assert response.status_code == 200 - assert data["valueFromContext"] == "a value from context" - - -@pytest.mark.parametrize("method", ["get", "post"]) -async def test_returning_status_code( - method: Literal["get", "post"], http_client: HttpClient -): - response = await http_client.query( - method=method, - query="{ returns401 }", - ) - - assert response.status_code == 401 - assert response.json == {"data": {"returns401": "hey"}} diff --git a/tests/http/test_query_via_get.py b/tests/http/test_query_via_get.py deleted file mode 100644 index aa033d23c4..0000000000 --- a/tests/http/test_query_via_get.py +++ /dev/null @@ -1,28 +0,0 @@ -from .clients import HttpClient - - -async def test_sending_empty_query(http_client_class): - http_client = http_client_class() - - response = await http_client.query( - method="get", query="", variables={"fake": "variable"} - ) - - assert response.status_code == 400 - assert "No GraphQL query found in the request" in response.text - - -async def test_does_not_allow_mutation(http_client: HttpClient): - response = await http_client.query(method="get", query="mutation { hello }") - - assert response.status_code == 400 - assert "mutations are not allowed when using GET" in response.text - - -async def test_fails_if_allow_queries_via_get_false(http_client_class): - http_client = http_client_class(allow_queries_via_get=False) - - response = await http_client.query(method="get", query="{ hello }") - - assert response.status_code == 400 - assert "queries are not allowed when using GET" in response.text diff --git a/tests/http/test_upload.py b/tests/http/test_upload.py deleted file mode 100644 index c75b39ec36..0000000000 --- a/tests/http/test_upload.py +++ /dev/null @@ -1,227 +0,0 @@ -import json -from io import BytesIO -from typing import Type - -import pytest - -import aiohttp - -from .clients import HttpClient -from .clients.chalice import ChaliceHttpClient - - -@pytest.fixture() -def http_client(http_client_class: Type[HttpClient]) -> HttpClient: - if http_client_class is ChaliceHttpClient: - pytest.xfail(reason="Chalice does not support uploads") - return http_client_class() - - -async def test_upload(http_client: HttpClient): - f = BytesIO(b"strawberry") - - query = """ - mutation($textFile: Upload!) { - readText(textFile: $textFile) - } - """ - - response = await http_client.query( - query, - variables={"textFile": None}, - files={"textFile": f}, - ) - - assert response.json == {"data": {"readText": "strawberry"}} - - -async def test_file_list_upload(http_client: HttpClient): - query = "mutation($files: [Upload!]!) { readFiles(files: $files) }" - file1 = BytesIO(b"strawberry1") - file2 = BytesIO(b"strawberry2") - - response = await http_client.query( - query=query, - variables={"files": [None, None]}, - files={"file1": file1, "file2": file2}, - ) - - data = response.json["data"] - - assert len(data["readFiles"]) == 2 - assert data["readFiles"][0] == "strawberry1" - assert data["readFiles"][1] == "strawberry2" - - -async def test_nested_file_list(http_client: HttpClient): - query = "mutation($folder: FolderInput!) { readFolder(folder: $folder) }" - file1 = BytesIO(b"strawberry1") - file2 = BytesIO(b"strawberry2") - - response = await http_client.query( - query=query, - variables={"folder": {"files": [None, None]}}, - files={"file1": file1, "file2": file2}, - ) - - data = response.json["data"] - assert len(data["readFolder"]) == 2 - assert data["readFolder"][0] == "strawberry1" - assert data["readFolder"][1] == "strawberry2" - - -async def test_upload_single_and_list_file_together(http_client: HttpClient): - query = """ - mutation($files: [Upload!]!, $textFile: Upload!) { - readFiles(files: $files) - readText(textFile: $textFile) - } - """ - file1 = BytesIO(b"strawberry1") - file2 = BytesIO(b"strawberry2") - file3 = BytesIO(b"strawberry3") - - response = await http_client.query( - query=query, - variables={"files": [None, None], "textFile": None}, - files={"file1": file1, "file2": file2, "textFile": file3}, - ) - - data = response.json["data"] - assert len(data["readFiles"]) == 2 - assert data["readFiles"][0] == "strawberry1" - assert data["readFiles"][1] == "strawberry2" - assert data["readText"] == "strawberry3" - - -async def test_upload_invalid_query(http_client: HttpClient): - f = BytesIO(b"strawberry") - - query = """ - mutation($textFile: Upload!) { - readT - """ - - response = await http_client.query( - query, - variables={"textFile": None}, - files={"textFile": f}, - ) - - assert response.status_code == 200 - assert response.json == { - "data": None, - "errors": [ - { - "locations": [{"column": 5, "line": 4}], - "message": "Syntax Error: Expected Name, found .", - } - ], - } - - -async def test_upload_missing_file(http_client: HttpClient): - f = BytesIO(b"strawberry") - - query = """ - mutation($textFile: Upload!) { - readText(textFile: $textFile) - } - """ - - response = await http_client.query( - query, - variables={"textFile": None}, - # using the wrong name to simulate a missing file - # this is to make it easier to run tests with our client - files={"a": f}, - ) - - assert response.status_code == 400 - assert "File(s) missing in form data" in response.text - - -class FakeWriter: - def __init__(self): - self.buffer = BytesIO() - - async def write(self, data: bytes): - self.buffer.write(data) - - @property - def value(self) -> bytes: - return self.buffer.getvalue() - - -async def test_extra_form_data_fields_are_ignored(http_client: HttpClient): - query = """mutation($textFile: Upload!) { - readText(textFile: $textFile) - }""" - - f = BytesIO(b"strawberry") - operations = json.dumps({"query": query, "variables": {"textFile": None}}) - file_map = json.dumps({"textFile": ["variables.textFile"]}) - extra_field_data = json.dumps({}) - - form_data = aiohttp.FormData() - form_data.add_field("textFile", f, filename="textFile.txt") - form_data.add_field("operations", operations) - form_data.add_field("map", file_map) - form_data.add_field("extra_field", extra_field_data) - - buffer = FakeWriter() - writer = form_data() - - await (writer.write(buffer)) # type: ignore - - response = await http_client.post( - url="/graphql", - data=buffer.value, - headers={"content-type": writer.content_type}, - ) - - assert response.status_code == 200 - assert response.json["data"] == {"readText": "strawberry"} - - -async def test_sending_invalid_form_data(http_client: HttpClient): - headers = {"content-type": "multipart/form-data; boundary=----fake"} - response = await http_client.post("/graphql", headers=headers) - - assert response.status_code == 400 - # TODO: can we consolidate this? - # - aiohttp returns "Unable to parse the multipart body" - # - fastapi returns "No valid query was provided for the request" - assert ( - "Unable to parse the multipart body" in response.text - or "No GraphQL query found in the request" in response.text - or "No valid query was provided for the request" in response.text - ) - - -async def test_sending_invalid_json_body(http_client: HttpClient): - f = BytesIO(b"strawberry") - operations = "}" - file_map = json.dumps({"textFile": ["variables.textFile"]}) - - form_data = aiohttp.FormData() - form_data.add_field("textFile", f, filename="textFile.txt") - form_data.add_field("operations", operations) - form_data.add_field("map", file_map) - - buffer = FakeWriter() - writer = form_data() - - await (writer.write(buffer)) # type: ignore - - response = await http_client.post( - "/graphql", - data=buffer.value, - headers={"content-type": writer.content_type}, - ) - - assert response.status_code == 400 - assert ( - "Unable to parse the multipart body" in response.text - or "Unable to parse request body as JSON" in response.text - ) diff --git a/tests/mypy/federation/test_decorators.yml b/tests/mypy/federation/test_decorators.yml deleted file mode 100644 index ec4218b893..0000000000 --- a/tests/mypy/federation/test_decorators.yml +++ /dev/null @@ -1,38 +0,0 @@ -- case: test_type - main: | - import strawberry - - @strawberry.federation.type - class User: - name: str - - User(name="Patrick") - User(n="Patrick") - out: | - main:8: error: Unexpected keyword argument "n" for "User" [call-arg] - -- case: test_input - main: | - import strawberry - - @strawberry.federation.input - class EditUserInput: - name: str - - EditUserInput(name="Patrick") - EditUserInput(n="Patrick") - out: | - main:8: error: Unexpected keyword argument "n" for "EditUserInput" [call-arg] - -- case: test_interface - main: | - import strawberry - - @strawberry.federation.interface - class NameInterface: - name: str - - NameInterface(name="Patrick") - NameInterface(n="Patrick") - out: | - main:8: error: Unexpected keyword argument "n" for "NameInterface" [call-arg] diff --git a/tests/mypy/federation/test_fields.yml b/tests/mypy/federation/test_fields.yml deleted file mode 100644 index 9b83024dd4..0000000000 --- a/tests/mypy/federation/test_fields.yml +++ /dev/null @@ -1,115 +0,0 @@ -- case: test_field - main: | - import strawberry - - @strawberry.federation.type - class User: - name: str = strawberry.federation.field(description="Example") - - User(name="Patrick") - User(n="Patrick") - out: | - main:8: error: Unexpected keyword argument "n" for "User" [call-arg] - -- case: test_all_field_usage - main: | - import strawberry - from strawberry.types import Info - - def some_resolver() -> str: - return "" - - def some_resolver_2(root: "Example") -> str: - return "" - - @strawberry.type - class Example: - a: str - b: str = strawberry.federation.field(name="b") - c: str = strawberry.federation.field(name="c", resolver=some_resolver) - d: str = strawberry.federation.field(resolver=some_resolver_2) - - @strawberry.federation.field(description="ABC") - def e(self, info: Info) -> str: - return "" - - @strawberry.federation.field(name="f") - def f_resolver(self, info) -> str: - return "" - - reveal_type(Example.a) - reveal_type(Example.b) - reveal_type(Example.c) - reveal_type(Example.d) - reveal_type(Example.e) - reveal_type(Example.f_resolver) - out: | - main:25: note: Revealed type is "builtins.str" - main:26: note: Revealed type is "builtins.str" - main:27: note: Revealed type is "builtins.str" - main:28: note: Revealed type is "builtins.str" - main:29: note: Revealed type is "Any" - main:30: note: Revealed type is "Any" - -- case: test_private_field - main: | - import strawberry - - @strawberry.type - class User: - age: strawberry.Private[int] - - @strawberry.field - def age_in_months(self) -> int: - return self.age * 12 - - @strawberry.field - def wrong_type(self) -> int: - reveal_type(self.age) - return self.age.trim() - out: | - main:13: note: Revealed type is "builtins.int" - main:14: error: "int" has no attribute "trim" [attr-defined] - -- case: test_field_with_default_before_non_default - main: | - import strawberry - - @strawberry.type - class Example: - a: str = "ABC" - b: str - - out: | - -- case: test_using_strawberry_field_does_not_break - main: | - import strawberry - - @strawberry.type - class Example: - a: str = strawberry.federation.field(description="Example") - b: str - - reveal_type(Example.a) - out: | - main:8: note: Revealed type is "builtins.str" - -- case: test_does_not_put_fields_with_resolver_in_init - main: | - import strawberry - - def resolver() -> str: - return "hi" - - @strawberry.type - class Example: - a: str = strawberry.federation.field(description="Example") - b: str = strawberry.federation.field(resolver=resolver) - c: str - - i = Example(a="a", c="c") - - reveal_type(i.a) - out: | - main:14: note: Revealed type is "builtins.str" diff --git a/tests/mypy/test_dataloaders.yml b/tests/mypy/test_dataloaders.yml index aa70ab5ffb..35997d2b19 100644 --- a/tests/mypy/test_dataloaders.yml +++ b/tests/mypy/test_dataloaders.yml @@ -26,49 +26,7 @@ async def load(keys: List[int]) -> List[str]: return [str(k) for k in keys] - loader = DataLoader(load) - - reveal_type(loader) - - async def run() -> None: - user = await loader.load(1) - - reveal_type(user) - out: | - main:10: note: Revealed type is "strawberry.dataloader.DataLoader[builtins.int, builtins.str]" - main:15: note: Revealed type is "builtins.str" - -- case: test_dataloader_exception - main: | - from typing import List, Union - - from strawberry.dataloader import DataLoader - - async def load(keys: List[int]) -> List[Union[str, ValueError, TypeError]]: - return [ValueError("x") for k in keys] - - loader = DataLoader(load) - - reveal_type(loader) - - async def run() -> None: - user = await loader.load(1) - - reveal_type(user) - out: | - main:10: note: Revealed type is "strawberry.dataloader.DataLoader[builtins.int, builtins.str]" - main:15: note: Revealed type is "builtins.str" - -- case: test_dataloader_optional - main: | - from typing import List, Optional - - from strawberry.dataloader import DataLoader - - async def load(keys: List[int]) -> List[Optional[str]]: - return [None for k in keys] - - loader = DataLoader(load) + loader = DataLoader[int, str](load) reveal_type(loader) @@ -77,5 +35,5 @@ reveal_type(user) out: | - main:10: note: Revealed type is "strawberry.dataloader.DataLoader[builtins.int, Union[builtins.str, None]]" - main:15: note: Revealed type is "Union[builtins.str, None]" + main:10: note: Revealed type is "strawberry.dataloader.DataLoader[builtins.int*, builtins.str*]" + main:15: note: Revealed type is "builtins.str*" diff --git a/tests/mypy/test_decorators.yml b/tests/mypy/test_decorators.yml index 37fccc8bbb..0df33d327f 100644 --- a/tests/mypy/test_decorators.yml +++ b/tests/mypy/test_decorators.yml @@ -9,7 +9,7 @@ User(name="Patrick") User(n="Patrick") out: | - main:8: error: Unexpected keyword argument "n" for "User" [call-arg] + main:8: error: Unexpected keyword argument "n" for "User" - case: test_input main: | @@ -22,7 +22,7 @@ EditUserInput(name="Patrick") EditUserInput(n="Patrick") out: | - main:8: error: Unexpected keyword argument "n" for "EditUserInput" [call-arg] + main:8: error: Unexpected keyword argument "n" for "EditUserInput" - case: test_interface main: | @@ -35,4 +35,30 @@ NameInterface(name="Patrick") NameInterface(n="Patrick") out: | - main:8: error: Unexpected keyword argument "n" for "NameInterface" [call-arg] + main:8: error: Unexpected keyword argument "n" for "NameInterface" + +- case: test_federation_type + main: | + import strawberry + + @strawberry.federation.type + class User: + name: str + + User(name="Patrick") + User(n="Patrick") + out: | + main:8: error: Unexpected keyword argument "n" for "User" + +- case: test_federation_input + main: | + import strawberry + + @strawberry.input + class EditUserInput: + name: str + + EditUserInput(name="Patrick") + EditUserInput(n="Patrick") + out: | + main:8: error: Unexpected keyword argument "n" for "EditUserInput" diff --git a/tests/mypy/test_enum.yml b/tests/mypy/test_enum.yml index 514430dc9a..79dda9ec89 100644 --- a/tests/mypy/test_enum.yml +++ b/tests/mypy/test_enum.yml @@ -82,7 +82,7 @@ reveal_type(a) reveal_type(IceCreamFlavour.VANILLA) out: | - main:12: note: Revealed type is "def (value: builtins.object) -> main.IceCreamFlavour" + main:12: note: Revealed type is "def (value: builtins.object) -> main.IceCreamFlavour*" main:13: note: Revealed type is "main.IceCreamFlavour" main:14: note: Revealed type is "Literal[main.IceCreamFlavour.VANILLA]?" - case: test_enum_with_decorator_and_name @@ -102,7 +102,7 @@ reveal_type(a) reveal_type(Flavour.VANILLA) out: | - main:12: note: Revealed type is "def (value: builtins.object) -> main.Flavour" + main:12: note: Revealed type is "def (value: builtins.object) -> main.Flavour*" main:13: note: Revealed type is "main.Flavour" main:14: note: Revealed type is "Literal[main.Flavour.VANILLA]?" - case: test_enum_with_manual_decorator @@ -119,7 +119,7 @@ reveal_type(strawberry.enum(IceCreamFlavour)) reveal_type(strawberry.enum(IceCreamFlavour).VANILLA) out: | - main:10: note: Revealed type is "def (value: builtins.object) -> main.IceCreamFlavour" + main:10: note: Revealed type is "def (value: builtins.object) -> main.IceCreamFlavour*" main:11: note: Revealed type is "Literal[main.IceCreamFlavour.VANILLA]?" - case: test_enum_with_manual_decorator_and_name main: | @@ -135,23 +135,5 @@ reveal_type(strawberry.enum(name="IceCreamFlavour")(Flavour)) reveal_type(strawberry.enum(name="IceCreamFlavour")(Flavour).VANILLA) out: | - main:10: note: Revealed type is "def (value: builtins.object) -> main.Flavour" + main:10: note: Revealed type is "def (value: builtins.object) -> main.Flavour*" main:11: note: Revealed type is "Literal[main.Flavour.VANILLA]?" -- case: test_enum_with_deprecation_reason - main: | - from enum import Enum - - import strawberry - - class Flavour(Enum): - VANILLA = strawberry.enum_value("vanilla") - STRAWBERRY = strawberry.enum_value( - "strawberry", deprecation_reason="We ran out" - ) - CHOCOLATE = "chocolate" - - reveal_type(strawberry.enum(name="IceCreamFlavour")(Flavour)) - reveal_type(strawberry.enum(name="IceCreamFlavour")(Flavour).STRAWBERRY) - out: | - main:12: note: Revealed type is "def (value: builtins.object) -> main.Flavour" - main:13: note: Revealed type is "Literal[main.Flavour.STRAWBERRY]?" diff --git a/tests/mypy/test_fields.yml b/tests/mypy/test_fields.yml index d342961e84..b29f0bd035 100644 --- a/tests/mypy/test_fields.yml +++ b/tests/mypy/test_fields.yml @@ -9,7 +9,7 @@ User(name="Patrick") User(n="Patrick") out: | - main:8: error: Unexpected keyword argument "n" for "User" [call-arg] + main:8: error: Unexpected keyword argument "n" for "User" - case: test_all_field_usage main: | @@ -66,9 +66,9 @@ return self.age.trim() out: | main:13: note: Revealed type is "builtins.int" - main:14: error: "int" has no attribute "trim" [attr-defined] + main:14: error: "int" has no attribute "trim" -- case: test_field_with_default_before_non_default +- case: test_field_with_default_cannot_be_before_non_default main: | import strawberry @@ -78,6 +78,7 @@ b: str out: | + main:6: error: Attributes without a default cannot follow attributes with one - case: test_using_strawberry_field_does_not_break main: | @@ -110,3 +111,16 @@ reveal_type(i.a) out: | main:14: note: Revealed type is "builtins.str" + +- case: test_using_strawberry_federation_field_does_not_break + main: | + import strawberry + + @strawberry.type + class Example: + a: str = strawberry.federation.field(description="Example") + b: str + + reveal_type(Example.a) + out: | + main:8: note: Revealed type is "builtins.str" diff --git a/tests/mypy/test_mutation.yml b/tests/mypy/test_mutation.yml deleted file mode 100644 index f95160ae89..0000000000 --- a/tests/mypy/test_mutation.yml +++ /dev/null @@ -1,46 +0,0 @@ -- case: test_mutation_decorator - main: | - import strawberry - - @strawberry.type - class Mutation: - @strawberry.mutation - def set_name(self, name: str) -> None: - self.name = name - - Mutation() - Mutation(n="Patrick") - out: | - main:10: error: Unexpected keyword argument "n" for "Mutation" [call-arg] - - -- case: test_mutation_field - main: | - import strawberry - - def set_name(self, name: str) -> None: - self.name = name - - @strawberry.type - class Mutation: - set_name: None = strawberry.mutation(resolver=set_name) - - Mutation() - Mutation(n="Patrick") - out: | - main:11: error: Unexpected keyword argument "n" for "Mutation" [call-arg] - - -- case: test_positional_arguments_in_mutation - main: | - import strawberry - - def set_name(self, name: str) -> None: - self.name = name - - @strawberry.type - class Example: - passed: None = strawberry.mutation(resolver=set_name) - failed: None = strawberry.mutation(set_name) - out: | - main:9: error: "field()" or "mutation()" only takes keyword arguments [misc] diff --git a/tests/mypy/test_plugin.py b/tests/mypy/test_plugin.py deleted file mode 100644 index 95995a8211..0000000000 --- a/tests/mypy/test_plugin.py +++ /dev/null @@ -1,40 +0,0 @@ -from decimal import Decimal - -import pytest - -from strawberry.ext.mypy_plugin import FALLBACK_VERSION, MypyVersion, plugin - -pytestmark = pytest.mark.usefixtures("maintain_version") - - -@pytest.fixture -def maintain_version(): - """Clean-up side-effected version after tests""" - - yield - - del MypyVersion.VERSION - - -@pytest.mark.parametrize( - ("version", "expected"), - [ - ("0.93", Decimal("0.93")), - ("0.800", Decimal("0.800")), - ("0.920", Decimal("0.920")), - ("0.980+dev.d89b28d973c3036ef154c9551b961d9119761380", Decimal("0.980")), - ("1.0.0", Decimal("1.0")), - ("99.999", Decimal("99.999")), - ], -) -def test_plugin(version, expected): - - plugin(version) - assert MypyVersion.VERSION == expected - - -def test_plugin_negative(): - invalid_version = "001.290" - with pytest.warns(UserWarning, match=f"Mypy version {invalid_version} could not"): - plugin(invalid_version) - assert MypyVersion.VERSION == FALLBACK_VERSION diff --git a/tests/mypy/test_pydantic.decorators.yml b/tests/mypy/test_pydantic.decorators.yml index aac89c011f..f7dd24d3b5 100644 --- a/tests/mypy/test_pydantic.decorators.yml +++ b/tests/mypy/test_pydantic.decorators.yml @@ -49,69 +49,6 @@ out: | main:11: note: Revealed type is "main.UserStrawberry" -- case: test_converted_to_pydantic_input - main: | - from pydantic import BaseModel - import strawberry - - class UserPydantic(BaseModel): - age: int - - @strawberry.experimental.pydantic.input(UserPydantic) - class UserStrawberry: - age: strawberry.auto - - reveal_type(UserStrawberry(age=123).to_pydantic()) - out: | - main:11: note: Revealed type is "main.UserPydantic" - -- case: test_converted_from_pydantic_input - main: | - from pydantic import BaseModel - import strawberry - - class UserPydantic(BaseModel): - age: int - - @strawberry.experimental.pydantic.input(UserPydantic) - class UserStrawberry: - age: strawberry.auto - - reveal_type(UserStrawberry.from_pydantic(UserPydantic(age=123))) - out: | - main:11: note: Revealed type is "main.UserStrawberry" - -- case: test_converted_to_pydantic_interface - main: | - from pydantic import BaseModel - import strawberry - - class UserPydantic(BaseModel): - age: int - - @strawberry.experimental.pydantic.interface(UserPydantic) - class UserStrawberry: - age: strawberry.auto - - reveal_type(UserStrawberry(age=123).to_pydantic()) - out: | - main:11: note: Revealed type is "main.UserPydantic" - -- case: test_converted_from_pydantic_interface - main: | - from pydantic import BaseModel - import strawberry - - class UserPydantic(BaseModel): - age: int - - @strawberry.experimental.pydantic.interface(UserPydantic) - class UserStrawberry: - age: strawberry.auto - - reveal_type(UserStrawberry.from_pydantic(UserPydantic(age=123))) - out: | - main:11: note: Revealed type is "main.UserStrawberry" - case: test_converted_from_pydantic_raise_error_wrong_instance main: | @@ -130,7 +67,7 @@ UserStrawberry.from_pydantic(AnotherModel(age=123)) out: | - main:14: error: Argument 1 to "from_pydantic" of "UserStrawberry" has incompatible type "AnotherModel"; expected "UserPydantic" [arg-type] + main:14: error: Argument 1 to "from_pydantic" of "UserStrawberry" has incompatible type "AnotherModel"; expected "UserPydantic" - case: test_converted_from_pydantic_chained main: | @@ -147,141 +84,3 @@ reveal_type(UserStrawberry.from_pydantic(UserPydantic(age=123)).to_pydantic()) out: | main:11: note: Revealed type is "main.UserPydantic" - -- case: test_to_pydantic_kwargs - main: | - from pydantic import BaseModel - import strawberry - - - class MyModel(BaseModel): - email: str - password: str - - - @strawberry.experimental.pydantic.input(model=MyModel) - class MyModelStrawberry: - email: strawberry.auto - - - MyModelStrawberry(email="").to_pydantic() - out: | - main:15: error: Missing named argument "password" for "to_pydantic" of "MyModelStrawberry" [call-arg] -- case: test_to_pydantic_kwargs_with_defaults - main: | - from pydantic import BaseModel - import strawberry - - - class MyModel(BaseModel): - email: str - password: str = "hunter2" - - - @strawberry.experimental.pydantic.input(model=MyModel) - class MyModelStrawberry: - email: strawberry.auto - - - MyModelStrawberry(email="").to_pydantic() - out: | -- case: test_to_pydantic_kwargs_with_optional_default - main: | - from pydantic import BaseModel - from typing import Optional - import strawberry - - - class MyModel(BaseModel): - email: str - password: Optional[str] # pydantic makes this implicit = None default - - - @strawberry.experimental.pydantic.input(model=MyModel) - class MyModelStrawberry: - email: strawberry.auto - - - MyModelStrawberry(email="").to_pydantic() - out: | -- case: test_to_pydantic_kwargs_with_all_fields - main: | - from pydantic import BaseModel - from typing import Optional - import strawberry - - - class MyModel(BaseModel): - email: str - password: Optional[str] - - - @strawberry.experimental.pydantic.input(model=MyModel, all_fields=True) - class MyModelStrawberry: - ... - - - MyModelStrawberry(email="").to_pydantic() - out: | -- case: test_to_pydantic_kwargs_with_all_fields_adding_field - main: | - from pydantic import BaseModel - from typing import Optional - import strawberry - - - class MyModel(BaseModel): - email: str - password: Optional[str] - - @property - def age(self) -> int: - return 42 - - - @strawberry.experimental.pydantic.input(model=MyModel, all_fields=True) - class MyModelStrawberry: - age: int # read from the property - - - MyModelStrawberry(email="").to_pydantic() - out: | -- case: test_to_pydantic_kwargs_private_field - main: | - from pydantic import BaseModel - from typing import Optional - import strawberry - - - class MyModel(BaseModel): - email: str - _age: int # for pydantic, underscore means private field - - - @strawberry.experimental.pydantic.input(model=MyModel) - class MyModelStrawberry: - email: strawberry.auto - - - MyModelStrawberry(email="").to_pydantic() - out: | -- case: test_to_pydantic_custom - main: | - from pydantic import BaseModel - import strawberry - - class MyModel(BaseModel): - email: str - - @strawberry.experimental.pydantic.input(model=MyModel) - class MyModelStrawberry: - email: strawberry.auto - - def to_pydantic(self, another_param: str): - # custom to_pydantic overwrites default params - ... - - MyModelStrawberry(email="").to_pydantic() - - out: | - main:15: error: Missing positional argument "another_param" in call to "to_pydantic" of "MyModelStrawberry" [call-arg] diff --git a/tests/mypy/test_scalar.yml b/tests/mypy/test_scalar.yml deleted file mode 100644 index 69516caf98..0000000000 --- a/tests/mypy/test_scalar.yml +++ /dev/null @@ -1,93 +0,0 @@ -- case: test_scalar_decorator - main: | - import strawberry - - @strawberry.scalar() - class X: - pass - - @strawberry.scalar - class Y: - pass - - @strawberry.scalar(name="Zed") - class Z: - pass - - @strawberry.type - class Me: - z: Z - - reveal_type(X) - reveal_type(Y) - reveal_type(Z) - reveal_type(X()) - reveal_type(Y()) - reveal_type(Z()) - reveal_type(Me(z=Z()).z) - out: | - main:19: note: Revealed type is "def () -> main.X" - main:20: note: Revealed type is "def () -> main.Y" - main:21: note: Revealed type is "def () -> main.Z" - main:22: note: Revealed type is "main.X" - main:23: note: Revealed type is "main.Y" - main:24: note: Revealed type is "main.Z" - main:25: note: Revealed type is "main.Z" - -- case: test_scalar_as_function - main: | - import strawberry - - X = strawberry.scalar(int) - Y = strawberry.scalar(str, name="Y") - - @strawberry.type - class Me: - x: X - - reveal_type(X()) - reveal_type(Y()) - reveal_type(Me(x=X("1")).x) - out: | - main:10: note: Revealed type is "builtins.int" - main:11: note: Revealed type is "builtins.str" - main:12: note: Revealed type is "builtins.int" - -- case: test_scalar_as_function_new_type - main: | - from typing import NewType - import strawberry - - Z = strawberry.scalar(NewType("X", int)) - - @strawberry.type - class Me: - x: Z - - reveal_type(Z()) - reveal_type(Me(x=Z("1")).x) - out: | - main:10: note: Revealed type is "Any" - main:11: note: Revealed type is "Any" - -- case: test_we_can_pass_scalar_overrides_to_schema - main: | - import strawberry - from datetime import datetime, timezone - - EpochDateTime = strawberry.scalar( - datetime, - ) - - @strawberry.type - class Query: - a: datetime - - schema = strawberry.Schema(query=Query, scalar_overrides={ - datetime: EpochDateTime, - }) - - reveal_type(EpochDateTime) - - out: | - main:16: note: Revealed type is "def (year: builtins.int, month: builtins.int, day: builtins.int, hour: builtins.int =, minute: builtins.int =, second: builtins.int =, microsecond: builtins.int =, tzinfo: Union[datetime.tzinfo, None] =, *, fold: builtins.int =) -> datetime.datetime" diff --git a/tests/mypy/test_union.yml b/tests/mypy/test_union.yml index d3c140bc0d..cdc7a9f401 100644 --- a/tests/mypy/test_union.yml +++ b/tests/mypy/test_union.yml @@ -18,7 +18,7 @@ reveal_type(Response) reveal_type(a) - a = User(name="abc") + a = User("abc") reveal_type(a) out: | main:16: note: Revealed type is "builtins.object" @@ -70,7 +70,7 @@ reveal_type(Response) reveal_type(a) - a = User(name="abc") + a = User("abc") reveal_type(a) out: | main:16: note: Revealed type is "builtins.object" diff --git a/tests/mypy/test_unset.yml b/tests/mypy/test_unset.yml index b1ae0f28f3..810fceff5f 100644 --- a/tests/mypy/test_unset.yml +++ b/tests/mypy/test_unset.yml @@ -2,7 +2,7 @@ main: | import strawberry from typing import Optional - from strawberry import UNSET + from strawberry.arguments import UNSET @strawberry.type class User: diff --git a/tests/objects/generics/test_generic_objects.py b/tests/objects/generics/test_generic_objects.py index c373aa5353..c572a0dcb4 100644 --- a/tests/objects/generics/test_generic_objects.py +++ b/tests/objects/generics/test_generic_objects.py @@ -7,6 +7,7 @@ from strawberry.type import StrawberryList, StrawberryOptional, StrawberryTypeVar from strawberry.union import StrawberryUnion + T = TypeVar("T") diff --git a/tests/objects/generics/test_names.py b/tests/objects/generics/test_names.py index c80bddd71b..9dd55ed0bc 100644 --- a/tests/objects/generics/test_names.py +++ b/tests/objects/generics/test_names.py @@ -1,6 +1,5 @@ import textwrap from typing import Generic, List, NewType, TypeVar -from typing_extensions import Annotated import pytest @@ -11,6 +10,7 @@ from strawberry.type import StrawberryList, StrawberryOptional from strawberry.union import StrawberryUnion + T = TypeVar("T") K = TypeVar("K") V = TypeVar("V") @@ -31,22 +31,18 @@ class TypeB: @pytest.mark.parametrize( - ("types", "expected_name"), + "types,expected_name", [ ([StrawberryList(str)], "StrListExample"), ([StrawberryList(StrawberryList(str))], "StrListListExample"), ([StrawberryOptional(StrawberryList(str))], "StrListOptionalExample"), ([StrawberryList(StrawberryOptional(str))], "StrOptionalListExample"), ([StrawberryList(Enum)], "EnumListExample"), - ([StrawberryUnion("Union", (TypeA, TypeB))], "UnionExample"), # pyright: ignore + ([StrawberryUnion("Union", (TypeA, TypeB))], "UnionExample"), # type: ignore ([TypeA], "TypeAExample"), ([CustomInt], "CustomIntExample"), ([TypeA, TypeB], "TypeATypeBExample"), ([TypeA, LazyType["TypeB", "test_names"]], "TypeATypeBExample"), # type: ignore - ( - [TypeA, Annotated["TypeB", strawberry.lazy("test_names")]], - "TypeATypeBExample", - ), ], ) def test_name_generation(types, expected_name): diff --git a/tests/objects/test_types.py b/tests/objects/test_types.py index e62fe12c27..c91fc60707 100644 --- a/tests/objects/test_types.py +++ b/tests/objects/test_types.py @@ -4,40 +4,40 @@ from strawberry.exceptions import ObjectIsNotClassError -@pytest.mark.raises_strawberry_exception( - ObjectIsNotClassError, - match=( +def test_raises_error_when_using_type_with_a_not_class_object(): + expected_error = ( r"strawberry.type can only be used with class types. Provided " r"object .* is not a type." - ), -) -def test_raises_error_when_using_type_with_a_not_class_object(): - @strawberry.type - def not_a_class(): - pass + ) + + with pytest.raises(ObjectIsNotClassError, match=expected_error): + @strawberry.type + def not_a_class(): + pass -@pytest.mark.raises_strawberry_exception( - ObjectIsNotClassError, - match=( + +def test_raises_error_when_using_input_with_a_not_class_object(): + expected_error = ( r"strawberry.input can only be used with class types. Provided " r"object .* is not a type." - ), -) -def test_raises_error_when_using_input_with_a_not_class_object(): - @strawberry.input - def not_a_class(): - pass + ) + with pytest.raises(ObjectIsNotClassError, match=expected_error): -@pytest.mark.raises_strawberry_exception( - ObjectIsNotClassError, - match=( + @strawberry.input + def not_a_class(): + pass + + +def test_raises_error_when_using_interface_with_a_not_class_object(): + expected_error = ( r"strawberry.interface can only be used with class types. Provided " r"object .* is not a type." - ), -) -def test_raises_error_when_using_interface_with_a_not_class_object(): - @strawberry.interface - def not_a_class(): - pass + ) + + with pytest.raises(ObjectIsNotClassError, match=expected_error): + + @strawberry.interface + def not_a_class(): + pass diff --git a/tests/plugins/__init__.py b/tests/plugins/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/plugins/strawberry_exceptions.py b/tests/plugins/strawberry_exceptions.py deleted file mode 100644 index fce0e59637..0000000000 --- a/tests/plugins/strawberry_exceptions.py +++ /dev/null @@ -1,172 +0,0 @@ -import contextlib -import os -import re -from collections import defaultdict -from dataclasses import dataclass -from pathlib import Path -from typing import DefaultDict, Generator, List, Type - -import pytest -import rich -import rich.console -from _pytest.nodes import Item -from pluggy._result import _Result -from rich.traceback import Traceback - -from strawberry.exceptions import StrawberryException, UnableToFindExceptionSource - -WORKSPACE_FOLDER = Path(__file__).parents[2] -DOCS_FOLDER = WORKSPACE_FOLDER / "docs/errors" - - -@dataclass -class Result: - text: str - raised_exception: StrawberryException - - -@contextlib.contextmanager -def suppress_output(verbosity_level: int = 0) -> Generator[None, None, None]: - if verbosity_level >= 2: - yield - - return - - with Path(os.devnull).open("w") as devnull: - with contextlib.redirect_stdout(devnull): - yield - - -class StrawberryExceptionsPlugin: - def __init__(self, verbosity_level: int) -> None: - self._info: DefaultDict[Type[StrawberryException], List[Result]] = defaultdict( - list - ) - self.verbosity_level = verbosity_level - - @pytest.hookimpl(hookwrapper=True) - def pytest_runtest_call(self, item: Item) -> Generator[None, _Result, None]: - __tracebackhide__ = True - - outcome = yield - - self._check_strawberry_exception(item, outcome) - - def _check_strawberry_exception(self, item: Item, outcome: _Result) -> None: - __tracebackhide__ = True - - raises_marker = item.get_closest_marker("raises_strawberry_exception") - - if raises_marker is None: - return - - exception = raises_marker.args[0] - match = raises_marker.kwargs.get("match", None) - - if not issubclass(exception, StrawberryException): - pytest.fail(f"{exception} is not a StrawberryException") - - raised_exception = outcome.excinfo[1] if outcome.excinfo else None - - # This plugin needs to work around the other hooks, see: - # https://docs.pytest.org/en/7.1.x/how-to/writing_hook_functions.html#hookwrapper-executing-around-other-hooks - outcome.force_result(None) - - if raised_exception is None: - failure_message = f"Expected exception {exception}, but it did not raise" - - pytest.fail(failure_message, pytrace=False) - - if not isinstance(raised_exception, exception): - failure_message = ( - f"Expected exception {exception}, but raised {raised_exception}" - ) - - raise raised_exception - - raised_message = str(raised_exception) - - self._collect_exception(item.name, raised_exception) - - if match is not None and not re.match(match, raised_message): - failure_message = ( - f'"{match}" does not match raised message "{raised_message}"' - ) - - if self.verbosity_level >= 1: - print(f"Exception: {exception}") # noqa: T201 - - pytest.fail(failure_message, pytrace=False) - - def _collect_exception( - self, test_name: str, raised_exception: StrawberryException - ) -> None: - console = rich.console.Console(record=True, width=120) - - with suppress_output(self.verbosity_level): - try: - console.print(raised_exception) - except UnableToFindExceptionSource: - traceback = Traceback( - Traceback.extract( - raised_exception.__class__, - raised_exception, - raised_exception.__traceback__, - ), - max_frames=10, - ) - console.print(traceback) - - exception_text = console.export_text() - - text = f"## {test_name}\n" - - if exception_text.strip() == "None": - text += "No exception raised\n" - else: - text += f"\n``````\n{exception_text.strip()}\n``````\n\n" - - documentation_path = DOCS_FOLDER / f"{raised_exception.documentation_path}.md" - - if not documentation_path.exists(): - pytest.fail( - f"{documentation_path.relative_to(WORKSPACE_FOLDER)} does not exist", - pytrace=False, - ) - - self._info[raised_exception.__class__].append( - Result(text=text, raised_exception=raised_exception) - ) - - def pytest_sessionfinish(self): - summary_path = os.environ.get("GITHUB_STEP_SUMMARY", None) - - if not summary_path: - return - - markdown = "" - - for exception_class, info in self._info.items(): - title = " ".join(re.findall("[a-zA-Z][^A-Z]*", exception_class.__name__)) - - markdown += f"# {title}\n\n" - markdown += ( - f"Documentation URL: {info[0].raised_exception.documentation_url}\n\n" - ) - - markdown += "\n".join([result.text for result in info]) - - with Path(summary_path).open("w") as f: - f.write(markdown) - - -def pytest_configure(config): - config.pluginmanager.register( - StrawberryExceptionsPlugin(verbosity_level=config.getoption("verbose")), - "strawberry_exceptions", - ) - - config.addinivalue_line( - "markers", - "raises_strawberry_exception: expect to raise a strawberry exception.", - ) diff --git a/tests/pyright/test_auto.py b/tests/pyright/test_auto.py deleted file mode 100644 index 4ad54fa015..0000000000 --- a/tests/pyright/test_auto.py +++ /dev/null @@ -1,47 +0,0 @@ -from .utils import Result, requires_pyright, run_pyright, skip_on_windows - -pytestmark = [skip_on_windows, requires_pyright] - - -CODE = """ -import strawberry - - -@strawberry.type -class SomeType: - foobar: strawberry.auto - - -obj1 = SomeType(foobar=1) -obj2 = SomeType(foobar="some text") -obj3 = SomeType(foobar={"some key": "some value"}) - -reveal_type(obj1.foobar) -reveal_type(obj2.foobar) -reveal_type(obj3.foobar) -""" - - -def test_pyright(): - results = run_pyright(CODE) - - assert results == [ - Result( - type="information", - message='Type of "obj1.foobar" is "Any"', - line=14, - column=13, - ), - Result( - type="information", - message='Type of "obj2.foobar" is "Any"', - line=15, - column=13, - ), - Result( - type="information", - message='Type of "obj3.foobar" is "Any"', - line=16, - column=13, - ), - ] diff --git a/tests/pyright/test_enum.py b/tests/pyright/test_enum.py index 2b80bb2d15..d9c3daf8a9 100644 --- a/tests/pyright/test_enum.py +++ b/tests/pyright/test_enum.py @@ -1,5 +1,6 @@ from .utils import Result, requires_pyright, run_pyright, skip_on_windows + pytestmark = [skip_on_windows, requires_pyright] @@ -154,43 +155,3 @@ def test_enum_with_manual_decorator_and_name(): column=13, ), ] - - -CODE_WITH_DEPRECATION_REASON = """ -from enum import Enum - -import strawberry - -@strawberry.enum -class IceCreamFlavour(Enum): - VANILLA = "vanilla" - STRAWBERRY = strawberry.enum_value( - "strawberry", deprecation_reason="We ran out" - ) - CHOCOLATE = "chocolate" - -reveal_type(IceCreamFlavour) -reveal_type(IceCreamFlavour.STRAWBERRY) -""" - - -def test_enum_deprecated(): - results = run_pyright(CODE_WITH_DEPRECATION_REASON) - - assert results == [ - Result( - type="information", - message='Type of "IceCreamFlavour" is "Type[IceCreamFlavour]"', - line=14, - column=13, - ), - Result( - type="information", - message=( - 'Type of "IceCreamFlavour.STRAWBERRY" is ' - '"Literal[IceCreamFlavour.STRAWBERRY]"' - ), - line=15, - column=13, - ), - ] diff --git a/tests/pyright/test_federation.py b/tests/pyright/test_federation.py index 98eb348c63..adde2bcb8b 100644 --- a/tests/pyright/test_federation.py +++ b/tests/pyright/test_federation.py @@ -1,5 +1,6 @@ from .utils import Result, requires_pyright, run_pyright, skip_on_windows + pytestmark = [skip_on_windows, requires_pyright] @@ -25,7 +26,7 @@ class User: """ -def test_federation_type(): +def test_pyright(): results = run_pyright(CODE) assert results == [ @@ -49,104 +50,8 @@ def test_federation_type(): ), Result( type="information", - message='Type of "User.__init__" is "(self: User, *, name: str) -> None"', + message='Type of "User.__init__" is "(self: User, name: str) -> None"', line=19, column=13, ), ] - - -CODE_INTERFACE = """ -import strawberry - - -@strawberry.federation.interface -class User: - name: str - age: int - - -User(name="Patrick", age=1) -User(n="Patrick", age=1) - -reveal_type(User) -reveal_type(User.__init__) -""" - - -def test_federation_interface(): - results = run_pyright(CODE_INTERFACE) - - assert results == [ - Result( - type="error", - message='No parameter named "n" (reportGeneralTypeIssues)', - line=12, - column=6, - ), - Result( - type="error", - message='Argument missing for parameter "name" (reportGeneralTypeIssues)', - line=12, - column=1, - ), - Result( - type="information", - message='Type of "User" is "Type[User]"', - line=14, - column=13, - ), - Result( - type="information", - message='Type of "User.__init__" is "(self: User, *, name: str, age: int) -> None"', - line=15, - column=13, - ), - ] - - -CODE_INPUT = """ -import strawberry - -@strawberry.federation.input -class User: - name: str - - -User(name="Patrick") -User(n="Patrick") - -reveal_type(User) -reveal_type(User.__init__) -""" - - -def test_federation_input(): - results = run_pyright(CODE_INPUT) - - assert results == [ - Result( - type="error", - message='No parameter named "n" (reportGeneralTypeIssues)', - line=10, - column=6, - ), - Result( - type="error", - message='Argument missing for parameter "name" (reportGeneralTypeIssues)', - line=10, - column=1, - ), - Result( - type="information", - message='Type of "User" is "Type[User]"', - line=12, - column=13, - ), - Result( - type="information", - message='Type of "User.__init__" is "(self: User, *, name: str) -> None"', - line=13, - column=13, - ), - ] diff --git a/tests/pyright/test_federation_fields.py b/tests/pyright/test_federation_fields.py deleted file mode 100644 index cbc9459d07..0000000000 --- a/tests/pyright/test_federation_fields.py +++ /dev/null @@ -1,98 +0,0 @@ -from .utils import Result, requires_pyright, run_pyright, skip_on_windows - -pytestmark = [skip_on_windows, requires_pyright] - -CODE = """ -import strawberry - -def some_resolver(root: "User") -> str: - return "An address" - -def some_resolver_2() -> str: - return "Another address" - -@strawberry.federation.type -class User: - age: int = strawberry.federation.field(description="Age") - name: str - address: str = strawberry.federation.field(resolver=some_resolver) - another_address: str = strawberry.federation.field(resolver=some_resolver_2) - -@strawberry.federation.input -class UserInput: - age: int = strawberry.federation.field(description="Age") - name: str - - -User(name="Patrick", age=1) -User(n="Patrick", age=1) - -UserInput(name="Patrick", age=1) -UserInput(n="Patrick", age=1) - -reveal_type(User) -reveal_type(User.__init__) - -reveal_type(UserInput) -reveal_type(UserInput.__init__) -""" - - -def test_pyright(): - results = run_pyright(CODE) - - assert results == [ - Result( - type="error", - message='No parameter named "n" (reportGeneralTypeIssues)', - line=24, - column=6, - ), - Result( - type="error", - message='Argument missing for parameter "name" (reportGeneralTypeIssues)', - line=24, - column=1, - ), - Result( - type="error", - message='No parameter named "n" (reportGeneralTypeIssues)', - line=27, - column=11, - ), - Result( - type="error", - message='Argument missing for parameter "name" ' - "(reportGeneralTypeIssues)", - line=27, - column=1, - ), - Result( - type="information", - message='Type of "User" is "Type[User]"', - line=29, - column=13, - ), - Result( - type="information", - message='Type of "User.__init__" is "(self: User, *, age: int, name: str) ' - '-> None"', - line=30, - column=13, - ), - Result( - type="information", - message='Type of "UserInput" is "Type[UserInput]"', - line=32, - column=13, - ), - Result( - type="information", - message=( - 'Type of "UserInput.__init__" is "(self: UserInput, *, age: int, ' - 'name: str) -> None"' - ), - line=33, - column=13, - ), - ] diff --git a/tests/pyright/test_federation_params.py b/tests/pyright/test_federation_params.py index e75a31197d..9e0957a1ed 100644 --- a/tests/pyright/test_federation_params.py +++ b/tests/pyright/test_federation_params.py @@ -1,5 +1,6 @@ from .utils import Result, requires_pyright, run_pyright, skip_on_windows + pytestmark = [skip_on_windows, requires_pyright] diff --git a/tests/pyright/test_fields.py b/tests/pyright/test_fields.py index 1a66495479..0d7fcb76d4 100644 --- a/tests/pyright/test_fields.py +++ b/tests/pyright/test_fields.py @@ -1,5 +1,6 @@ from .utils import Result, requires_pyright, run_pyright, skip_on_windows + pytestmark = [skip_on_windows, requires_pyright] @@ -44,7 +45,7 @@ def test_pyright(): ), Result( type="information", - message='Type of "User.__init__" is "(self: User, *, name: str) -> None"', + message='Type of "User.__init__" is "(self: User, name: str) -> None"', line=14, column=13, ), diff --git a/tests/pyright/test_fields_input.py b/tests/pyright/test_fields_input.py index 6634e92904..ba5a3b98a9 100644 --- a/tests/pyright/test_fields_input.py +++ b/tests/pyright/test_fields_input.py @@ -1,5 +1,6 @@ from .utils import Result, requires_pyright, run_pyright, skip_on_windows + pytestmark = [skip_on_windows, requires_pyright] CODE = """ @@ -43,7 +44,7 @@ def test_pyright(): ), Result( type="information", - message='Type of "User.__init__" is "(self: User, *, name: str) -> None"', + message='Type of "User.__init__" is "(self: User, name: str) -> None"', line=14, column=13, ), diff --git a/tests/pyright/test_fields_keyword.py b/tests/pyright/test_fields_keyword.py deleted file mode 100644 index 660a4563a4..0000000000 --- a/tests/pyright/test_fields_keyword.py +++ /dev/null @@ -1,37 +0,0 @@ -from .utils import Result, requires_pyright, run_pyright, skip_on_windows - -pytestmark = [skip_on_windows, requires_pyright] - - -CODE = """ -import strawberry - - -@strawberry.type -class User: - name: str - - -User("Patrick") - -reveal_type(User.__init__) -""" - - -def test_pyright(): - results = run_pyright(CODE) - - assert results == [ - Result( - type="error", - message="Expected 0 positional arguments (reportGeneralTypeIssues)", - line=10, - column=6, - ), - Result( - type="information", - message='Type of "User.__init__" is "(self: User, *, name: str) -> None"', - line=12, - column=13, - ), - ] diff --git a/tests/pyright/test_fields_resolver.py b/tests/pyright/test_fields_resolver.py index e213b8d3d7..9636022375 100644 --- a/tests/pyright/test_fields_resolver.py +++ b/tests/pyright/test_fields_resolver.py @@ -1,5 +1,6 @@ from .utils import Result, requires_pyright, run_pyright, skip_on_windows + pytestmark = [skip_on_windows, requires_pyright] CODE = """ @@ -47,7 +48,7 @@ def test_pyright(): ), Result( type="information", - message='Type of "User.__init__" is "(self: User, *, name: str) -> None"', + message='Type of "User.__init__" is "(self: User, name: str) -> None"', line=18, column=13, ), diff --git a/tests/pyright/test_interface.py b/tests/pyright/test_interface.py deleted file mode 100644 index 651ac0f395..0000000000 --- a/tests/pyright/test_interface.py +++ /dev/null @@ -1,52 +0,0 @@ -from .utils import Result, requires_pyright, run_pyright, skip_on_windows - -pytestmark = [skip_on_windows, requires_pyright] - -CODE = """ -import strawberry - - -@strawberry.interface -class Node: - id: strawberry.ID - -reveal_type(Node) -""" - - -def test_pyright(): - results = run_pyright(CODE) - - assert results == [ - Result( - type="information", - message='Type of "Node" is "Type[Node]"', - line=9, - column=13, - ), - ] - - -CODE_2 = """ -import strawberry - - -@strawberry.interface(name="nodeinterface") -class Node: - id: strawberry.ID - -reveal_type(Node) -""" - - -def test_pyright_calling(): - results = run_pyright(CODE_2) - - assert results == [ - Result( - type="information", - message='Type of "Node" is "Type[Node]"', - line=9, - column=13, - ), - ] diff --git a/tests/pyright/test_params.py b/tests/pyright/test_params.py index 0e857eac07..480987c659 100644 --- a/tests/pyright/test_params.py +++ b/tests/pyright/test_params.py @@ -1,5 +1,6 @@ from .utils import Result, requires_pyright, run_pyright, skip_on_windows + pytestmark = [skip_on_windows, requires_pyright] diff --git a/tests/pyright/test_private.py b/tests/pyright/test_private.py index 97fa5affd0..7ad8e1d6ca 100644 --- a/tests/pyright/test_private.py +++ b/tests/pyright/test_private.py @@ -1,5 +1,6 @@ from .utils import Result, requires_pyright, run_pyright, skip_on_windows + pytestmark = [skip_on_windows, requires_pyright] diff --git a/tests/pyright/test_scalars.py b/tests/pyright/test_scalars.py deleted file mode 100644 index 2eb473c186..0000000000 --- a/tests/pyright/test_scalars.py +++ /dev/null @@ -1,106 +0,0 @@ -from .utils import Result, requires_pyright, run_pyright, skip_on_windows - -pytestmark = [skip_on_windows, requires_pyright] - - -CODE = """ -import strawberry -from strawberry.scalars import ID, JSON, Base16, Base32, Base64 - - -@strawberry.type -class SomeType: - id: ID - json: JSON - base16: Base16 - base32: Base32 - base64: Base64 - - -obj = SomeType( - id=ID("123"), - json=JSON({"foo": "bar"}), - base16=Base16(b""), - base32=Base32(b""), - base64=Base64(b""), -) - -reveal_type(obj.id) -reveal_type(obj.json) -reveal_type(obj.base16) -reveal_type(obj.base16) -reveal_type(obj.base64) -""" - - -def test_pyright(): - results = run_pyright(CODE) - - # NOTE: This is also guaranteeing that those scalars could be used to annotate - # the attributes. Pyright 1.1.224+ doesn't allow non-types to be used there - assert results == [ - Result( - type="information", - message='Type of "obj.id" is "ID"', - line=23, - column=13, - ), - Result( - type="information", - message='Type of "obj.json" is "JSON"', - line=24, - column=13, - ), - Result( - type="information", - message='Type of "obj.base16" is "Base16"', - line=25, - column=13, - ), - Result( - type="information", - message='Type of "obj.base16" is "Base16"', - line=26, - column=13, - ), - Result( - type="information", - message='Type of "obj.base64" is "Base64"', - line=27, - column=13, - ), - ] - - -CODE_SCHEMA_OVERRIDES = """ -import strawberry -from datetime import datetime, timezone - -EpochDateTime = strawberry.scalar( - datetime, -) - -@strawberry.type -class Query: - a: datetime - -schema = strawberry.Schema(query=Query, scalar_overrides={ - datetime: EpochDateTime, -}) - -reveal_type(EpochDateTime) -""" - - -def test_schema_overrides(): - # TODO: change strict to true when we improve type hints for scalar - results = run_pyright(CODE_SCHEMA_OVERRIDES, strict=False) - - assert results == [ - Result( - type="information", - message='Type of "EpochDateTime" is "Type[datetime]"', - line=17, - column=13, - ), - ] diff --git a/tests/pyright/test_union.py b/tests/pyright/test_union.py index 4203ad1047..7b2030399c 100644 --- a/tests/pyright/test_union.py +++ b/tests/pyright/test_union.py @@ -1,11 +1,12 @@ from .utils import Result, requires_pyright, run_pyright, skip_on_windows + pytestmark = [skip_on_windows, requires_pyright] CODE = """ import strawberry -from typing_extensions import TypeAlias + @strawberry.type class User: @@ -16,7 +17,7 @@ class User: class Error: message: str -UserOrError: TypeAlias = strawberry.union("UserOrError", (User, Error)) # type: ignore +UserOrError = strawberry.union("UserOrError", (User, Error)) reveal_type(UserOrError) diff --git a/tests/pyright/utils.py b/tests/pyright/utils.py index 59c62ea001..11b8b94cbd 100644 --- a/tests/pyright/utils.py +++ b/tests/pyright/utils.py @@ -5,11 +5,13 @@ import tempfile from dataclasses import dataclass from typing import List, cast -from typing_extensions import Literal import pytest -ResultType = Literal["error", "information"] +from typing_extensions import Literal + + +ResultType = Literal["error", "info"] @dataclass @@ -25,35 +27,28 @@ def from_output_line(cls, output_line: str) -> "Result": file_info, result = output_line.split("-", maxsplit=1) - line, column = (int(value) for value in file_info.split(":")[1:]) - type_, message = (value.strip() for value in result.split(":", maxsplit=1)) + line, column = [int(value) for value in file_info.split(":")[1:]] + type_, message = [value.strip() for value in result.split(":", maxsplit=1)] type_ = cast(ResultType, type_) return cls(type=type_, message=message, line=line, column=column) -def run_pyright(code: str, strict: bool = True) -> List[Result]: - if strict: - code = "# pyright: strict\n" + code - +def run_pyright(code: str) -> List[Result]: with tempfile.NamedTemporaryFile("w", suffix=".py", delete=False) as f: f.write(code) - process_result = subprocess.run(["pyright", f.name], stdout=subprocess.PIPE) + result = subprocess.run(["pyright", f.name], stdout=subprocess.PIPE) - os.unlink(f.name) # noqa: PTH108 + os.remove(f.name) - output = process_result.stdout.decode("utf-8") + output = result.stdout.decode("utf-8") results: List[Result] = [] for line in output.splitlines(): if line.strip().startswith(f"{f.name}:"): - result = Result.from_output_line(line) - if strict: - result.line -= 1 - - results.append(result) + results.append(Result.from_output_line(line)) return results diff --git a/strawberry/exceptions/utils/__init__.py b/tests/sanic/__init__.py similarity index 100% rename from strawberry/exceptions/utils/__init__.py rename to tests/sanic/__init__.py diff --git a/tests/sanic/app.py b/tests/sanic/app.py new file mode 100644 index 0000000000..7dfc6ee748 --- /dev/null +++ b/tests/sanic/app.py @@ -0,0 +1,51 @@ +import typing +from random import random + +import strawberry +from sanic import Sanic +from strawberry.file_uploads import Upload +from strawberry.sanic.views import GraphQLView as BaseGraphQLView + + +def create_app(**kwargs): + @strawberry.input + class FolderInput: + files: typing.List[Upload] + + @strawberry.type + class Query: + hello: str = "strawberry" + + @strawberry.type + class Mutation: + @strawberry.mutation + def read_text(self, text_file: Upload) -> str: + return text_file.read().decode() + + @strawberry.mutation + def read_files(self, files: typing.List[Upload]) -> typing.List[str]: + contents = [] + for file in files: + contents.append(file.read().decode()) + return contents + + @strawberry.mutation + def read_folder(self, folder: FolderInput) -> typing.List[str]: + contents = [] + for file in folder.files: + contents.append(file.read().decode()) + return contents + + schema = strawberry.Schema(query=Query, mutation=Mutation) + + class GraphQLView(BaseGraphQLView): + def get_root_value(self): + return Query() + + app = Sanic(f"test_{int(random()*1000)}") + + app.add_route( + GraphQLView.as_view(schema=schema, graphiql=kwargs.get("graphiql", True)), + "/graphql", + ) + return app diff --git a/tests/sanic/conftest.py b/tests/sanic/conftest.py new file mode 100644 index 0000000000..f45cfa2a62 --- /dev/null +++ b/tests/sanic/conftest.py @@ -0,0 +1,8 @@ +import pytest + +from .app import create_app + + +@pytest.fixture +def sanic_client(): + yield create_app() diff --git a/tests/sanic/test_upload.py b/tests/sanic/test_upload.py new file mode 100644 index 0000000000..4117f6793c --- /dev/null +++ b/tests/sanic/test_upload.py @@ -0,0 +1,230 @@ +OPERATIONS_FIELD = ( + "------sanic\r\n" + 'Content-Disposition: form-data; name="operations"\r\n' + "\r\n" + "{" + '"query": "mutation($textFile: Upload!){readText(textFile: $textFile)}",' + '"variables": {"textFile": null}' + "}\r\n" +) + +MAP_FIELD = ( + "------sanic\r\n" + 'Content-Disposition: form-data; name="map"\r\n' + "\r\n" + '{"textFile": ["variables.textFile"]}\r\n' +) + +TEXT_FILE_FIELD = ( + "------sanic\r\n" + 'Content-Disposition: form-data; name="textFile"; filename="textFile.txt"\r\n' + "Content-Type: text/plain\r\n" + "\r\n" + "strawberry\r\n" +) + +MULTI_UPLOAD_OPERATIONS_FIELD = ( + "------sanic\r\n" + 'Content-Disposition: form-data; name="operations"\r\n' + "\r\n" + "{" + '"query": "mutation($files: [Upload!]!){readFiles(files: $files)}",' + '"variables": {"files": [null, null]}' + "}\r\n" +) + +MULTI_UPLOAD_MAP_FIELD = ( + "------sanic\r\n" + 'Content-Disposition: form-data; name="map"\r\n' + "\r\n" + '{"file1": ["variables.files.0"], "file2": ["variables.files.1"]}\r\n' +) + +MULTI_UPLOAD_TEXT_FILE1_FIELD = ( + "------sanic\r\n" + 'Content-Disposition: form-data; name="file1"; filename="file1.txt"\r\n' + "Content-Type: text/plain\r\n" + "\r\n" + "strawberry1\r\n" +) + +MULTI_UPLOAD_TEXT_FILE2_FIELD = ( + "------sanic\r\n" + 'Content-Disposition: form-data; name="file2"; filename="file2.txt"\r\n' + "Content-Type: text/plain\r\n" + "\r\n" + "strawberry2\r\n" +) + +COMPLEX_UPLOAD_OPERATIONS_FIELD = ( + "------sanic\r\n" + 'Content-Disposition: form-data; name="operations"\r\n' + "\r\n" + "{" + '"query": "mutation($folder: FolderInput!){readFolder(folder: $folder)}",' + '"variables": {"folder": {"files": [null, null]}}' + "}\r\n" +) + +COMPLEX_UPLOAD_MAP_FIELD = ( + "------sanic\r\n" + 'Content-Disposition: form-data; name="map"\r\n' + "\r\n" + '{"file1": ["variables.folder.files.0"], "file2": ["variables.folder.files.1"]}\r\n' +) + +END = "------sanic--" + + +def test_single_file_upload(sanic_client): + data = OPERATIONS_FIELD + MAP_FIELD + TEXT_FILE_FIELD + END + headers = {"content-type": "multipart/form-data; boundary=----sanic"} + + request, response = sanic_client.test_client.post( + "/graphql", + data=data, + headers=headers, + ) + + assert response.status_code == 200 + assert not response.json.get("errors") + assert response.json["data"]["readText"] == "strawberry" + + +def test_file_list_upload(sanic_client): + data = ( + MULTI_UPLOAD_OPERATIONS_FIELD + + MULTI_UPLOAD_MAP_FIELD + + MULTI_UPLOAD_TEXT_FILE1_FIELD + + MULTI_UPLOAD_TEXT_FILE2_FIELD + + END + ) + headers = {"content-type": "multipart/form-data; boundary=----sanic"} + + request, response = sanic_client.test_client.post( + "/graphql", + data=data, + headers=headers, + ) + + assert response.status_code == 200 + assert not response.json.get("errors") + + assert len(response.json["data"]["readFiles"]) == 2 + assert response.json["data"]["readFiles"][0] == "strawberry1" + assert response.json["data"]["readFiles"][1] == "strawberry2" + + +def test_nested_file_list(sanic_client): + data = ( + COMPLEX_UPLOAD_OPERATIONS_FIELD + + COMPLEX_UPLOAD_MAP_FIELD + + MULTI_UPLOAD_TEXT_FILE1_FIELD + + MULTI_UPLOAD_TEXT_FILE2_FIELD + + END + ) + headers = {"content-type": "multipart/form-data; boundary=----sanic"} + + request, response = sanic_client.test_client.post( + "/graphql", + data=data, + headers=headers, + ) + + assert response.status_code == 200 + assert not response.json.get("errors") + + assert len(response.json["data"]["readFolder"]) == 2 + assert response.json["data"]["readFolder"][0] == "strawberry1" + assert response.json["data"]["readFolder"][1] == "strawberry2" + + +def test_extra_form_data_fields_are_ignored(sanic_client): + extra_field = ( + "------sanic\r\n" + 'Content-Disposition: form-data; name="extra_field"\r\n' + "\r\n" + "{}\r\n" + ) + + data = OPERATIONS_FIELD + MAP_FIELD + TEXT_FILE_FIELD + extra_field + END + headers = {"content-type": "multipart/form-data; boundary=----sanic"} + + request, response = sanic_client.test_client.post( + "/graphql", + data=data, + headers=headers, + ) + + assert response.status_code == 200 + assert not response.json.get("errors") + + +def test_sending_invalid_form_data(sanic_client): + headers = {"content-type": "multipart/form-data; boundary=----fake"} + request, response = sanic_client.test_client.post( + "/graphql", + headers=headers, + ) + + assert response.status_code == 400 + assert "No GraphQL query found in the request" in response.text + + +def test_malformed_query(sanic_client): + operations_field = ( + "------sanic\r\n" + 'Content-Disposition: form-data; name="operations"\r\n' + "\r\n" + "{" + '"NOT_QUERY": "",' + '"variables": {"textFile": null}' + "}\r\n" + ) + + data = operations_field + MAP_FIELD + TEXT_FILE_FIELD + END + headers = {"content-type": "multipart/form-data; boundary=----sanic"} + + request, response = sanic_client.test_client.post( + "/graphql", + data=data, + headers=headers, + ) + + assert response.status_code == 400 + assert "No GraphQL query found in the request" in response.text + + +def test_sending_invalid_json_body(sanic_client): + operations_field = ( + "------sanic\r\n" + 'Content-Disposition: form-data; name="operations"\r\n' + "\r\n" + "}\r\n" + ) + + data = operations_field + MAP_FIELD + TEXT_FILE_FIELD + END + headers = {"content-type": "multipart/form-data; boundary=----sanic"} + + request, response = sanic_client.test_client.post( + "/graphql", + data=data, + headers=headers, + ) + + assert response.status_code == 400 + assert "Unable to parse request body as JSON" in response.text + + +def test_upload_with_missing_file(sanic_client): + data = OPERATIONS_FIELD + MAP_FIELD + END + headers = {"content-type": "multipart/form-data; boundary=----sanic"} + + request, response = sanic_client.test_client.post( + "/graphql", + data=data, + headers=headers, + ) + + assert response.status_code == 400 + assert "File(s) missing in form data" in response.text diff --git a/tests/sanic/test_view.py b/tests/sanic/test_view.py new file mode 100644 index 0000000000..2efb3bd4a5 --- /dev/null +++ b/tests/sanic/test_view.py @@ -0,0 +1,102 @@ +import strawberry +from sanic import Sanic +from strawberry.sanic.views import GraphQLView as BaseGraphQLView +from strawberry.types import ExecutionResult, Info + +from .app import create_app + + +def test_graphql_query(sanic_client): + query = { + "query": """ + query { + hello + } + """ + } + + request, response = sanic_client.test_client.post("/graphql", json=query) + data = response.json + assert response.status == 200 + assert data["data"]["hello"] == "strawberry" + + +def test_graphiql_view(sanic_client): + request, response = sanic_client.test_client.get("/graphql") + body = response.body.decode() + + assert "GraphiQL" in body + + +def test_graphiql_disabled_view(): + app = create_app(graphiql=False) + + request, response = app.test_client.get("/graphql") + assert response.status == 404 + + +def test_custom_context(): + class CustomGraphQLView(BaseGraphQLView): + async def get_context(self, request): + return {"request": request, "custom_value": "Hi!"} + + @strawberry.type + class Query: + @strawberry.field + def custom_context_value(self, info: Info) -> str: + return info.context["custom_value"] + + schema = strawberry.Schema(query=Query) + + app = Sanic("test-app-custom_context") + app.debug = True + + app.add_route(CustomGraphQLView.as_view(schema=schema, graphiql=True), "/graphql") + + query = "{ customContextValue }" + + request, response = app.test_client.post("/graphql", json={"query": query}) + data = response.json + + assert response.status == 200 + assert data["data"] == {"customContextValue": "Hi!"} + + +def test_custom_process_result(): + class CustomGraphQLView(BaseGraphQLView): + def process_result(self, result: ExecutionResult): + return {} + + @strawberry.type + class Query: + @strawberry.field + def abc(self) -> str: + return "ABC" + + schema = strawberry.Schema(query=Query) + + app = Sanic("test-app-custom_process_result") + app.debug = True + + app.add_route(CustomGraphQLView.as_view(schema=schema, graphiql=True), "/graphql") + + query = "{ abc }" + + request, response = app.test_client.post("/graphql", json={"query": query}) + data = response.json + + assert response.status == 200 + assert data == {} + + +def test_malformed_query(sanic_client): + query = { + "qwary": """ + qwary { + hello + } + """ + } + + request, response = sanic_client.test_client.post("/graphql", json=query) + assert response.status == 400 diff --git a/tests/schema/extensions/test_apollo.py b/tests/schema/extensions/test_apollo.py index b13c9dacd9..60cd6c34c0 100644 --- a/tests/schema/extensions/test_apollo.py +++ b/tests/schema/extensions/test_apollo.py @@ -1,5 +1,7 @@ import pytest + from freezegun import freeze_time + from graphql.utilities import get_introspection_query import strawberry diff --git a/tests/schema/extensions/test_datadog.py b/tests/schema/extensions/test_datadog.py deleted file mode 100644 index cb0bf00362..0000000000 --- a/tests/schema/extensions/test_datadog.py +++ /dev/null @@ -1,250 +0,0 @@ -from typing import AsyncGenerator - -import pytest - -import strawberry - - -@pytest.fixture -def datadog_extension(mocker): - datadog_mock = mocker.MagicMock() - - mocker.patch.dict("sys.modules", ddtrace=datadog_mock) - - from strawberry.extensions.tracing.datadog import DatadogTracingExtension - - return DatadogTracingExtension, datadog_mock - - -@pytest.fixture -def datadog_extension_sync(mocker): - datadog_mock = mocker.MagicMock() - - mocker.patch.dict("sys.modules", ddtrace=datadog_mock) - - from strawberry.extensions.tracing.datadog import DatadogTracingExtensionSync - - return DatadogTracingExtensionSync, datadog_mock - - -@strawberry.type -class Person: - name: str = "Jack" - - -@strawberry.type -class Query: - @strawberry.field - def person(self) -> Person: - return Person() - - @strawberry.field - async def person_async(self) -> Person: - return Person() - - -@strawberry.type -class Mutation: - @strawberry.mutation - def say_hi(self) -> str: - return "hello" - - -@strawberry.type -class Subscription: - @strawberry.field - async def on_hi(self) -> AsyncGenerator[str, None]: - yield "Hello" - - -# TODO: this test could be improved by passing a custom tracer to the datadog extension -# and maybe we could unify datadog and opentelemetry extensions by doing that - - -@pytest.mark.asyncio -async def test_datadog_tracer(datadog_extension, mocker): - extension, mock = datadog_extension - - schema = strawberry.Schema( - query=Query, - mutation=Mutation, - extensions=[extension], - ) - - query = """ - query { - personAsync { - name - } - } - """ - - await schema.execute(query) - - mock.tracer.assert_has_calls( - [ - mocker.call.trace( - "Anonymous Query", - resource="63a280256ca4e8514e06cf90b30c8c3a", - span_type="graphql", - service="strawberry", - ), - mocker.call.trace().set_tag("graphql.operation_name", None), - mocker.call.trace().set_tag("graphql.operation_type", "query"), - mocker.call.trace("Parsing", span_type="graphql"), - mocker.call.trace().finish(), - mocker.call.trace("Validation", span_type="graphql"), - mocker.call.trace().finish(), - mocker.call.trace("Resolving: Query.personAsync", span_type="graphql"), - mocker.call.trace().__enter__(), - mocker.call.trace() - .__enter__() - .set_tag("graphql.field_name", "personAsync"), - mocker.call.trace().__enter__().set_tag("graphql.parent_type", "Query"), - mocker.call.trace() - .__enter__() - .set_tag("graphql.field_path", "Query.personAsync"), - mocker.call.trace().__enter__().set_tag("graphql.path", "personAsync"), - mocker.call.trace().__exit__(None, None, None), - mocker.call.trace().finish(), - ] - ) - - -@pytest.mark.asyncio -async def test_uses_operation_name_and_hash(datadog_extension): - extension, mock = datadog_extension - - schema = strawberry.Schema(query=Query, mutation=Mutation, extensions=[extension]) - - query = """ - query MyExampleQuery { - person { - name - } - } - """ - - await schema.execute(query, operation_name="MyExampleQuery") - - mock.tracer.trace.assert_any_call( - "MyExampleQuery", - resource="MyExampleQuery:efe8d7247ee8136f45e3824c2768b155", - span_type="graphql", - service="strawberry", - ) - - -@pytest.mark.asyncio -async def test_uses_operation_type(datadog_extension): - extension, mock = datadog_extension - - schema = strawberry.Schema(query=Query, mutation=Mutation, extensions=[extension]) - - query = """ - mutation MyMutation { - sayHi - } - """ - - await schema.execute(query, operation_name="MyMutation") - mock.tracer.trace().set_tag.assert_any_call("graphql.operation_type", "mutation") - - -@pytest.mark.asyncio -async def test_uses_operation_subscription(datadog_extension): - extension, mock = datadog_extension - - schema = strawberry.Schema(query=Query, mutation=Mutation, extensions=[extension]) - - query = """ - subscription MySubscription { - onHi - } - """ - - await schema.execute(query, operation_name="MySubscription") - mock.tracer.trace().set_tag.assert_any_call( - "graphql.operation_type", "subscription" - ) - - -def test_datadog_tracer_sync(datadog_extension_sync, mocker): - extension, mock = datadog_extension_sync - schema = strawberry.Schema(query=Query, mutation=Mutation, extensions=[extension]) - - query = """ - query { - person { - name - } - } - """ - - schema.execute_sync(query) - - mock.tracer.assert_has_calls( - [ - mocker.call.trace( - "Anonymous Query", - resource="659edba9e6ac9c20d03da1b2d0f9a956", - span_type="graphql", - service="strawberry", - ), - mocker.call.trace().set_tag("graphql.operation_name", None), - mocker.call.trace().set_tag("graphql.operation_type", "query"), - mocker.call.trace("Parsing", span_type="graphql"), - mocker.call.trace().finish(), - mocker.call.trace("Validation", span_type="graphql"), - mocker.call.trace().finish(), - mocker.call.trace("Resolving: Query.person", span_type="graphql"), - mocker.call.trace().__enter__(), - mocker.call.trace().__enter__().set_tag("graphql.field_name", "person"), - mocker.call.trace().__enter__().set_tag("graphql.parent_type", "Query"), - mocker.call.trace() - .__enter__() - .set_tag("graphql.field_path", "Query.person"), - mocker.call.trace().__enter__().set_tag("graphql.path", "person"), - mocker.call.trace().__exit__(None, None, None), - mocker.call.trace().finish(), - ] - ) - - -def test_uses_operation_name_and_hash_sync(datadog_extension_sync): - extension, mock = datadog_extension_sync - - schema = strawberry.Schema(query=Query, mutation=Mutation, extensions=[extension]) - - query = """ - query MyExampleQuery { - person { - name - } - } - """ - - schema.execute_sync(query, operation_name="MyExampleQuery") - - mock.tracer.trace.assert_any_call( - "MyExampleQuery", - resource="MyExampleQuery:efe8d7247ee8136f45e3824c2768b155", - span_type="graphql", - service="strawberry", - ) - - -def test_uses_operation_type_sync(datadog_extension_sync): - extension, mock = datadog_extension_sync - - schema = strawberry.Schema(query=Query, mutation=Mutation, extensions=[extension]) - - query = """ - mutation MyMutation { - sayHi - } - """ - - schema.execute_sync(query, operation_name="MyMutation") - - mock.tracer.trace().set_tag.assert_any_call("graphql.operation_type", "mutation") diff --git a/tests/schema/extensions/test_extensions.py b/tests/schema/extensions/test_extensions.py index c1adb6f057..4c1d4b4ad0 100644 --- a/tests/schema/extensions/test_extensions.py +++ b/tests/schema/extensions/test_extensions.py @@ -3,12 +3,14 @@ from unittest.mock import patch import pytest -from graphql import ExecutionResult as GraphQLExecutionResult -from graphql import GraphQLError -from graphql import execute as original_execute + +from graphql import ( + ExecutionResult as GraphQLExecutionResult, + GraphQLError, + execute as original_execute, +) import strawberry -from strawberry.exceptions import StrawberryGraphQLError from strawberry.extensions import Extension @@ -291,7 +293,7 @@ async def get_results(self): class Query: @strawberry.field def string(self) -> str: - return "" + return str() schema = strawberry.Schema(query=Query, extensions=[MyExtension]) query = "query { string }" @@ -539,89 +541,3 @@ def ping(self) -> str: assert result.errors == [GraphQLError("Well you asked for it")] assert mock_original_execute.call_count == 1 - - -def test_extend_error_format_example(): - # Test that the example of how to extend error format - - class ExtendErrorFormat(Extension): - def on_request_end(self): - result = self.execution_context.result - if getattr(result, "errors", None): - result.errors = [ - StrawberryGraphQLError( - extensions={"additional_key": "additional_value"}, - nodes=error.nodes, - source=error.source, - positions=error.positions, - path=error.path, - original_error=error.original_error, - message=error.message, - ) - for error in result.errors - ] - - @strawberry.type - class Query: - @strawberry.field - def ping(self) -> str: - raise Exception("This error occurred while querying the ping field") - - schema = strawberry.Schema(query=Query, extensions=[ExtendErrorFormat]) - query = """ - query TestQuery { - ping - } - """ - - result = schema.execute_sync(query) - assert result.errors[0].extensions == {"additional_key": "additional_value"} - assert ( - result.errors[0].message == "This error occurred while querying the ping field" - ) - assert result.data is None - - -def test_extension_can_set_query(): - class MyExtension(Extension): - def on_request_start(self): - self.execution_context.query = "{ hi }" - - @strawberry.type - class Query: - @strawberry.field - def hi(self) -> str: - return "๐Ÿ‘‹" - - schema = strawberry.Schema(query=Query, extensions=[MyExtension]) - - # Query not set on input - query = "" - - result = schema.execute_sync(query) - - assert not result.errors - assert result.data == {"hi": "๐Ÿ‘‹"} - - -@pytest.mark.asyncio -async def test_extension_can_set_query_async(): - class MyExtension(Extension): - def on_request_start(self): - self.execution_context.query = "{ hi }" - - @strawberry.type - class Query: - @strawberry.field - async def hi(self) -> str: - return "๐Ÿ‘‹" - - schema = strawberry.Schema(query=Query, extensions=[MyExtension]) - - # Query not set on input - query = "" - - result = await schema.execute(query) - - assert not result.errors - assert result.data == {"hi": "๐Ÿ‘‹"} diff --git a/tests/schema/extensions/test_imports.py b/tests/schema/extensions/test_imports.py deleted file mode 100644 index 13a41a7fc6..0000000000 --- a/tests/schema/extensions/test_imports.py +++ /dev/null @@ -1,11 +0,0 @@ -import pytest - - -def test_can_import(mocker): - # mocking sys.modules.ddtrace so we don't get an ImportError - mocker.patch.dict("sys.modules", ddtrace=mocker.MagicMock()) - - -def test_fails_if_import_is_not_found(): - with pytest.raises(ImportError): - from strawberry.extensions.tracing import Blueberry # noqa diff --git a/tests/schema/extensions/test_mask_errors.py b/tests/schema/extensions/test_mask_errors.py deleted file mode 100644 index 06d0842ad8..0000000000 --- a/tests/schema/extensions/test_mask_errors.py +++ /dev/null @@ -1,139 +0,0 @@ -from unittest.mock import Mock - -from graphql.error import GraphQLError -from graphql.error.graphql_error import format_error as format_graphql_error - -import strawberry -from strawberry.extensions import MaskErrors - - -def test_mask_all_errors(): - @strawberry.type - class Query: - @strawberry.field - def hidden_error(self) -> str: - raise KeyError("This error is not visible") - - schema = strawberry.Schema(query=Query, extensions=[MaskErrors()]) - - query = "query { hiddenError }" - - result = schema.execute_sync(query) - assert result.errors is not None - formatted_errors = [format_graphql_error(err) for err in result.errors] - assert formatted_errors == [ - { - "locations": [{"column": 9, "line": 1}], - "message": "Unexpected error.", - "path": ["hiddenError"], - } - ] - - -def test_mask_some_errors(): - class VisibleError(Exception): - pass - - @strawberry.type - class Query: - @strawberry.field - def visible_error(self) -> str: - raise VisibleError("This error is visible") - - @strawberry.field - def hidden_error(self) -> str: - raise Exception("This error is not visible") - - def should_mask_error(error: GraphQLError) -> bool: - original_error = error.original_error - if original_error and isinstance(original_error, VisibleError): - return False - return True - - schema = strawberry.Schema( - query=Query, extensions=[MaskErrors(should_mask_error=should_mask_error)] - ) - - query = "query { hiddenError }" - - result = schema.execute_sync(query) - assert result.errors is not None - formatted_errors = [format_graphql_error(err) for err in result.errors] - assert formatted_errors == [ - { - "locations": [{"column": 9, "line": 1}], - "message": "Unexpected error.", - "path": ["hiddenError"], - } - ] - - query = "query { visibleError }" - - result = schema.execute_sync(query) - assert result.errors is not None - formatted_errors = [format_graphql_error(err) for err in result.errors] - assert formatted_errors == [ - { - "locations": [{"column": 9, "line": 1}], - "message": "This error is visible", - "path": ["visibleError"], - } - ] - - -def test_process_errors_original_error(): - @strawberry.type - class Query: - @strawberry.field - def hidden_error(self) -> str: - raise ValueError("This error is not visible") - - mock_process_error = Mock() - - class CustomSchema(strawberry.Schema): - def process_errors(self, errors, execution_context): - for error in errors: - mock_process_error(error) - - schema = CustomSchema(query=Query, extensions=[MaskErrors()]) - - query = "query { hiddenError }" - - result = schema.execute_sync(query) - assert result.errors is not None - formatted_errors = [format_graphql_error(err) for err in result.errors] - assert formatted_errors == [ - { - "locations": [{"column": 9, "line": 1}], - "message": "Unexpected error.", - "path": ["hiddenError"], - } - ] - - assert mock_process_error.call_count == 1 - call = mock_process_error.call_args_list[0] - assert call[0][0].message == "This error is not visible" - assert isinstance(call[0][0].original_error, ValueError) - - -def test_graphql_error_masking(): - @strawberry.type - class Query: - @strawberry.field - def graphql_error(self) -> str: - return None # type: ignore - - schema = strawberry.Schema(query=Query, extensions=[MaskErrors()]) - - query = "query { graphqlError }" - - result = schema.execute_sync(query) - assert result.errors is not None - formatted_errors = [format_graphql_error(err) for err in result.errors] - assert formatted_errors == [ - { - "locations": [{"column": 9, "line": 1}], - "message": "Unexpected error.", - "path": ["graphqlError"], - } - ] diff --git a/tests/schema/extensions/test_opentelemetry.py b/tests/schema/extensions/test_opentelemetry.py index 54ae86f423..8a5e34afee 100644 --- a/tests/schema/extensions/test_opentelemetry.py +++ b/tests/schema/extensions/test_opentelemetry.py @@ -1,4 +1,5 @@ import pytest + from opentelemetry.trace import SpanKind import strawberry diff --git a/tests/schema/extensions/test_parser_cache.py b/tests/schema/extensions/test_parser_cache.py index 3d75724a0f..5382860880 100644 --- a/tests/schema/extensions/test_parser_cache.py +++ b/tests/schema/extensions/test_parser_cache.py @@ -1,6 +1,7 @@ from unittest.mock import patch import pytest + from graphql import parse import strawberry diff --git a/tests/schema/extensions/test_query_depth_limiter.py b/tests/schema/extensions/test_query_depth_limiter.py index fa6f678eea..bd71b787d4 100644 --- a/tests/schema/extensions/test_query_depth_limiter.py +++ b/tests/schema/extensions/test_query_depth_limiter.py @@ -2,6 +2,7 @@ from typing import List, Optional import pytest + from graphql import get_introspection_query, parse, specified_rules, validate import strawberry @@ -70,7 +71,7 @@ def callback(query_depths): errors = validate( schema._schema, document, - rules=(*specified_rules, validation_rule), + rules=(specified_rules + [validation_rule]), ) return errors, result @@ -246,7 +247,7 @@ def test_should_raise_invalid_ignore(): user { address { city } } } """ - with pytest.raises(TypeError, match="Invalid ignore option:"): + with pytest.raises(ValueError, match="Invalid ignore option:"): run_query( query, 10, diff --git a/tests/schema/extensions/test_validation_cache.py b/tests/schema/extensions/test_validation_cache.py index 00bf7c644d..c396835acd 100644 --- a/tests/schema/extensions/test_validation_cache.py +++ b/tests/schema/extensions/test_validation_cache.py @@ -1,6 +1,7 @@ from unittest.mock import patch import pytest + from graphql import validate import strawberry diff --git a/tests/schema/test_arguments.py b/tests/schema/test_arguments.py index 984b4c6d8d..b8c4ab0a40 100644 --- a/tests/schema/test_arguments.py +++ b/tests/schema/test_arguments.py @@ -1,9 +1,10 @@ from textwrap import dedent from typing import Optional + from typing_extensions import Annotated import strawberry -from strawberry.unset import UNSET +from strawberry.arguments import UNSET, is_unset def test_argument_descriptions(): @@ -12,7 +13,7 @@ class Query: @strawberry.field def hello( # type: ignore name: Annotated[ - str, strawberry.argument(description="Your name") + str, strawberry.argument(description="Your name") # noqa: F722 ] = "Patrick" ) -> str: return f"Hi {name}" @@ -36,7 +37,7 @@ class Query: @strawberry.field def hello( # type: ignore name: Annotated[ - str, strawberry.argument(deprecation_reason="Your reason") + str, strawberry.argument(deprecation_reason="Your reason") # noqa: F722 ] = "Patrick" ) -> str: return f"Hi {name}" @@ -101,7 +102,7 @@ def test_optional_argument_unset(): class Query: @strawberry.field def hello(self, name: Optional[str] = UNSET, age: Optional[int] = UNSET) -> str: - if name is UNSET: + if is_unset(name): return "Hi there" return f"Hi {name}" @@ -135,7 +136,7 @@ class TestInput: class Query: @strawberry.field def hello(self, input: TestInput) -> str: - if input.name is UNSET: + if is_unset(input.name): return "Hi there" return f"Hi {input.name}" diff --git a/tests/schema/test_basic.py b/tests/schema/test_basic.py index a3e26ca2cb..2e41f20382 100644 --- a/tests/schema/test_basic.py +++ b/tests/schema/test_basic.py @@ -7,14 +7,10 @@ import pytest import strawberry -from strawberry import ID from strawberry.exceptions import ( FieldWithResolverAndDefaultFactoryError, FieldWithResolverAndDefaultValueError, ) -from strawberry.scalars import Base64 -from strawberry.schema_directive import Location -from strawberry.type import StrawberryList def test_raises_exception_with_unsupported_types(): @@ -108,7 +104,7 @@ class Hello: class Query: @strawberry.field def hello(self) -> Hello: - return Hello(value="hi") + return Hello("hi") @strawberry.field(name="example1") def example(self, query_param: str) -> str: @@ -299,38 +295,6 @@ class Query: assert result.data["pizzas"]["description"] is None -def test_enum_value_description(): - @strawberry.enum - class IceCreamFlavour(Enum): - VANILLA = "vainilla" - STRAWBERRY = strawberry.enum_value("strawberry", description="Our favourite.") - CHOCOLATE = "chocolate" - - @strawberry.type - class Query: - favorite_ice_cream: IceCreamFlavour = IceCreamFlavour.STRAWBERRY - - schema = strawberry.Schema(query=Query) - - query = """{ - iceCreamFlavour: __type(name: "IceCreamFlavour") { - enumValues { - name - description - } - } - }""" - - result = schema.execute_sync(query) - - assert not result.errors - assert result.data["iceCreamFlavour"]["enumValues"] == [ - {"name": "VANILLA", "description": None}, - {"name": "STRAWBERRY", "description": "Our favourite."}, - {"name": "CHOCOLATE", "description": None}, - ] - - def test_parent_class_fields_are_inherited(): @strawberry.type class Parent: @@ -529,89 +493,3 @@ class Query: @strawberry.field(default_factory=lambda: "Example C") def c(self) -> str: return "I'm a resolver" - - -def test_with_types(): - # Ensures Schema(types=[...]) works with all data types - @strawberry.type - class Type: - foo: int - - @strawberry.interface - class Interface: - foo: int - - @strawberry.input - class Input: - foo: int - - @strawberry.type - class Query: - foo: int - - @strawberry.schema_directive(locations=[Location.SCALAR], name="specifiedBy") - class SpecifiedBy: - name: str - - schema = strawberry.Schema( - query=Query, types=[Type, Interface, Input, Base64, ID, str, int, SpecifiedBy] - ) - expected = ''' - directive @specifiedBy(name: String!) on SCALAR - - """ - Represents binary data as Base64-encoded strings, using the standard alphabet. - """ - scalar Base64 @specifiedBy(url: "https://datatracker.ietf.org/doc/html/rfc4648.html#section-4") - - input Input { - foo: Int! - } - - interface Interface { - foo: Int! - } - - type Query { - foo: Int! - } - - type Type { - foo: Int! - } - ''' # noqa: E501 - - assert str(schema) == textwrap.dedent(expected).strip() - - -def test_with_types_non_named(): - @strawberry.type - class Query: - foo: int - - with pytest.raises(TypeError, match=r"\[Int!\] is not a named GraphQL Type"): - strawberry.Schema(query=Query, types=[StrawberryList(int)]) - - -def test_kw_only(): - @strawberry.type - class FooBar1: - foo: int = 1 - bar: int - - @strawberry.type - class FooBar2: - foo: int = strawberry.field(default=1) - bar: int = strawberry.field() - - for FooBar in (FooBar1, FooBar2): - with pytest.raises( - TypeError, match="missing 1 required keyword-only argument: 'bar'" - ): - FooBar() - with pytest.raises( - TypeError, match="missing 1 required keyword-only argument: 'bar'" - ): - FooBar(foo=1) - FooBar(bar=2) - FooBar(foo=1, bar=2) diff --git a/tests/schema/test_custom_scalar.py b/tests/schema/test_custom_scalar.py index 848fb01754..aa6f2513fb 100644 --- a/tests/schema/test_custom_scalar.py +++ b/tests/schema/test_custom_scalar.py @@ -3,6 +3,7 @@ import strawberry + Base64Encoded = strawberry.scalar( NewType("Base64Encoded", bytes), serialize=base64.b64encode, diff --git a/tests/schema/test_directives.py b/tests/schema/test_directives.py index a748cd3f61..439081bd3d 100644 --- a/tests/schema/test_directives.py +++ b/tests/schema/test_directives.py @@ -1,14 +1,12 @@ import textwrap -from enum import Enum -from typing import Dict, List, Optional +from typing import List import pytest import strawberry -from strawberry.directive import DirectiveLocation, DirectiveValue +from strawberry.directive import DirectiveLocation from strawberry.extensions import Extension from strawberry.schema.config import StrawberryConfig -from strawberry.types.info import Info from strawberry.utils.await_maybe import await_maybe @@ -32,11 +30,7 @@ def person(self) -> Person: }""" schema = strawberry.Schema(query=Query) - result = schema.execute_sync( - query, - variable_values={"includePoints": False}, - context_value={"username": "foo"}, - ) + result = schema.execute_sync(query, variable_values={"includePoints": False}) assert not result.errors assert result.data["person"] == {"name": "Jess"} @@ -120,42 +114,6 @@ def uppercase(value: str, example: str): assert schema.as_str() == textwrap.dedent(expected_schema).strip() -def test_directive_arguments_without_value_param(): - """Regression test for Strawberry Issue #1666. - - https://github.com/strawberry-graphql/strawberry/issues/1666 - """ - - @strawberry.type - class Query: - cake: str = "victoria sponge" - - @strawberry.directive( - locations=[DirectiveLocation.FIELD], - description="Don't actually like cake? try ice cream instead", - ) - def ice_cream(flavor: str): - return f"{flavor} ice cream" - - schema = strawberry.Schema(query=Query, directives=[ice_cream]) - - expected_schema = ''' - """Don't actually like cake? try ice cream instead""" - directive @iceCream(flavor: String!) on FIELD - - type Query { - cake: String! - } - ''' - - assert schema.as_str() == textwrap.dedent(expected_schema).strip() - - query = 'query { cake @iceCream(flavor: "strawberry") }' - result = schema.execute_sync(query, root_value=Query()) - - assert result.data == {"cake": "strawberry ice cream"} - - def test_runs_directives(): @strawberry.type class Person: @@ -387,276 +345,3 @@ async def resolve(self, _next, root, info, *args, **kwargs): assert not result.errors assert result.data assert result.data["person"]["name"] == "JESS" - - -@pytest.fixture -def info_directive_schema() -> strawberry.Schema: - """Returns a schema with directive that validates if info is recieved.""" - - @strawberry.enum - class Locale(Enum): - EN: str = "EN" - NL: str = "NL" - - greetings: Dict[Locale, str] = { - Locale.EN: "Hello {username}", - Locale.NL: "Hallo {username}", - } - - @strawberry.type - class Query: - @strawberry.field - def greetingTemplate(self, locale: Locale = Locale.EN) -> str: - return greetings[locale] - - field = Query._type_definition.fields[0] # type: ignore - - @strawberry.directive( - locations=[DirectiveLocation.FIELD], - description="Interpolate string on the server from context data", - ) - def interpolate(value: str, info: Info): - try: - assert isinstance(info, Info) - assert info._field is field - return value.format(**info.context["userdata"]) - except KeyError: - return value - - return strawberry.Schema(query=Query, directives=[interpolate]) - - -def test_info_directive_schema(info_directive_schema: strawberry.Schema): - - expected_schema = ''' - """Interpolate string on the server from context data""" - directive @interpolate on FIELD - - enum Locale { - EN - NL - } - - type Query { - greetingTemplate(locale: Locale! = EN): String! - } - ''' - - assert textwrap.dedent(expected_schema).strip() == str(info_directive_schema) - - -def test_info_directive(info_directive_schema: strawberry.Schema): - query = "query { greetingTemplate @interpolate }" - result = info_directive_schema.execute_sync( - query, context_value={"userdata": {"username": "Foo"}} - ) - assert result.data == {"greetingTemplate": "Hello Foo"} - - -@pytest.mark.asyncio -async def test_info_directive_async(info_directive_schema: strawberry.Schema): - query = "query { greetingTemplate @interpolate }" - result = await info_directive_schema.execute( - query, context_value={"userdata": {"username": "Foo"}} - ) - assert result.data == {"greetingTemplate": "Hello Foo"} - - -def test_directive_value(): - """Tests if directive value is detected by type instead of by arg-name `value`.""" - - @strawberry.type - class Cake: - frosting: Optional[str] = None - flavor: str = "Chocolate" - - @strawberry.type - class Query: - @strawberry.field - def cake(self) -> Cake: - return Cake() - - @strawberry.directive( - locations=[DirectiveLocation.FIELD], - description="Add frostring with ``flavor`` to a cake.", - ) - def add_frosting(flavor: str, v: DirectiveValue[Cake], value: str): - assert isinstance(v, Cake) - assert value == "foo" # Check if value can be used as an argument - v.frosting = flavor - return v - - schema = strawberry.Schema(query=Query, directives=[add_frosting]) - result = schema.execute_sync( - """query { - cake @addFrosting(flavor: "Vanilla", value: "foo") { - frosting - flavor - } - } - """ - ) - assert result.data == {"cake": {"frosting": "Vanilla", "flavor": "Chocolate"}} - - -# Defined in module scope to allow the FowardRef to be resolvable with eval -@strawberry.directive( - locations=[DirectiveLocation.FIELD], - description="Add frostring with ``flavor`` to a cake.", -) -def add_frosting(flavor: str, v: DirectiveValue["Cake"], value: str): - assert isinstance(v, Cake) - assert value == "foo" - v.frosting = flavor - return v - - -@strawberry.type -class Query: - @strawberry.field - def cake(self) -> "Cake": - return Cake() - - -@strawberry.type -class Cake: - frosting: Optional[str] = None - flavor: str = "Chocolate" - - -def test_directive_value_forward_ref(): - """Tests if directive value by type works with PEP-563.""" - schema = strawberry.Schema(query=Query, directives=[add_frosting]) - result = schema.execute_sync( - """query { - cake @addFrosting(flavor: "Vanilla", value: "foo") { - frosting - flavor - } - } - """ - ) - assert result.data == {"cake": {"frosting": "Vanilla", "flavor": "Chocolate"}} - - -def test_name_first_directive_value(): - @strawberry.type - class Query: - @strawberry.field - def greeting(self) -> str: - return "Hi" - - @strawberry.directive(locations=[DirectiveLocation.FIELD]) - def personalize_greeting(value: str, v: DirectiveValue[str]): - assert v == "Hi" - return f"{v} {value}" - - schema = strawberry.Schema(Query, directives=[personalize_greeting]) - result = schema.execute_sync('{ greeting @personalizeGreeting(value: "Bar")}') - - assert result.data is not None - assert not result.errors - assert result.data["greeting"] == "Hi Bar" - - -def test_named_based_directive_value_is_deprecated(): - - with pytest.deprecated_call(match=r"Argument name-based matching of 'value'"): - - @strawberry.type - class Query: - hello: str = "hello" - - @strawberry.directive(locations=[DirectiveLocation.FIELD]) - def deprecated_value(value): - ... - - strawberry.Schema(query=Query, directives=[deprecated_value]) - - -@pytest.mark.xfail( - reason="List arguments are not yet supported", raises=AttributeError, strict=True -) -@pytest.mark.asyncio -async def test_directive_list_argument(): - @strawberry.type - class Query: - @strawberry.field - def greeting(self) -> str: - return "Hi" - - @strawberry.directive(locations=[DirectiveLocation.FIELD]) - def append_names(value: DirectiveValue[str], names: List[str]): - assert isinstance(names, list) - return f"{value} {', '.join(names)}" - - schema = strawberry.Schema(query=Query, directives=[append_names]) - - result = await schema.execute( - 'query { greeting @appendNames(names: ["foo", "bar"])}' - ) - - assert result.errors - raise result.errors[0].original_error # type: ignore - - -def test_directives_with_custom_types(): - @strawberry.input - class DirectiveInput: - example: str - - @strawberry.type - class Query: - cake: str = "made_in_switzerland" - - @strawberry.directive( - locations=[DirectiveLocation.FIELD], description="Make string uppercase" - ) - def uppercase(value: str, input: DirectiveInput): - return value.upper() - - schema = strawberry.Schema(query=Query, directives=[uppercase]) - - expected_schema = ''' - """Make string uppercase""" - directive @uppercase(input: DirectiveInput!) on FIELD - - input DirectiveInput { - example: String! - } - - type Query { - cake: String! - } - ''' - - assert schema.as_str() == textwrap.dedent(expected_schema).strip() - - -def test_directives_with_scalar(): - DirectiveInput = strawberry.scalar(str, name="DirectiveInput") - - @strawberry.type - class Query: - cake: str = "made_in_switzerland" - - @strawberry.directive( - locations=[DirectiveLocation.FIELD], description="Make string uppercase" - ) - def uppercase(value: str, input: DirectiveInput): - return value.upper() - - schema = strawberry.Schema(query=Query, directives=[uppercase]) - - expected_schema = ''' - """Make string uppercase""" - directive @uppercase(input: DirectiveInput!) on FIELD - - scalar DirectiveInput - - type Query { - cake: String! - } - ''' - - assert schema.as_str() == textwrap.dedent(expected_schema).strip() diff --git a/tests/schema/test_duplicated_types.py b/tests/schema/test_duplicated_types.py deleted file mode 100644 index 0eb2c958a4..0000000000 --- a/tests/schema/test_duplicated_types.py +++ /dev/null @@ -1,158 +0,0 @@ -import textwrap -from enum import Enum -from typing import Generic, TypeVar - -import pytest - -import strawberry -from strawberry.exceptions import DuplicatedTypeName - - -@pytest.mark.raises_strawberry_exception( - DuplicatedTypeName, - match=r"Type (.*) is defined multiple times in the schema", -) -def test_schema_has_no_duplicated_input_types(): - @strawberry.input(name="DuplicatedInput") - class A: - a: int - - @strawberry.input(name="DuplicatedInput") - class B: - b: int - - @strawberry.type - class Query: - field: int - - strawberry.Schema(query=Query, types=[A, B]) - - -@pytest.mark.raises_strawberry_exception( - DuplicatedTypeName, - match=r"Type (.*) is defined multiple times in the schema", -) -def test_schema_has_no_duplicated_types(): - @strawberry.type(name="DuplicatedType") - class A: - a: int - - @strawberry.type(name="DuplicatedType") - class B: - b: int - - @strawberry.type - class Query: - field: int - - strawberry.Schema(query=Query, types=[A, B]) - - -@pytest.mark.raises_strawberry_exception( - DuplicatedTypeName, - match=r"Type (.*) is defined multiple times in the schema", -) -def test_schema_has_no_duplicated_interfaces(): - @strawberry.interface(name="DuplicatedType") - class A: - a: int - - @strawberry.interface(name="DuplicatedType") - class B: - b: int - - @strawberry.type - class Query: - pass - - strawberry.Schema(query=Query, types=[A, B]) - - -@pytest.mark.raises_strawberry_exception( - DuplicatedTypeName, - match=r"Type (.*) is defined multiple times in the schema", -) -def test_schema_has_no_duplicated_enums(): - @strawberry.enum(name="DuplicatedType") - class A(Enum): - A = 1 - - @strawberry.enum(name="DuplicatedType") - class B(Enum): - B = 1 - - @strawberry.type - class Query: - field: int - - strawberry.Schema(query=Query, types=[A, B]) - - -@pytest.mark.raises_strawberry_exception( - DuplicatedTypeName, - match=r"Type (.*) is defined multiple times in the schema", -) -def test_schema_has_no_duplicated_names_across_different_types(): - @strawberry.interface(name="DuplicatedType") - class A: - a: int - - @strawberry.type(name="DuplicatedType") - class B: - b: int - - @strawberry.type - class Query: - field: int - - strawberry.Schema(query=Query, types=[A, B]) - - -@pytest.mark.raises_strawberry_exception( - DuplicatedTypeName, - match=r"Type (.*) is defined multiple times in the schema", -) -def test_schema_has_no_duplicated_types_between_schema_and_extra_types(): - @strawberry.type(name="DuplicatedType") - class A: - a: int - - @strawberry.type(name="DuplicatedType") - class B: - b: int - - @strawberry.type - class Query: - field: A - - strawberry.Schema(query=Query, types=[B]) - - -def test_allows_multiple_instance_of_same_generic(): - T = TypeVar("T") - - @strawberry.type - class A(Generic[T]): - a: T - - @strawberry.type - class Query: - first: A[int] - second: A[int] - - schema = strawberry.Schema(Query) - - expected_schema = textwrap.dedent( - """ - type IntA { - a: Int! - } - - type Query { - first: IntA! - second: IntA! - } - """ - ).strip() - - assert str(schema) == expected_schema diff --git a/tests/schema/test_enum.py b/tests/schema/test_enum.py index 244d8f212a..323ad66af8 100644 --- a/tests/schema/test_enum.py +++ b/tests/schema/test_enum.py @@ -2,12 +2,10 @@ from enum import Enum from textwrap import dedent from typing import List, Optional -from typing_extensions import Annotated import pytest import strawberry -from strawberry.lazy_type import lazy def test_enum_resolver(): @@ -102,25 +100,6 @@ def eat_cone(self, input: ConeInput) -> bool: assert result.data["eatCone"] is True -def test_lazy_enum_arguments(): - LazyEnum = Annotated[ - "LazyEnum", lazy("tests.schema.test_lazy_types.test_lazy_enums") - ] - - @strawberry.type - class Query: - @strawberry.field - def something(self, enum: LazyEnum) -> LazyEnum: - return enum - - schema = strawberry.Schema(query=Query) - - query = "{ something(enum: BREAD) }" - result = schema.execute_sync(query) - assert not result.errors - assert result.data["something"] == "BREAD" - - def test_enum_falsy_values(): @strawberry.enum class IceCreamFlavour(Enum): @@ -284,16 +263,11 @@ def create_flavour(self, flavour: IceCreamFlavour) -> str: assert str(schema) == expected query = "{ createFlavour(flavour: CHOCOLATE) }" + result = schema.execute_sync(query) - assert not result.errors - assert result.data["createFlavour"] == "CHOCOLATE" - # Explicitly using `variable_values` now so that the enum is parsed using - # `CustomGraphQLEnumType.parse_value()` instead of `.parse_literal` - query = "query ($flavour: IceCreamFlavour!) { createFlavour(flavour: $flavour) }" - result = schema.execute_sync(query, variable_values={"flavour": "VANILLA"}) assert not result.errors - assert result.data["createFlavour"] == "VANILLA" + assert result.data["createFlavour"] == "CHOCOLATE" def test_enum_as_default_argument(): @@ -360,75 +334,3 @@ def best_flavour(self) -> IceCreamFlavour: assert not result.errors assert result.data["bestFlavour"] == "STRAWBERRY" - - -def test_enum_deprecated_value(): - @strawberry.enum - class IceCreamFlavour(Enum): - VANILLA = "vanilla" - STRAWBERRY = strawberry.enum_value( - "strawberry", deprecation_reason="We ran out" - ) - CHOCOLATE = strawberry.enum_value("chocolate") - - @strawberry.type - class Query: - @strawberry.field - def best_flavour(self) -> IceCreamFlavour: - return IceCreamFlavour.STRAWBERRY - - schema = strawberry.Schema(query=Query) - - query = """ - { - __type(name: "IceCreamFlavour") { - enumValues(includeDeprecated: true) { - name - isDeprecated - deprecationReason - } - } - } - """ - - result = schema.execute_sync(query) - - assert not result.errors - assert result.data - assert result.data["__type"]["enumValues"] == [ - {"deprecationReason": None, "isDeprecated": False, "name": "VANILLA"}, - {"deprecationReason": "We ran out", "isDeprecated": True, "name": "STRAWBERRY"}, - {"deprecationReason": None, "isDeprecated": False, "name": "CHOCOLATE"}, - ] - - -def test_can_use_enum_values_in_input(): - @strawberry.enum - class TestEnum(Enum): - A = "A" - B = strawberry.enum_value("B") - C = strawberry.enum_value("Coconut", deprecation_reason="We ran out") - - @strawberry.type - class Query: - @strawberry.field - def receive_enum(self, test: TestEnum) -> str: - return str(test) - - schema = strawberry.Schema(query=Query) - - query = """ - query { - a: receiveEnum(test: A) - b: receiveEnum(test: B) - c: receiveEnum(test: C) - } - """ - - result = schema.execute_sync(query) - - assert not result.errors - assert result.data - assert result.data["a"] == "TestEnum.A" - assert result.data["b"] == "TestEnum.B" - assert result.data["c"] == "TestEnum.C" diff --git a/tests/schema/test_execution.py b/tests/schema/test_execution.py index 2ed854df6a..3ff2ffcfbb 100644 --- a/tests/schema/test_execution.py +++ b/tests/schema/test_execution.py @@ -4,6 +4,7 @@ from unittest.mock import patch import pytest + from graphql import GraphQLError, ValidationRule, validate import strawberry @@ -182,7 +183,6 @@ def example(self) -> int: root_value=Query(), ) - assert result.errors assert len(result.errors) == 1 # Exception was logged @@ -240,148 +240,6 @@ def example(self) -> int: assert record.exc_info[0] is TypeError -@pytest.mark.asyncio -async def test_logging_parsing_error(caplog): - @strawberry.type - class Query: - @strawberry.field - def example(self) -> str: - return "hi" - - schema = strawberry.Schema(query=Query) - - query = """ - query { - example - """ - - result = await schema.execute( - query, - root_value=Query(), - ) - - assert result.errors - assert len(result.errors) == 1 - - # Exception was logged - assert len(caplog.records) == 1 - record = caplog.records[0] - - assert record.levelname == "ERROR" - assert record.name == "strawberry.execution" - assert "Syntax Error" in record.message - - -def test_logging_parsing_error_sync(caplog): - @strawberry.type - class Query: - @strawberry.field - def example(self) -> str: - return "hi" - - schema = strawberry.Schema(query=Query) - - query = """ - query { - example - """ - - result = schema.execute_sync( - query, - root_value=Query(), - ) - - assert result.errors - assert len(result.errors) == 1 - - # Exception was logged - assert len(caplog.records) == 1 - record = caplog.records[0] - - assert record.levelname == "ERROR" - assert record.name == "strawberry.execution" - assert "Syntax Error" in record.message - - -@pytest.mark.asyncio -async def test_logging_validation_errors(caplog): - @strawberry.type - class Query: - @strawberry.field - def example(self) -> str: - return "hi" - - schema = strawberry.Schema(query=Query) - - query = """ - query { - example { - foo - } - missingField - } - """ - - result = await schema.execute( - query, - root_value=Query(), - ) - - assert result.errors - assert len(result.errors) == 2 - - # Exception was logged - assert len(caplog.records) == 2 - record1 = caplog.records[0] - assert record1.levelname == "ERROR" - assert record1.name == "strawberry.execution" - assert "Field 'example' must not have a selection" in record1.message - - record2 = caplog.records[1] - assert record2.levelname == "ERROR" - assert record2.name == "strawberry.execution" - assert "Cannot query field 'missingField'" in record2.message - - -def test_logging_validation_errors_sync(caplog): - @strawberry.type - class Query: - @strawberry.field - def example(self) -> str: - return "hi" - - schema = strawberry.Schema(query=Query) - - query = """ - query { - example { - foo - } - missingField - } - """ - - result = schema.execute_sync( - query, - root_value=Query(), - ) - - assert result.errors - assert len(result.errors) == 2 - - # Exception was logged - assert len(caplog.records) == 2 - record1 = caplog.records[0] - assert record1.levelname == "ERROR" - assert record1.name == "strawberry.execution" - assert "Field 'example' must not have a selection" in record1.message - - record2 = caplog.records[1] - assert record2.levelname == "ERROR" - assert record2.name == "strawberry.execution" - assert "Cannot query field 'missingField'" in record2.message - - def test_overriding_process_errors(caplog): @strawberry.type class Query: @@ -414,7 +272,7 @@ def process_errors(self, errors, execution_context): assert result.errors == execution_errors # Exception wasn't logged - assert caplog.records == [] + assert len(caplog.records) == 0 def test_adding_custom_validation_rules(): diff --git a/tests/schema/test_extensions.py b/tests/schema/test_extensions.py deleted file mode 100644 index 53488f489a..0000000000 --- a/tests/schema/test_extensions.py +++ /dev/null @@ -1,189 +0,0 @@ -from enum import Enum, auto -from typing import cast - -from graphql import ( - DirectiveLocation, - GraphQLEnumType, - GraphQLInputType, - GraphQLObjectType, - GraphQLSchema, -) - -import strawberry -from strawberry.scalars import JSON -from strawberry.schema.schema_converter import GraphQLCoreConverter -from strawberry.schema_directive import Location - -DEFINITION_BACKREF = GraphQLCoreConverter.DEFINITION_BACKREF - - -def test_extensions_schema_directive(): - @strawberry.schema_directive(locations=[Location.OBJECT, Location.INPUT_OBJECT]) - class SchemaDirective: - name: str - - @strawberry.type(directives=[SchemaDirective(name="Query")]) - class Query: - hello: str - - schema = strawberry.Schema(query=Query) - graphql_schema: GraphQLSchema = schema._schema - - # Schema - assert graphql_schema.extensions[DEFINITION_BACKREF] is schema - - """ - FIXME: Apparently I stumbled on a bug: - SchemaDirective are used on schema.__str__(), - but aren't added to graphql_schema.directives - - graphql_scheme_directive = graphql_schema.get_directive("schemaDirective") - """ - graphql_scheme_directive = schema.schema_converter.from_schema_directive( - Query._type_definition.directives[0] - ) - assert ( - graphql_scheme_directive.extensions[DEFINITION_BACKREF] - is SchemaDirective.__strawberry_directive__ - ) - - -def test_directive(): - @strawberry.directive(locations=[DirectiveLocation.FIELD]) - def uppercase(value: str, foo: str): - return value.upper() - - @strawberry.type() - class Query: - hello: str - - schema = strawberry.Schema(query=Query, directives=[uppercase]) - graphql_schema: GraphQLSchema = schema._schema - - graphql_directive = graphql_schema.get_directive("uppercase") - assert graphql_directive.extensions[DEFINITION_BACKREF] is uppercase - assert ( - graphql_directive.args["foo"].extensions[DEFINITION_BACKREF] - is uppercase.arguments[0] - ) - - -def test_enum(): - @strawberry.enum - class ThingType(Enum): - JSON = auto() - STR = auto() - - @strawberry.type() - class Query: - hello: ThingType - - schema = strawberry.Schema(query=Query) - graphql_schema: GraphQLSchema = schema._schema - - graphql_thing_type = cast(GraphQLEnumType, graphql_schema.get_type("ThingType")) - assert ( - graphql_thing_type.extensions[DEFINITION_BACKREF] is ThingType._enum_definition - ) - assert ( - graphql_thing_type.values["JSON"].extensions[DEFINITION_BACKREF] - is ThingType._enum_definition.values[0] - ) - assert ( - graphql_thing_type.values["STR"].extensions[DEFINITION_BACKREF] - is ThingType._enum_definition.values[1] - ) - - -def test_scalar(): - @strawberry.type() - class Query: - hello: JSON - hi: str - - schema = strawberry.Schema(query=Query) - graphql_schema: GraphQLSchema = schema._schema - - assert ( - graphql_schema.get_type("JSON").extensions[DEFINITION_BACKREF] - is JSON._scalar_definition - ) - - -def test_interface(): - @strawberry.interface - class Thing: - name: str - - @strawberry.type() - class Query: - hello: Thing - - schema = strawberry.Schema(query=Query) - graphql_schema: GraphQLSchema = schema._schema - - assert ( - graphql_schema.get_type("Thing").extensions[DEFINITION_BACKREF] - is Thing._type_definition - ) - - -def test_union(): - @strawberry.type - class JsonThing: - value: JSON - - @strawberry.type - class StrThing: - value: str - - SomeThing = strawberry.union("SomeThing", types=[JsonThing, StrThing]) - - @strawberry.type() - class Query: - hello: SomeThing - - schema = strawberry.Schema(query=Query) - graphql_schema: GraphQLSchema = schema._schema - - assert ( - graphql_schema.get_type("SomeThing").extensions[DEFINITION_BACKREF] is SomeThing - ) - - -def test_object_types(): - @strawberry.input - class Input: - name: str - - @strawberry.type() - class Query: - @strawberry.field - def hello(self, input: Input) -> str: - ... - - schema = strawberry.Schema(query=Query) - graphql_schema: GraphQLSchema = schema._schema - - assert ( - graphql_schema.get_type("Input").extensions[DEFINITION_BACKREF] - is Input._type_definition - ) - assert ( - graphql_schema.get_type("Query").extensions[DEFINITION_BACKREF] - is Query._type_definition - ) - - graphql_query = cast(GraphQLObjectType, graphql_schema.get_type("Query")) - assert graphql_query.fields["hello"].extensions[ - DEFINITION_BACKREF - ] is Query._type_definition.get_field("hello") - assert ( - graphql_query.fields["hello"].args["input"].extensions[DEFINITION_BACKREF] - is Query._type_definition.get_field("hello").arguments[0] - ) - - graphql_input = cast(GraphQLInputType, graphql_schema.get_type("Input")) - assert graphql_input.fields["name"].extensions[ - DEFINITION_BACKREF - ] is Input._type_definition.get_field("name") diff --git a/tests/schema/test_fields.py b/tests/schema/test_fields.py deleted file mode 100644 index 0bcdd02d13..0000000000 --- a/tests/schema/test_fields.py +++ /dev/null @@ -1,149 +0,0 @@ -import dataclasses -import textwrap -from operator import getitem - -import strawberry -from strawberry.field import StrawberryField -from strawberry.printer import print_schema -from strawberry.schema.config import StrawberryConfig - - -def test_custom_field(): - class CustomField(StrawberryField): - def get_result(self, root, info, args, kwargs): - return getattr(root, self.python_name) * 2 - - @strawberry.type - class Query: - a: str = CustomField(default="Example") # type: ignore - - schema = strawberry.Schema(query=Query) - - query = "{ a }" - - result = schema.execute_sync(query, root_value=Query()) - - assert not result.errors - assert result.data == {"a": "ExampleExample"} - - -def test_default_resolver_gets_attribute(): - @strawberry.type - class User: - name: str - - @strawberry.type - class Query: - @strawberry.field - def user(self) -> User: - return User(name="Patrick") - - schema = strawberry.Schema(query=Query) - - query = "{ user { name } }" - - result = schema.execute_sync(query) - - assert not result.errors - assert result.data - assert result.data["user"]["name"] == "Patrick" - - -def test_can_change_default_resolver(): - @strawberry.type - class User: - name: str - - @strawberry.type - class Query: - @strawberry.field - def user(self) -> User: - return {"name": "Patrick"} # type: ignore - - schema = strawberry.Schema( - query=Query, - config=StrawberryConfig(default_resolver=getitem), - ) - - query = "{ user { name } }" - - result = schema.execute_sync(query) - - assert not result.errors - assert result.data - assert result.data["user"]["name"] == "Patrick" - - -def test_field_metadata(): - @strawberry.type - class Query: - a: str = strawberry.field(default="Example", metadata={"Foo": "Bar"}) - - (a,) = dataclasses.fields(Query) - assert a.metadata == {"Foo": "Bar"} - - -def test_field_type_priority(): - """ - Prioritise the field annotation on the class over the resolver annotation. - """ - - def my_resolver() -> str: - return "1.33" - - @strawberry.type - class Query: - a: float = strawberry.field(resolver=my_resolver) - - schema = strawberry.Schema(Query) - - expected = """ - type Query { - a: Float! - } - """ - - assert print_schema(schema) == textwrap.dedent(expected).strip() - - query = "{ a }" - - result = schema.execute_sync(query, root_value=Query()) - - assert not result.errors - assert result.data == { - "a": 1.33, - } - - -def test_field_type_override(): - @strawberry.type - class Query: - a: float = strawberry.field(graphql_type=str) - b = strawberry.field(graphql_type=int) - - @strawberry.field(graphql_type=float) - def c(self): - return "3.4" - - schema = strawberry.Schema(Query) - - expected = """ - type Query { - a: String! - b: Int! - c: Float! - } - """ - - assert print_schema(schema) == textwrap.dedent(expected).strip() - - query = "{ a, b, c }" - - result = schema.execute_sync(query, root_value=Query(a=1.33, b=2)) - - assert not result.errors - assert result.data == { - "a": "1.33", - "b": 2, - "c": 3.4, - } diff --git a/tests/schema/test_generics.py b/tests/schema/test_generics.py index f5f3b97f38..fb40e839ad 100644 --- a/tests/schema/test_generics.py +++ b/tests/schema/test_generics.py @@ -1,7 +1,6 @@ import textwrap +import typing from enum import Enum -from typing import Any, Generic, List, Optional, TypeVar, Union -from typing_extensions import Self import pytest @@ -9,10 +8,10 @@ def test_supports_generic_simple_type(): - T = TypeVar("T") + T = typing.TypeVar("T") @strawberry.type - class Edge(Generic[T]): + class Edge(typing.Generic[T]): cursor: strawberry.ID node_field: T @@ -41,10 +40,10 @@ def example(self) -> Edge[int]: def test_supports_generic(): - T = TypeVar("T") + T = typing.TypeVar("T") @strawberry.type - class Edge(Generic[T]): + class Edge(typing.Generic[T]): cursor: strawberry.ID node: T @@ -83,11 +82,11 @@ def example(self) -> Edge[Person]: def test_supports_multiple_generic(): - A = TypeVar("A") - B = TypeVar("B") + A = typing.TypeVar("A") + B = typing.TypeVar("B") @strawberry.type - class Multiple(Generic[A, B]): + class Multiple(typing.Generic[A, B]): a: A b: B @@ -116,25 +115,25 @@ def multiple(self) -> Multiple[int, str]: def test_support_nested_generics(): - T = TypeVar("T") + T = typing.TypeVar("T") @strawberry.type class User: name: str @strawberry.type - class Edge(Generic[T]): + class Edge(typing.Generic[T]): node: T @strawberry.type - class Connection(Generic[T]): + class Connection(typing.Generic[T]): edge: Edge[T] @strawberry.type class Query: @strawberry.field def users(self) -> Connection[User]: - return Connection(edge=Edge(node=User(name="Patrick"))) + return Connection(edge=Edge(node=User("Patrick"))) schema = strawberry.Schema(query=Query) @@ -162,15 +161,15 @@ def users(self) -> Connection[User]: def test_supports_optional(): - T = TypeVar("T") + T = typing.TypeVar("T") @strawberry.type class User: name: str @strawberry.type - class Edge(Generic[T]): - node: Optional[T] = None + class Edge(typing.Generic[T]): + node: typing.Optional[T] = None @strawberry.type class Query: @@ -196,15 +195,15 @@ def user(self) -> Edge[User]: def test_supports_lists(): - T = TypeVar("T") + T = typing.TypeVar("T") @strawberry.type class User: name: str @strawberry.type - class Edge(Generic[T]): - nodes: List[T] + class Edge(typing.Generic[T]): + nodes: typing.List[T] @strawberry.type class Query: @@ -230,15 +229,15 @@ def user(self) -> Edge[User]: def test_supports_lists_of_optionals(): - T = TypeVar("T") + T = typing.TypeVar("T") @strawberry.type class User: name: str @strawberry.type - class Edge(Generic[T]): - nodes: List[Optional[T]] + class Edge(typing.Generic[T]): + nodes: typing.List[typing.Optional[T]] @strawberry.type class Query: @@ -264,19 +263,19 @@ def user(self) -> Edge[User]: def test_can_extend_generics(): - T = TypeVar("T") + T = typing.TypeVar("T") @strawberry.type class User: name: str @strawberry.type - class Edge(Generic[T]): + class Edge(typing.Generic[T]): node: T @strawberry.type - class Connection(Generic[T]): - edges: List[Edge[T]] + class Connection(typing.Generic[T]): + edges: typing.List[Edge[T]] @strawberry.type class ConnectionWithMeta(Connection[T]): @@ -286,9 +285,7 @@ class ConnectionWithMeta(Connection[T]): class Query: @strawberry.field def users(self) -> ConnectionWithMeta[User]: - return ConnectionWithMeta( - meta="123", edges=[Edge(node=User(name="Patrick"))] - ) + return ConnectionWithMeta(meta="123", edges=[Edge(node=User("Patrick"))]) schema = strawberry.Schema(query=Query) @@ -318,10 +315,10 @@ def users(self) -> ConnectionWithMeta[User]: def test_supports_generic_in_unions(): - T = TypeVar("T") + T = typing.TypeVar("T") @strawberry.type - class Edge(Generic[T]): + class Edge(typing.Generic[T]): cursor: strawberry.ID node: T @@ -332,7 +329,7 @@ class Fallback: @strawberry.type class Query: @strawberry.field - def example(self) -> Union[Fallback, Edge[int]]: + def example(self) -> typing.Union[Fallback, Edge[int]]: return Edge(cursor=strawberry.ID("1"), node=1) schema = strawberry.Schema(query=Query) @@ -357,14 +354,14 @@ def example(self) -> Union[Fallback, Edge[int]]: def test_generic_with_enum_as_param_of_type_inside_unions(): - T = TypeVar("T") + T = typing.TypeVar("T") @strawberry.type class Pet: name: str @strawberry.type - class ErrorNode(Generic[T]): + class ErrorNode(typing.Generic[T]): code: T @strawberry.enum @@ -375,7 +372,7 @@ class Codes(Enum): @strawberry.type class Query: @strawberry.field - def result(self) -> Union[Pet, ErrorNode[Codes]]: + def result(self) -> typing.Union[Pet, ErrorNode[Codes]]: return ErrorNode(code=Codes.a) schema = strawberry.Schema(query=Query) @@ -395,53 +392,12 @@ def result(self) -> Union[Pet, ErrorNode[Codes]]: assert result.data == {"result": {"__typename": "CodesErrorNode", "code": "a"}} -def test_generic_with_enum(): - T = TypeVar("T") - - @strawberry.enum - class EstimatedValueEnum(Enum): - test = "test" - testtest = "testtest" - - @strawberry.type - class EstimatedValue(Generic[T]): - value: T - type: EstimatedValueEnum - - @strawberry.type - class Query: - @strawberry.field - def estimated_value(self) -> Optional[EstimatedValue[int]]: - return EstimatedValue(value=1, type=EstimatedValueEnum.test) - - schema = strawberry.Schema(query=Query) - - query = """{ - estimatedValue { - __typename - value - type - } - }""" - - result = schema.execute_sync(query) - - assert not result.errors - assert result.data == { - "estimatedValue": { - "__typename": "IntEstimatedValue", - "value": 1, - "type": "test", - } - } - - def test_supports_generic_in_unions_multiple_vars(): - A = TypeVar("A") - B = TypeVar("B") + A = typing.TypeVar("A") + B = typing.TypeVar("B") @strawberry.type - class Edge(Generic[A, B]): + class Edge(typing.Generic[A, B]): info: A node: B @@ -452,7 +408,7 @@ class Fallback: @strawberry.type class Query: @strawberry.field - def example(self) -> Union[Fallback, Edge[int, str]]: + def example(self) -> typing.Union[Fallback, Edge[int, str]]: return Edge(node="string", info=1) schema = strawberry.Schema(query=Query) @@ -477,18 +433,18 @@ def example(self) -> Union[Fallback, Edge[int, str]]: def test_supports_generic_in_unions_with_nesting(): - T = TypeVar("T") + T = typing.TypeVar("T") @strawberry.type class User: name: str @strawberry.type - class Edge(Generic[T]): + class Edge(typing.Generic[T]): node: T @strawberry.type - class Connection(Generic[T]): + class Connection(typing.Generic[T]): edge: Edge[T] @strawberry.type @@ -498,8 +454,8 @@ class Fallback: @strawberry.type class Query: @strawberry.field - def users(self) -> Union[Connection[User], Fallback]: - return Connection(edge=Edge(node=User(name="Patrick"))) + def users(self) -> typing.Union[Connection[User], Fallback]: + return Connection(edge=Edge(node=User("Patrick"))) schema = strawberry.Schema(query=Query) @@ -529,17 +485,17 @@ def users(self) -> Union[Connection[User], Fallback]: def test_supports_multiple_generics_in_union(): - T = TypeVar("T") + T = typing.TypeVar("T") @strawberry.type - class Edge(Generic[T]): + class Edge(typing.Generic[T]): cursor: strawberry.ID node: T @strawberry.type class Query: @strawberry.field - def example(self) -> List[Union[Edge[int], Edge[str]]]: + def example(self) -> typing.List[typing.Union[Edge[int], Edge[str]]]: return [ Edge(cursor=strawberry.ID("1"), node=1), Edge(cursor=strawberry.ID("2"), node="string"), @@ -595,10 +551,10 @@ def example(self) -> List[Union[Edge[int], Edge[str]]]: def test_generated_names(): - T = TypeVar("T") + T = typing.TypeVar("T") @strawberry.type - class EdgeWithCursor(Generic[T]): + class EdgeWithCursor(typing.Generic[T]): cursor: strawberry.ID node: T @@ -639,21 +595,21 @@ def person_edge(self) -> EdgeWithCursor[SpecialPerson]: def test_supports_lists_within_unions(): - T = TypeVar("T") + T = typing.TypeVar("T") @strawberry.type class User: name: str @strawberry.type - class Edge(Generic[T]): - nodes: List[T] + class Edge(typing.Generic[T]): + nodes: typing.List[T] @strawberry.type class Query: @strawberry.field - def user(self) -> Union[User, Edge[User]]: - return Edge(nodes=[User(name="P")]) + def user(self) -> typing.Union[User, Edge[User]]: + return Edge(nodes=[User("P")]) schema = strawberry.Schema(query=Query) @@ -676,20 +632,20 @@ def user(self) -> Union[User, Edge[User]]: def test_supports_lists_within_unions_empty_list(): - T = TypeVar("T") + T = typing.TypeVar("T") @strawberry.type class User: name: str @strawberry.type - class Edge(Generic[T]): - nodes: List[T] + class Edge(typing.Generic[T]): + nodes: typing.List[T] @strawberry.type class Query: @strawberry.field - def user(self) -> Union[User, Edge[User]]: + def user(self) -> typing.Union[User, Edge[User]]: return Edge(nodes=[]) schema = strawberry.Schema(query=Query) @@ -714,20 +670,20 @@ def user(self) -> Union[User, Edge[User]]: @pytest.mark.xfail() def test_raises_error_when_unable_to_find_type(): - T = TypeVar("T") + T = typing.TypeVar("T") @strawberry.type class User: name: str @strawberry.type - class Edge(Generic[T]): - nodes: List[T] + class Edge(typing.Generic[T]): + nodes: typing.List[T] @strawberry.type class Query: @strawberry.field - def user(self) -> Union[User, Edge[User]]: + def user(self) -> typing.Union[User, Edge[User]]: return Edge(nodes=["bad example"]) # type: ignore schema = strawberry.Schema(query=Query) @@ -754,12 +710,12 @@ def user(self) -> Union[User, Edge[User]]: def test_generic_with_arguments(): - T = TypeVar("T") + T = typing.TypeVar("T") @strawberry.type - class Collection(Generic[T]): + class Collection(typing.Generic[T]): @strawberry.field - def by_id(self, ids: List[int]) -> List[T]: + def by_id(self, ids: typing.List[int]) -> typing.List[T]: return [] @strawberry.type @@ -789,80 +745,14 @@ class Query: assert str(schema) == textwrap.dedent(expected_schema).strip() -def test_generic_argument(): - T = TypeVar("T") - - @strawberry.type - class Node(Generic[T]): - @strawberry.field - def edge(self, arg: T) -> bool: - return bool(arg) - - @strawberry.field - def edges(self, args: List[T]) -> int: - return len(args) - - @strawberry.type - class Query: - i_node: Node[int] - b_node: Node[bool] - - schema = strawberry.Schema(Query) - - expected_schema = """ - type BoolNode { - edge(arg: Boolean!): Boolean! - edges(args: [Boolean!]!): Int! - } - - type IntNode { - edge(arg: Int!): Boolean! - edges(args: [Int!]!): Int! - } - - type Query { - iNode: IntNode! - bNode: BoolNode! - } - """ - - assert str(schema) == textwrap.dedent(expected_schema).strip() - - -def test_generic_extra_type(): - T = TypeVar("T") - - @strawberry.type - class Node(Generic[T]): - field: T - - @strawberry.type - class Query: - name: str - - schema = strawberry.Schema(Query, types=[Node[int]]) - - expected_schema = """ - type IntNode { - field: Int! - } - - type Query { - name: String! - } - """ - - assert str(schema) == textwrap.dedent(expected_schema).strip() - - def test_generic_extending_with_type_var(): - T = TypeVar("T") + T = typing.TypeVar("T") @strawberry.interface - class Node(Generic[T]): + class Node(typing.Generic[T]): id: strawberry.ID - def _resolve(self) -> Optional[T]: + def _resolve(self) -> typing.Optional[T]: return None @strawberry.type @@ -872,7 +762,7 @@ class Book(Node[str]): @strawberry.type class Query: @strawberry.field - def books(self) -> List[Book]: + def books(self) -> typing.List[Book]: return list() schema = strawberry.Schema(query=Query) @@ -895,53 +785,11 @@ def books(self) -> List[Book]: assert str(schema) == textwrap.dedent(expected_schema).strip() -def test_self(): - @strawberry.interface - class INode: - field: Optional[Self] - fields: List[Self] - - @strawberry.type - class Node(INode): - ... - - schema = strawberry.Schema(query=Node) - - expected_schema = """ - schema { - query: Node - } - - interface INode { - field: INode - fields: [INode!]! - } - - type Node implements INode { - field: Node - fields: [Node!]! - } - """ - - assert str(schema) == textwrap.dedent(expected_schema).strip() - - query = """{ - field { - __typename - } - fields { - __typename - } - }""" - result = schema.execute_sync(query, root_value=Node(field=None, fields=[])) - assert result.data == {"field": None, "fields": []} - - def test_supports_generic_input_type(): - T = TypeVar("T") + T = typing.TypeVar("T") @strawberry.input - class Input(Generic[T]): + class Input(typing.Generic[T]): field: T @strawberry.type @@ -960,50 +808,3 @@ def field(self, input: Input[str]) -> str: assert not result.errors assert result.data == {"field": "data"} - - -def test_generic_interface(): - @strawberry.interface - class ObjectType: - - obj: strawberry.Private[Any] - - @strawberry.field - def repr(self) -> str: - return str(self.obj) - - T = TypeVar("T") - - @strawberry.type - class GenericObject(ObjectType, Generic[T]): - @strawberry.field - def value(self) -> T: - return self.obj - - @strawberry.type - class Query: - @strawberry.field - def foo(self) -> GenericObject[str]: - return GenericObject(obj="foo") - - schema = strawberry.Schema(query=Query) - query_result = schema.execute_sync( - """ - query { - foo { - __typename - value - repr - } - } - """ - ) - - assert not query_result.errors - assert query_result.data == { - "foo": { - "__typename": "StrGenericObject", - "value": "foo", - "repr": "foo", - } - } diff --git a/tests/schema/test_get_extensions.py b/tests/schema/test_get_extensions.py deleted file mode 100644 index 5177d53871..0000000000 --- a/tests/schema/test_get_extensions.py +++ /dev/null @@ -1,61 +0,0 @@ -import strawberry -from strawberry.directive import DirectiveLocation -from strawberry.extensions import Extension -from strawberry.extensions.directives import ( - DirectivesExtension, - DirectivesExtensionSync, -) - - -@strawberry.type -class Query: - example: str - - -@strawberry.directive(locations=[DirectiveLocation.FIELD]) -def uppercase(value: str) -> str: - return value.upper() - - -class MyExtension(Extension): - ... - - -def test_returns_empty_list_when_no_custom_directives(): - schema = strawberry.Schema(query=Query) - - assert schema.get_extensions() == [] - - -def test_returns_extension_passed_by_user(): - schema = strawberry.Schema(query=Query, extensions=[MyExtension]) - - assert schema.get_extensions() == [MyExtension] - - -def test_returns_directives_extension_when_passing_directives(): - schema = strawberry.Schema(query=Query, directives=[uppercase]) - - assert schema.get_extensions() == [DirectivesExtension] - - -def test_returns_extension_passed_by_user_and_directives_extension(): - schema = strawberry.Schema( - query=Query, extensions=[MyExtension], directives=[uppercase] - ) - - assert schema.get_extensions() == [MyExtension, DirectivesExtension] - - -def test_returns_directives_extension_when_passing_directives_sync(): - schema = strawberry.Schema(query=Query, directives=[uppercase]) - - assert schema.get_extensions(sync=True) == [DirectivesExtensionSync] - - -def test_returns_extension_passed_by_user_and_directives_extension_sync(): - schema = strawberry.Schema( - query=Query, extensions=[MyExtension], directives=[uppercase] - ) - - assert schema.get_extensions(sync=True) == [MyExtension, DirectivesExtensionSync] diff --git a/tests/schema/test_info.py b/tests/schema/test_info.py index dfd1d6e6eb..50343d05ba 100644 --- a/tests/schema/test_info.py +++ b/tests/schema/test_info.py @@ -5,9 +5,9 @@ import pytest import strawberry +from strawberry.arguments import UNSET from strawberry.types import Info from strawberry.types.nodes import FragmentSpread, InlineFragment, SelectedField -from strawberry.unset import UNSET def test_info_has_the_correct_shape(): @@ -291,7 +291,7 @@ def hello( @pytest.mark.parametrize( - ("return_type", "return_value"), + "return_type,return_value", [ (str, "text"), (List[str], ["text"]), diff --git a/tests/schema/test_input.py b/tests/schema/test_input.py deleted file mode 100644 index 4135b35913..0000000000 --- a/tests/schema/test_input.py +++ /dev/null @@ -1,29 +0,0 @@ -from typing import Optional - -import strawberry - - -def test_renaming_input_fields(): - @strawberry.input - class FilterInput: - in_: Optional[str] = strawberry.field(name="in", default=strawberry.UNSET) - - @strawberry.type - class Query: - hello: str = "Hello" - - @strawberry.type - class Mutation: - @strawberry.mutation - def filter(self, input: FilterInput) -> str: - return f"Hello {input.in_ or 'nope'}" - - schema = strawberry.Schema(query=Query, mutation=Mutation) - - query = "mutation { filter(input: {}) }" - - result = schema.execute_sync(query) - - assert not result.errors - assert result.data - assert result.data["filter"] == "Hello nope" diff --git a/tests/schema/test_lazy/__init__.py b/tests/schema/test_lazy/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/schema/test_lazy/test_lazy.py b/tests/schema/test_lazy/test_lazy.py deleted file mode 100644 index ce1c31f831..0000000000 --- a/tests/schema/test_lazy/test_lazy.py +++ /dev/null @@ -1,34 +0,0 @@ -import textwrap - -import strawberry -from strawberry.printer import print_schema - - -def test_cyclic_import(): - from .type_a import TypeA - from .type_b import TypeB - - @strawberry.type - class Query: - a: TypeA - b: TypeB - - expected = """ - type Query { - a: TypeA! - b: TypeB! - } - - type TypeA { - listOfB: [TypeB!] - typeB: TypeB! - } - - type TypeB { - typeA: TypeA! - } - """ - - schema = strawberry.Schema(Query) - - assert print_schema(schema) == textwrap.dedent(expected).strip() diff --git a/tests/schema/test_lazy/test_lazy_generic.py b/tests/schema/test_lazy/test_lazy_generic.py deleted file mode 100644 index 7486b3d261..0000000000 --- a/tests/schema/test_lazy/test_lazy_generic.py +++ /dev/null @@ -1,129 +0,0 @@ -import os -import subprocess -import sys -import textwrap -from typing import TYPE_CHECKING, Generic, List, Optional, Sequence, TypeVar -from typing_extensions import Annotated - -import pytest - -import strawberry - -if TYPE_CHECKING: - from tests.schema.test_lazy.type_a import TypeA # noqa - - -T = TypeVar("T") - -TypeAType = Annotated["TypeA", strawberry.lazy("tests.schema.test_lazy.type_a")] - - -def test_lazy_types_with_generic(): - @strawberry.type - class Edge(Generic[T]): - node: T - - @strawberry.type - class Query: - users: Edge[TypeAType] - - strawberry.Schema(query=Query) - - -def test_no_generic_type_duplication_with_lazy(): - from tests.schema.test_lazy.type_a import TypeB_abs, TypeB_rel - from tests.schema.test_lazy.type_b import TypeB - - @strawberry.type - class Edge(Generic[T]): - node: T - - @strawberry.type - class Query: - users: Edge[TypeB] - relatively_lazy_users: Edge[TypeB_rel] - absolutely_lazy_users: Edge[TypeB_abs] - - schema = strawberry.Schema(query=Query) - - expected_schema = textwrap.dedent( - """ - type Query { - users: TypeBEdge! - relativelyLazyUsers: TypeBEdge! - absolutelyLazyUsers: TypeBEdge! - } - - type TypeA { - listOfB: [TypeB!] - typeB: TypeB! - } - - type TypeB { - typeA: TypeA! - } - - type TypeBEdge { - node: TypeB! - } - """ - ).strip() - - assert str(schema) == expected_schema - - -@pytest.mark.parametrize( - "commands", - [ - pytest.param(["tests/schema/test_lazy/type_c.py"], id="script"), - pytest.param(["-m", "tests.schema.test_lazy.type_c"], id="module"), - ], -) -def test_lazy_types_loaded_from_same_module(commands: Sequence[str]): - """Test if lazy types resolved from the same module produce duplication error. - - Note: - `subprocess` is used since the test must be run as the main module / script. - """ - result = subprocess.run( - args=[sys.executable, *commands], - env=os.environ, - capture_output=True, - ) - result.check_returncode() - - -def test_lazy_types_declared_within_optional(): - from tests.schema.test_lazy.type_c import Edge, TypeC - - @strawberry.type - class Query: - - normal_edges: List[Edge[Optional[TypeC]]] - lazy_edges: List[ - Edge[ - Optional[ - Annotated["TypeC", strawberry.lazy("tests.schema.test_lazy.type_c")] - ] - ] - ] - - schema = strawberry.Schema(query=Query) - expected_schema = textwrap.dedent( - """ - type Query { - normalEdges: [TypeCOptionalEdge!]! - lazyEdges: [TypeCOptionalEdge!]! - } - - type TypeC { - name: String! - } - - type TypeCOptionalEdge { - node: TypeC - } - """ - ).strip() - - assert str(schema) == expected_schema diff --git a/tests/schema/test_lazy/type_a.py b/tests/schema/test_lazy/type_a.py deleted file mode 100644 index 65617b5f3b..0000000000 --- a/tests/schema/test_lazy/type_a.py +++ /dev/null @@ -1,24 +0,0 @@ -from typing import TYPE_CHECKING, List, Optional -from typing_extensions import Annotated - -import strawberry - -if TYPE_CHECKING: - from .type_b import TypeB - - TypeB_rel = TypeB - TypeB_abs = TypeB -else: - TypeB_rel = Annotated["TypeB", strawberry.lazy(".type_b")] - TypeB_abs = Annotated["TypeB", strawberry.lazy("tests.schema.test_lazy.type_b")] - - -@strawberry.type -class TypeA: - list_of_b: Optional[List[TypeB_abs]] = None - - @strawberry.field - def type_b(self) -> TypeB_rel: - from .type_b import TypeB - - return TypeB() diff --git a/tests/schema/test_lazy/type_b.py b/tests/schema/test_lazy/type_b.py deleted file mode 100644 index 7b43591634..0000000000 --- a/tests/schema/test_lazy/type_b.py +++ /dev/null @@ -1,20 +0,0 @@ -from typing import TYPE_CHECKING -from typing_extensions import Annotated - -import strawberry - -if TYPE_CHECKING: - from .type_a import TypeA -else: - TypeA = Annotated["TypeA", strawberry.lazy("tests.schema.test_lazy.type_a")] - - -@strawberry.type -class TypeB: - @strawberry.field() - def type_a( - self, - ) -> TypeA: - from .type_a import TypeA - - return TypeA() diff --git a/tests/schema/test_lazy/type_c.py b/tests/schema/test_lazy/type_c.py deleted file mode 100644 index 494b89239c..0000000000 --- a/tests/schema/test_lazy/type_c.py +++ /dev/null @@ -1,29 +0,0 @@ -from typing import Generic, TypeVar -from typing_extensions import Annotated - -import strawberry - -T = TypeVar("T") - - -@strawberry.type -class TypeC: - name: str - - -@strawberry.type -class Edge(Generic[T]): - @strawberry.field - def node(self) -> T: # type: ignore - ... - - -@strawberry.type -class Query: - - type_a: Edge[TypeC] - type_b: Edge[Annotated["TypeC", strawberry.lazy("tests.schema.test_lazy.type_c")]] - - -if __name__ == "__main__": - schema = strawberry.Schema(query=Query) diff --git a/tests/schema/test_lazy_types/test_lazy_enums.py b/tests/schema/test_lazy_types/test_lazy_enums.py index bab11130e8..6bd6a81391 100644 --- a/tests/schema/test_lazy_types/test_lazy_enums.py +++ b/tests/schema/test_lazy_types/test_lazy_enums.py @@ -2,11 +2,10 @@ import textwrap from typing import TYPE_CHECKING -import pytest - import strawberry from strawberry.printer import print_schema + if TYPE_CHECKING: import tests @@ -17,13 +16,11 @@ class LazyEnum(enum.Enum): def test_lazy_enum(): - with pytest.deprecated_call(): - - @strawberry.type - class Query: - a: strawberry.LazyType[ - "LazyEnum", "tests.schema.test_lazy_types.test_lazy_enums" - ] + @strawberry.type + class Query: + a: strawberry.LazyType[ + "LazyEnum", "tests.schema.test_lazy_types.test_lazy_enums" + ] expected = """ enum LazyEnum { diff --git a/tests/schema/test_lazy_types/type_a.py b/tests/schema/test_lazy_types/type_a.py index 45dbf1a9e2..f1586ea9ff 100644 --- a/tests/schema/test_lazy_types/type_a.py +++ b/tests/schema/test_lazy_types/type_a.py @@ -1,8 +1,9 @@ -from typing import TYPE_CHECKING, List, Optional +import typing import strawberry -if TYPE_CHECKING: + +if typing.TYPE_CHECKING: import tests.schema.test_lazy_types from .type_b import TypeB @@ -10,11 +11,11 @@ @strawberry.type class TypeA: - list_of_b: Optional[ - List[strawberry.LazyType["TypeB", "tests.schema.test_lazy_types.type_b"]] + list_of_b: typing.Optional[ + typing.List[strawberry.LazyType["TypeB", "tests.schema.test_lazy_types.type_b"]] ] = None - @strawberry.field + @strawberry.field() def type_b(self) -> strawberry.LazyType["TypeB", ".type_b"]: # noqa from .type_b import TypeB diff --git a/tests/schema/test_lazy_types/type_b.py b/tests/schema/test_lazy_types/type_b.py index 9335be4b87..6c57e8a9e8 100644 --- a/tests/schema/test_lazy_types/type_b.py +++ b/tests/schema/test_lazy_types/type_b.py @@ -1,8 +1,9 @@ -from typing import TYPE_CHECKING +import typing import strawberry -if TYPE_CHECKING: + +if typing.TYPE_CHECKING: import tests from .type_a import TypeA diff --git a/tests/schema/test_mutation.py b/tests/schema/test_mutation.py index 2671cb3fd4..e01d7b1816 100644 --- a/tests/schema/test_mutation.py +++ b/tests/schema/test_mutation.py @@ -3,7 +3,7 @@ from textwrap import dedent import strawberry -from strawberry.unset import UNSET +from strawberry.arguments import UNSET, is_unset def test_mutation(): @@ -97,14 +97,14 @@ class InputExample: class Mutation: @strawberry.mutation def say(self, name: typing.Optional[str] = UNSET) -> str: # type: ignore - if name is UNSET: + if is_unset(name): return "Name is unset" return f"Hello {name}!" @strawberry.mutation def say_age(self, input: InputExample) -> str: - age = "unset" if input.age is UNSET else input.age + age = "unset" if is_unset(input.age) else input.age return f"Hello {input.name} of age {age}!" @@ -133,7 +133,7 @@ class InputExample: class Mutation: @strawberry.mutation def say(self, first_name: typing.Optional[str] = UNSET) -> str: # type: ignore - if first_name is UNSET: + if is_unset(first_name): return "Name is unset" if first_name == "": @@ -143,7 +143,7 @@ def say(self, first_name: typing.Optional[str] = UNSET) -> str: # type: ignore @strawberry.mutation def say_age(self, input: InputExample) -> str: - age = "unset" if input.age is UNSET else input.age + age = "unset" if is_unset(input.age) else input.age age = "empty" if age == "" else age return f"Hello {input.first_name} of age {age}!" @@ -217,7 +217,7 @@ class Mutation: def say(self, input: Input) -> str: data = dataclasses.asdict(input) - if data["name"] is UNSET: + if is_unset(data["name"]): return "Hello ๐Ÿคจ" return f"Hello {data['name']}!" diff --git a/tests/schema/test_private_field.py b/tests/schema/test_private_field.py index 976db42263..76a4396f3c 100644 --- a/tests/schema/test_private_field.py +++ b/tests/schema/test_private_field.py @@ -1,12 +1,7 @@ -from dataclasses import dataclass -from typing import Generic, TypeVar - import pytest import strawberry -from strawberry.annotation import StrawberryAnnotation from strawberry.exceptions import PrivateStrawberryFieldError -from strawberry.field import StrawberryField def test_private_field(): @@ -29,15 +24,15 @@ class Query: assert instance.age == 22 -@pytest.mark.raises_strawberry_exception( - PrivateStrawberryFieldError, - match=("Field age on type Query cannot be both private and a strawberry.field"), -) def test_private_field_with_strawberry_field_error(): - @strawberry.type - class Query: - name: str - age: strawberry.Private[int] = strawberry.field(description="๐Ÿคซ") + with pytest.raises(PrivateStrawberryFieldError) as error: + + @strawberry.type + class Query: + name: str + age: strawberry.Private[int] = strawberry.field(description="๐Ÿคซ") + + assert "Field age on type Query" in str(error) def test_private_field_access_in_resolver(): @@ -60,82 +55,3 @@ def age_in_months(self) -> int: assert result.data == { "ageInMonths": 84, } - - -@strawberry.type -class Query: - not_seen: "strawberry.Private[SensitiveData]" - - @strawberry.field - def accessible_info(self) -> str: - return self.not_seen.info - - -@dataclass -class SensitiveData: - value: int - info: str - - -def test_private_field_with_str_annotations(): - """Check compatibility of strawberry.Private with annotations as string.""" - - schema = strawberry.Schema(query=Query) - - result = schema.execute_sync( - "query { accessibleInfo }", - root_value=Query(not_seen=SensitiveData(1, "foo")), - ) - assert result.data == {"accessibleInfo": "foo"} - - # Check if querying `notSeen` raises error and no data is returned - assert "notSeen" not in str(schema) - failed_result = schema.execute_sync( - "query { notSeen }", root_value=Query(not_seen=SensitiveData(1, "foo")) - ) - assert failed_result.data is None - - -def test_private_field_defined_outside_module_scope(): - """Check compatibility of strawberry.Private when defined outside module scope.""" - - global LocallyScopedSensitiveData - - @strawberry.type - class LocallyScopedQuery: - not_seen: "strawberry.Private[LocallyScopedSensitiveData]" - - @strawberry.field - def accessible_info(self) -> str: - return self.not_seen.info - - @dataclass - class LocallyScopedSensitiveData: - value: int - info: str - - schema = strawberry.Schema(query=LocallyScopedQuery) - - assert "notSeen" not in str(schema) - - del LocallyScopedSensitiveData - - -def test_private_field_type_resolution_with_generic_type(): - """Check strawberry.Private when its argument is a implicit `Any` generic type. - - Refer to: https://github.com/strawberry-graphql/strawberry/issues/1938 - """ - - T = TypeVar("T") - - class GenericPrivateType(Generic[T]): - pass - - private_field = StrawberryField( - type_annotation=StrawberryAnnotation( - annotation="strawberry.Private[GenericPrivateType]", - namespace={**globals(), **locals()}, - ), - ) - assert private_field.type == strawberry.Private[GenericPrivateType] diff --git a/tests/schema/test_pydantic.py b/tests/schema/test_pydantic.py index f4dc07551a..8859909e4a 100644 --- a/tests/schema/test_pydantic.py +++ b/tests/schema/test_pydantic.py @@ -15,7 +15,7 @@ class User: @strawberry.type class Query: - user: User = strawberry.field(default_factory=lambda: User(age_=5)) + user: User = User(age_=5) schema = strawberry.Schema(query=Query) query = """{ @@ -46,7 +46,7 @@ class User: @strawberry.type class Query: - user: User = strawberry.field(default_factory=lambda: User(age_=5)) + user: User = User(age_=5) schema = strawberry.Schema(query=Query) query = """{ diff --git a/tests/schema/test_resolvers.py b/tests/schema/test_resolvers.py index f0c162cf67..a8c62e8b29 100644 --- a/tests/schema/test_resolvers.py +++ b/tests/schema/test_resolvers.py @@ -1,11 +1,10 @@ # type: ignore import typing -from typing import Any, Generic, List, Optional, Type, TypeVar, Union +from typing import List import pytest import strawberry -from strawberry.types.info import Info def test_resolver(): @@ -174,10 +173,10 @@ def __post_init__(self): def test_only_info_function_resolvers(): - def function_resolver(info: Info) -> str: + def function_resolver(info) -> str: return f"I'm a function resolver for {info.field_name}" - def function_resolver_with_params(info: Info, x: str) -> str: + def function_resolver_with_params(info, x: str) -> str: return f"I'm {x} for {info.field_name}" @strawberry.type @@ -350,194 +349,3 @@ def hello(self, source: str) -> str: assert not result.errors assert result.data["hello"] == "I'm a resolver for ๐Ÿ“" - - -def test_generic_resolver_factory(): - @strawberry.type - class AType: - some: int - - T = TypeVar("T") - - def resolver_factory(strawberry_type: Type[T]): - def resolver() -> T: - return strawberry_type(some=1) - - return resolver - - @strawberry.type - class Query: - a_type: AType = strawberry.field(resolver_factory(AType)) - - strawberry.Schema(query=Query) - - schema = strawberry.Schema(query=Query) - - query = "{ aType { some } }" - - result = schema.execute_sync(query) - - assert not result.errors - assert result.data == {"aType": {"some": 1}} - - -def test_generic_resolver_optional(): - @strawberry.type - class AType: - some: int - - T = TypeVar("T") - - def resolver() -> Optional[T]: - return AType(some=1) - - @strawberry.type - class Query: - a_type: Optional[AType] = strawberry.field(resolver) - - strawberry.Schema(query=Query) - - schema = strawberry.Schema(query=Query) - - query = "{ aType { some } }" - - result = schema.execute_sync(query) - - assert not result.errors - assert result.data == {"aType": {"some": 1}} - - -def test_generic_resolver_container(): - T = TypeVar("T") - - @strawberry.type - class Container(Generic[T]): - item: T - - @strawberry.type - class AType: - some: int - - def resolver() -> Container[T]: - return Container(item=AType(some=1)) - - @strawberry.type - class Query: - a_type_in_container: Container[AType] = strawberry.field(resolver) - - strawberry.Schema(query=Query) - - schema = strawberry.Schema(query=Query) - - query = "{ aTypeInContainer { item { some } } }" - - result = schema.execute_sync(query) - - assert not result.errors - assert result.data == {"aTypeInContainer": {"item": {"some": 1}}} - - -def test_generic_resolver_union(): - T = TypeVar("T") - - @strawberry.type - class AType: - some: int - - @strawberry.type - class OtherType: - other: int - - def resolver() -> Union[T, OtherType]: - return AType(some=1) - - @strawberry.type - class Query: - union_type: Union[AType, OtherType] = strawberry.field(resolver) - - strawberry.Schema(query=Query) - - schema = strawberry.Schema(query=Query) - - query = "{ unionType { ... on AType { some } } }" - - result = schema.execute_sync(query) - - assert not result.errors - assert result.data == {"unionType": {"some": 1}} - - -def test_generic_resolver_list(): - T = TypeVar("T") - - @strawberry.type - class AType: - some: int - - def resolver() -> List[T]: - return [AType(some=1)] - - @strawberry.type - class Query: - list_type: List[AType] = strawberry.field(resolver) - - strawberry.Schema(query=Query) - - schema = strawberry.Schema(query=Query) - - query = "{ listType { some } }" - - result = schema.execute_sync(query) - - assert not result.errors - assert result.data == {"listType": [{"some": 1}]} - - -def name_based_info(info, icon: str) -> str: - return f"I'm a resolver for {icon} {info.field_name}" - - -def type_based_info(info: Info, icon: str) -> str: - return f"I'm a resolver for {icon} {info.field_name}" - - -def generic_type_based_info(icon: str, info: Info[Any, Any]) -> str: - return f"I'm a resolver for {icon} {info.field_name}" - - -def arbitrarily_named_info(icon: str, info_argument: Info) -> str: - return f"I'm a resolver for {icon} {info_argument.field_name}" - - -@pytest.mark.parametrize( - "resolver", - ( - pytest.param(name_based_info), - pytest.param(type_based_info), - pytest.param(generic_type_based_info), - pytest.param(arbitrarily_named_info), - ), -) -def test_info_argument(resolver): - @strawberry.type - class ResolverGreeting: - hello: str = strawberry.field(resolver=resolver) - - schema = strawberry.Schema(query=ResolverGreeting) - result = schema.execute_sync('{ hello(icon: "๐Ÿ“") }') - - assert not result.errors - assert result.data["hello"] == "I'm a resolver for ๐Ÿ“ hello" - - -def test_name_based_info_is_deprecated(): - - with pytest.deprecated_call(match=r"Argument name-based matching of 'info'"): - - @strawberry.type - class Query: - @strawberry.field - def foo(info: Any) -> str: - ... - - strawberry.Schema(query=Query) diff --git a/tests/schema/test_scalars.py b/tests/schema/test_scalars.py index 315a351235..4919177832 100644 --- a/tests/schema/test_scalars.py +++ b/tests/schema/test_scalars.py @@ -1,17 +1,10 @@ -import sys -from datetime import date, datetime, timedelta, timezone -from decimal import Decimal +from datetime import datetime, timedelta, timezone from textwrap import dedent -from typing import Optional from uuid import UUID import pytest import strawberry -from strawberry import scalar -from strawberry.exceptions import ScalarAlreadyRegisteredError -from strawberry.scalars import JSON, Base16, Base32, Base64 -from strawberry.schema.types.base_scalars import Date def test_void_function(): @@ -160,154 +153,6 @@ def uuid_input(self, input_id: UUID) -> str: } -def test_json(): - @strawberry.type - class Query: - @strawberry.field - def echo_json(data: JSON) -> JSON: - return data - - @strawberry.field - def echo_json_nullable(data: Optional[JSON]) -> Optional[JSON]: - return data - - schema = strawberry.Schema(query=Query) - - expected_schema = dedent( - ''' - """ - The `JSON` scalar type represents JSON values as specified by [ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf). - """ - scalar JSON @specifiedBy(url: "http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf") - - type Query { - echoJson(data: JSON!): JSON! - echoJsonNullable(data: JSON): JSON - } - ''' # noqa: E501 - ).strip() - - assert str(schema) == expected_schema - - result = schema.execute_sync( - """ - query { - echoJson(data: {hello: {a: 1}, someNumbers: [1, 2, 3], null: null}) - echoJsonNullable(data: {hello: {a: 1}, someNumbers: [1, 2, 3], null: null}) - } - """ - ) - - assert not result.errors - assert result.data == { - "echoJson": {"hello": {"a": 1}, "someNumbers": [1, 2, 3], "null": None}, - "echoJsonNullable": {"hello": {"a": 1}, "someNumbers": [1, 2, 3], "null": None}, - } - - result = schema.execute_sync( - """ - query { - echoJson(data: null) - } - """ - ) - assert result.errors # echoJson is not-null null - - result = schema.execute_sync( - """ - query { - echoJsonNullable(data: null) - } - """ - ) - assert not result.errors - assert result.data == { - "echoJsonNullable": None, - } - - -def test_base16(): - @strawberry.type - class Query: - @strawberry.field - def base16_encode(data: str) -> Base16: - return bytes(data, "utf-8") - - @strawberry.field - def base16_decode(data: Base16) -> str: - return data.decode("utf-8") - - @strawberry.field - def base32_encode(data: str) -> Base32: - return bytes(data, "utf-8") - - @strawberry.field - def base32_decode(data: Base32) -> str: - return data.decode("utf-8") - - @strawberry.field - def base64_encode(data: str) -> Base64: - return bytes(data, "utf-8") - - @strawberry.field - def base64_decode(data: Base64) -> str: - return data.decode("utf-8") - - schema = strawberry.Schema(query=Query) - - assert ( - str(schema) - == dedent( - ''' - """Represents binary data as Base16-encoded (hexadecimal) strings.""" - scalar Base16 @specifiedBy(url: "https://datatracker.ietf.org/doc/html/rfc4648.html#section-8") - - """ - Represents binary data as Base32-encoded strings, using the standard alphabet. - """ - scalar Base32 @specifiedBy(url: "https://datatracker.ietf.org/doc/html/rfc4648.html#section-6") - - """ - Represents binary data as Base64-encoded strings, using the standard alphabet. - """ - scalar Base64 @specifiedBy(url: "https://datatracker.ietf.org/doc/html/rfc4648.html#section-4") - - type Query { - base16Encode(data: String!): Base16! - base16Decode(data: Base16!): String! - base32Encode(data: String!): Base32! - base32Decode(data: Base32!): String! - base64Encode(data: String!): Base64! - base64Decode(data: Base64!): String! - } - ''' # noqa: E501 - ).strip() - ) - - result = schema.execute_sync( - """ - query { - base16Encode(data: "Hello") - base16Decode(data: "48656c6C6f") # < Mix lowercase and uppercase - base32Encode(data: "Hello") - base32Decode(data: "JBSWY3dp") # < Mix lowercase and uppercase - base64Encode(data: "Hello") - base64Decode(data: "SGVsbG8=") - } - """ - ) - - assert not result.errors - assert result.data == { - "base16Encode": "48656C6C6F", - "base16Decode": "Hello", - "base32Encode": "JBSWY3DP", - "base32Decode": "Hello", - "base64Encode": "SGVsbG8=", - "base64Decode": "Hello", - } - - def test_override_built_in_scalars(): EpochDateTime = strawberry.scalar( datetime, @@ -346,64 +191,7 @@ def isoformat(self, input_datetime: datetime) -> str: assert result.data["isoformat"] == "2021-08-11T12:00:00+00:00" -def test_override_unknown_scalars(): - Duration = strawberry.scalar( - timedelta, - name="Duration", - serialize=timedelta.total_seconds, - parse_value=lambda s: timedelta(seconds=s), - ) - - @strawberry.type - class Query: - @strawberry.field - def duration(self, value: timedelta) -> timedelta: - return value - - schema = strawberry.Schema(Query, scalar_overrides={timedelta: Duration}) - - result = schema.execute_sync("{ duration(value: 10) }") - - assert not result.errors - assert result.data == {"duration": 10} - - -def test_decimal(): - @strawberry.type - class Query: - @strawberry.field - def decimal(value: Decimal) -> Decimal: - return value - - schema = strawberry.Schema(query=Query) - - result = schema.execute_sync( - """ - query { - floatDecimal: decimal(value: 3.14) - floatDecimal2: decimal(value: 3.14509999) - floatDecimal3: decimal(value: 0.000001) - stringDecimal: decimal(value: "3.14") - stringDecimal2: decimal(value: "3.1499999991") - } - """ - ) - - assert not result.errors - assert result.data == { - "floatDecimal": "3.14", - "floatDecimal2": "3.14509999", - "floatDecimal3": "0.000001", - "stringDecimal": "3.14", - "stringDecimal2": "3.1499999991", - } - - -@pytest.mark.raises_strawberry_exception( - ScalarAlreadyRegisteredError, - match="Scalar `MyCustomScalar` has already been registered", -) -def test_duplicate_scalars_raises_exception(): +def test_duplicate_scalars(): MyCustomScalar = strawberry.scalar( str, name="MyCustomScalar", @@ -419,51 +207,29 @@ class Query: scalar_1: MyCustomScalar scalar_2: MyCustomScalar2 - strawberry.Schema(Query) + with pytest.raises( + TypeError, match="Scalar `MyCustomScalar` has already been registered" + ): + strawberry.Schema(Query) -@pytest.mark.raises_strawberry_exception( - ScalarAlreadyRegisteredError, - match="Scalar `MyCustomScalar` has already been registered", -) -def test_duplicate_scalars_raises_exception_using_alias(): - MyCustomScalar = scalar( - str, - name="MyCustomScalar", - ) - - MyCustomScalar2 = scalar( - int, - name="MyCustomScalar", +def test_override_unknown_scalars(): + Duration = strawberry.scalar( + timedelta, + name="Duration", + serialize=timedelta.total_seconds, + parse_value=lambda s: timedelta(seconds=s), ) @strawberry.type class Query: - scalar_1: MyCustomScalar - scalar_2: MyCustomScalar2 - - strawberry.Schema(Query) - - -@pytest.mark.skipif( - sys.version_info < (3, 10), - reason="pipe syntax for union is only available on python 3.10+", -) -def test_optional_scalar_with_or_operator(): - """Check `|` operator support with an optional scalar.""" - - @strawberry.type - class Query: - date: Date | None - - schema = strawberry.Schema(query=Query) + @strawberry.field + def duration(self, value: timedelta) -> timedelta: + return value - query = "{ date }" + schema = strawberry.Schema(Query, scalar_overrides={timedelta: Duration}) - result = schema.execute_sync(query, root_value=Query(date=None)) - assert not result.errors - assert result.data["date"] is None + result = schema.execute_sync("{ duration(value: 10) }") - result = schema.execute_sync(query, root_value=Query(date=date(2020, 1, 1))) assert not result.errors - assert result.data["date"] == "2020-01-01" + assert result.data == {"duration": 10} diff --git a/tests/schema/test_schema_generation.py b/tests/schema/test_schema_generation.py index 5770eb7349..40ff63e790 100644 --- a/tests/schema/test_schema_generation.py +++ b/tests/schema/test_schema_generation.py @@ -1,17 +1,18 @@ -from typing import Any, Dict, List, Optional +from typing import Any, Dict, Optional, cast import pytest -from graphql import ExecutionContext as GraphQLExecutionContext + from graphql import ( + ExecutionContext as GraphQLExecutionContext, ExecutionResult, - GraphQLError, GraphQLField, GraphQLNonNull, GraphQLObjectType, GraphQLSchema, GraphQLString, + print_schema as graphql_core_print_schema, ) -from graphql import print_schema as graphql_core_print_schema +from graphql.pyutils import AwaitableOrValue import strawberry @@ -65,14 +66,10 @@ class Query: def test_custom_execution_context(): class CustomExecutionContext(GraphQLExecutionContext): - @staticmethod def build_response( - data: Optional[Dict[str, Any]], - errors: List[GraphQLError], - ) -> ExecutionResult: - result = super( - CustomExecutionContext, CustomExecutionContext - ).build_response(data, errors) + self, data: AwaitableOrValue[Optional[Dict[str, Any]]] + ) -> AwaitableOrValue[ExecutionResult]: + result = cast(ExecutionResult, super().build_response(data)) if not result.data: return result diff --git a/tests/schema/test_subscription.py b/tests/schema/test_subscription.py index 017e7fb1af..3e87f1ba22 100644 --- a/tests/schema/test_subscription.py +++ b/tests/schema/test_subscription.py @@ -1,8 +1,4 @@ -from __future__ import annotations - -import sys import typing -from collections import abc # noqa: F401 import pytest @@ -53,47 +49,3 @@ async def example(self, name: str) -> typing.AsyncGenerator[str, None]: assert not result.errors assert result.data["example"] == "Hi Nina" - - -requires_builtin_generics = pytest.mark.skipif( - sys.version_info < (3, 9), - reason="built-in generic annotations were added in python 3.9", -) - - -@pytest.mark.parametrize( - "return_annotation", - ( - "typing.AsyncGenerator[str, None]", - "typing.AsyncIterable[str]", - "typing.AsyncIterator[str]", - pytest.param("abc.AsyncIterator[str]", marks=requires_builtin_generics), - pytest.param("abc.AsyncGenerator[str, None]", marks=requires_builtin_generics), - pytest.param("abc.AsyncIterable[str]", marks=requires_builtin_generics), - ), -) -@pytest.mark.asyncio -async def test_subscription_return_annotations(return_annotation: str): - async def async_resolver(): - yield "Hi" - - async_resolver.__annotations__["return"] = return_annotation - - @strawberry.type - class Query: - x: str = "Hello" - - @strawberry.type - class Subscription: - - example = strawberry.subscription(resolver=async_resolver) - - schema = strawberry.Schema(query=Query, subscription=Subscription) - - query = "subscription { example }" - - sub = await schema.subscribe(query) - result = await sub.__anext__() - - assert not result.errors - assert result.data["example"] == "Hi" diff --git a/tests/schema/test_union.py b/tests/schema/test_union.py index 088a9dd463..cf0039954d 100644 --- a/tests/schema/test_union.py +++ b/tests/schema/test_union.py @@ -1,13 +1,11 @@ import sys from dataclasses import dataclass from textwrap import dedent -from typing import Generic, List, Optional, TypeVar, Union -from typing_extensions import Annotated +from typing import Optional, Union import pytest import strawberry -from strawberry.lazy_type import lazy def test_union_as_field(): @@ -21,7 +19,7 @@ class B: @strawberry.type class Query: - ab: Union[A, B] = strawberry.field(default_factory=lambda: A(a=5)) + ab: Union[A, B] = A(a=5) schema = strawberry.Schema(query=Query) query = """{ @@ -51,7 +49,7 @@ class B: @strawberry.type class Query: - ab: Union[A, B] = strawberry.field(default_factory=lambda: B(b=5)) + ab: Union[A, B] = B(b=5) schema = strawberry.Schema(query=Query) query = """{ @@ -189,42 +187,6 @@ def hello(self) -> Union[A, B]: ) -def test_unknown_types_are_rejected(): - @strawberry.type - class Outside: - c: int - - @strawberry.type - class A: - a: int - - @strawberry.type - class B: - b: int - - @strawberry.type - class Query: - @strawberry.field - def hello(self) -> Union[A, B]: - return Outside(c=5) # type:ignore - - schema = strawberry.Schema(query=Query) - - query = """ - { - hello { - ... on A { - a - } - } - } - """ - - result = schema.execute_sync(query) - - assert "Outside" in result.errors[0].message - - def test_named_union(): @strawberry.type class A: @@ -238,7 +200,7 @@ class B: @strawberry.type class Query: - ab: Result = strawberry.field(default_factory=lambda: A(a=5)) + ab: Result = A(a=5) schema = strawberry.Schema(query=Query) @@ -277,7 +239,7 @@ class B: @strawberry.type class Query: - ab: Result = strawberry.field(default_factory=lambda: A(a=5)) + ab: Result = A(a=5) schema = strawberry.Schema(query=Query) @@ -495,152 +457,3 @@ def animal(self) -> animal_union | None: assert not result.errors assert result.data["animal"] is None - - -def test_union_with_input_types(): - """ - Verify that union of input types raises an error - """ - - @strawberry.type - class User: - name: str - age: int - - @strawberry.input - class A: - a: str - - @strawberry.input - class B: - b: str - - @strawberry.input - class Input: - name: str - something: Union[A, B] - - with pytest.raises( - TypeError, match="Union for A is not supported because it is an Input type" - ): - - @strawberry.type - class Query: - @strawberry.field - def user(self, data: Input) -> User: - return User(name=data.name, age=100) - - strawberry.Schema(query=Query) - - -def test_union_with_similar_nested_generic_types(): - """ - Previously this failed due to an edge case where Strawberry would choose AContainer - as the resolved type for container_b due to the inability to exactly match the - nested generic `Container.items`. - """ - T = TypeVar("T") - - @strawberry.type - class Container(Generic[T]): - items: List[T] - - @strawberry.type - class A: - a: str - - @strawberry.type - class B: - b: int - - @strawberry.type - class Query: - @strawberry.field - def container_a(self) -> Union[Container[A], A]: - return Container(items=[A(a="hello")]) - - @strawberry.field - def container_b(self) -> Union[Container[B], B]: - return Container(items=[B(b=3)]) - - schema = strawberry.Schema(query=Query) - - query = """ - { - containerA { - __typename - ... on AContainer { - items { - a - } - } - ... on A { - a - } - } - } - """ - - result = schema.execute_sync(query) - - assert result.data["containerA"]["items"][0]["a"] == "hello" - - query = """ - { - containerB { - __typename - ... on BContainer { - items { - b - } - } - ... on B { - b - } - } - } - """ - - result = schema.execute_sync(query) - - assert result.data["containerB"]["items"][0]["b"] == 3 - - -def test_lazy_union(): - """ - Previously this failed to evaluate generic parameters on lazy types - """ - TypeA = Annotated["TypeA", lazy("tests.schema.test_lazy_types.type_a")] - TypeB = Annotated["TypeB", lazy("tests.schema.test_lazy_types.type_b")] - - @strawberry.type - class Query: - @strawberry.field - def a(self) -> Union[TypeA, TypeB]: - from tests.schema.test_lazy_types.type_a import TypeA - - return TypeA(list_of_b=[]) - - @strawberry.field - def b(self) -> Union[TypeA, TypeB]: - from tests.schema.test_lazy_types.type_b import TypeB - - return TypeB() - - schema = strawberry.Schema(query=Query) - - query = """ - { - a { - __typename - } - b { - __typename - } - } - """ - - result = schema.execute_sync(query) - - assert result.data["a"]["__typename"] == "TypeA" - assert result.data["b"]["__typename"] == "TypeB" diff --git a/tests/schema/test_unresolved_fields.py b/tests/schema/test_unresolved_fields.py deleted file mode 100644 index 392cdd7b81..0000000000 --- a/tests/schema/test_unresolved_fields.py +++ /dev/null @@ -1,36 +0,0 @@ -import pytest - -import strawberry -from strawberry.exceptions.unresolved_field_type import UnresolvedFieldTypeError - - -@pytest.mark.raises_strawberry_exception( - UnresolvedFieldTypeError, - match=( - "Could not resolve the type of 'user'. Check that " - "the class is accessible from the global module scope." - ), -) -def test_unresolved_field_fails(): - @strawberry.type - class Query: - user: "User" # type: ignore # noqa: F821 - - strawberry.Schema(query=Query) - - -@pytest.mark.raises_strawberry_exception( - UnresolvedFieldTypeError, - match=( - "Could not resolve the type of 'user'. Check that " - "the class is accessible from the global module scope." - ), -) -def test_unresolved_field_with_resolver_fails(): - @strawberry.type - class Query: - @strawberry.field - def user(self) -> "User": # type: ignore # noqa: F821 - ... - - strawberry.Schema(query=Query) diff --git a/tests/schema/types/test_date.py b/tests/schema/types/test_date.py index 6ae3fbd8ed..df98bd3ee1 100644 --- a/tests/schema/types/test_date.py +++ b/tests/schema/types/test_date.py @@ -1,6 +1,7 @@ import datetime import pytest + from graphql import GraphQLError import strawberry @@ -93,8 +94,7 @@ def date_input(self, date_input: datetime.date) -> datetime.date: "2012-12-01T09:00", "2012-13-01", "2012-04-9", - # this might have been fixed in 3.11 - # "20120411", + "20120411", ), ) def test_serialization_of_incorrect_date_string(value): diff --git a/tests/schema/types/test_datetime.py b/tests/schema/types/test_datetime.py index 89bbe348ad..7b08ccad8a 100644 --- a/tests/schema/types/test_datetime.py +++ b/tests/schema/types/test_datetime.py @@ -1,14 +1,16 @@ import datetime -import dateutil.tz import pytest + +import dateutil.tz + from graphql import GraphQLError import strawberry @pytest.mark.parametrize( - ("typing", "instance", "serialized"), + "typing,instance,serialized", [ (datetime.date, datetime.date(2019, 10, 25), "2019-10-25"), ( @@ -35,7 +37,7 @@ def serialize(self) -> typing: @pytest.mark.parametrize( - ("typing", "name", "instance", "serialized"), + "typing,name,instance,serialized", [ (datetime.date, "Date", datetime.date(2019, 10, 25), "2019-10-25"), ( @@ -75,7 +77,7 @@ def deserialize(self, arg: typing) -> bool: @pytest.mark.parametrize( - ("typing", "instance", "serialized"), + "typing,instance,serialized", [ (datetime.date, datetime.date(2019, 10, 25), "2019-10-25"), ( @@ -143,7 +145,7 @@ def datetime_input( "20120411T03:30-25:40", "20120411T03:30+00:60", "20120411T03:30+00:61", - "20120411T033030.123456012:00" "2014-03-12T12:30:14", + "20120411T033030.123456012:00" "2014-03-12ะข12:30:14", "2014-04-21T24:00:01", ), ) diff --git a/tests/schema/types/test_time.py b/tests/schema/types/test_time.py index f8d0f5c090..057ce4b248 100644 --- a/tests/schema/types/test_time.py +++ b/tests/schema/types/test_time.py @@ -1,6 +1,7 @@ import datetime import pytest + from graphql import GraphQLError import strawberry diff --git a/tests/starlite/__init__.py b/tests/starlite/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/starlite/app.py b/tests/starlite/app.py deleted file mode 100644 index 9a10301e38..0000000000 --- a/tests/starlite/app.py +++ /dev/null @@ -1,62 +0,0 @@ -from starlite import Provide, Request, Starlite -from strawberry.starlite import make_graphql_controller -from strawberry.starlite.handlers.graphql_transport_ws_handler import ( - GraphQLTransportWSHandler, -) -from strawberry.starlite.handlers.graphql_ws_handler import GraphQLWSHandler -from tests.starlite.schema import schema - - -class DebuggableGraphQLTransportWSHandler(GraphQLTransportWSHandler): - async def get_context(self) -> object: - context = await super().get_context() - context["ws"] = self._ws - context["tasks"] = self.tasks - context["connectionInitTimeoutTask"] = self.connection_init_timeout_task - return context - - -class DebuggableGraphQLWSHandler(GraphQLWSHandler): - async def get_context(self) -> object: - context = await super().get_context() - context["ws"] = self._ws - context["tasks"] = self.tasks - context["connectionInitTimeoutTask"] = None - return context - - -def custom_context_dependency() -> str: - return "Hi!" - - -async def get_root_value(request: Request = None): - return request - - -async def get_context(app_dependency: str, request: Request = None): - return { - "custom_value": app_dependency, - "request": request, - } - - -def create_app(schema=schema, **kwargs): - - GraphQLController = make_graphql_controller( - schema, - path="/graphql", - context_getter=get_context, - root_value_getter=get_root_value, - **kwargs - ) - - class DebuggableGraphQLController(GraphQLController): - graphql_ws_handler_class = DebuggableGraphQLWSHandler - graphql_transport_ws_handler_class = DebuggableGraphQLTransportWSHandler - - app = Starlite( - route_handlers=[DebuggableGraphQLController], - dependencies={"app_dependency": Provide(custom_context_dependency)}, - ) - - return app diff --git a/tests/starlite/conftest.py b/tests/starlite/conftest.py deleted file mode 100644 index a7bd125a24..0000000000 --- a/tests/starlite/conftest.py +++ /dev/null @@ -1,16 +0,0 @@ -import pytest - -from starlite.testing import TestClient -from tests.starlite.app import create_app - - -@pytest.fixture -def test_client(): - app = create_app() - return TestClient(app) - - -@pytest.fixture -def test_client_keep_alive(): - app = create_app(keep_alive=True, keep_alive_interval=0.1) - return TestClient(app) diff --git a/tests/starlite/schema.py b/tests/starlite/schema.py deleted file mode 100644 index 86ac8bf400..0000000000 --- a/tests/starlite/schema.py +++ /dev/null @@ -1,153 +0,0 @@ -import asyncio -import typing -from enum import Enum -from typing import Optional - -from graphql import GraphQLError - -import strawberry -from strawberry.file_uploads import Upload -from strawberry.permission import BasePermission -from strawberry.subscriptions.protocols.graphql_transport_ws.types import PingMessage -from strawberry.types import Info - - -class AlwaysFailPermission(BasePermission): - message = "You are not authorized" - - def has_permission(self, source: typing.Any, info: Info, **kwargs) -> bool: - return False - - -@strawberry.enum -class Flavor(Enum): - VANILLA = "vanilla" - STRAWBERRY = "strawberry" - CHOCOLATE = "chocolate" - - -@strawberry.input -class FolderInput: - files: typing.List[Upload] - - -@strawberry.type -class DebugInfo: - num_active_result_handlers: int - is_connection_init_timeout_task_done: typing.Optional[bool] - - -@strawberry.type -class Query: - @strawberry.field - def hello(self, name: typing.Optional[str] = None) -> str: - return f"Hello {name or 'world'}" - - @strawberry.field - async def async_hello(self, name: str, delay: float = 0) -> str: - await asyncio.sleep(delay) - return f"Hello {name or 'world'}" - - @strawberry.field(permission_classes=[AlwaysFailPermission]) - def always_fail(self) -> Optional[str]: - return "Hey" - - @strawberry.field - def root_name(root) -> str: - return type(root).__name__ - - @strawberry.field - async def exception(self, message: str) -> str: - raise ValueError(message) - return message - - -@strawberry.type -class Mutation: - @strawberry.mutation - async def hello(self) -> str: - return "strawberry" - - @strawberry.mutation - async def read_text(self, text_file: Upload) -> str: - return (await text_file.read()).decode() - - @strawberry.mutation - async def read_files(self, files: typing.List[Upload]) -> typing.List[str]: - contents = [] - for file in files: - content = (await file.read()).decode() - contents.append(content) - return contents - - @strawberry.mutation - async def read_folder(self, folder: FolderInput) -> typing.List[str]: - contents = [] - for file in folder.files: - content = (await file.read()).decode() - contents.append(content) - return contents - - -@strawberry.type -class Subscription: - @strawberry.subscription - async def echo( - self, message: str, delay: float = 0 - ) -> typing.AsyncGenerator[str, None]: - await asyncio.sleep(delay) - yield message - - @strawberry.subscription - async def request_ping(self, info) -> typing.AsyncGenerator[bool, None]: - ws = info.context["ws"] - await ws.send_json(PingMessage().as_dict()) - yield True - - @strawberry.subscription - async def infinity(self, message: str) -> typing.AsyncGenerator[str, None]: - while True: - yield message - await asyncio.sleep(1) - - @strawberry.subscription - async def context(self, info) -> typing.AsyncGenerator[str, None]: - yield info.context["custom_value"] - - @strawberry.subscription - async def error(self, message: str) -> typing.AsyncGenerator[str, None]: - yield GraphQLError(message) # type: ignore - - @strawberry.subscription - async def exception(self, message: str) -> typing.AsyncGenerator[str, None]: - raise ValueError(message) - - # Without this yield, the method is not recognised as an async generator - yield "Hi" - - @strawberry.subscription - async def flavors(self) -> typing.AsyncGenerator[Flavor, None]: - yield Flavor.VANILLA - yield Flavor.STRAWBERRY - yield Flavor.CHOCOLATE - - @strawberry.subscription - async def debug(self, info) -> typing.AsyncGenerator[DebugInfo, None]: - active_result_handlers = [ - task for task in info.context["tasks"].values() if not task.done() - ] - - connection_init_timeout_task = info.context["connectionInitTimeoutTask"] - is_connection_init_timeout_task_done = ( - connection_init_timeout_task.done() - if connection_init_timeout_task - else None - ) - - yield DebugInfo( - num_active_result_handlers=len(active_result_handlers), - is_connection_init_timeout_task_done=is_connection_init_timeout_task_done, - ) - - -schema = strawberry.Schema(Query, mutation=Mutation, subscription=Subscription) diff --git a/tests/starlite/test_context.py b/tests/starlite/test_context.py deleted file mode 100644 index 7b6fcfa226..0000000000 --- a/tests/starlite/test_context.py +++ /dev/null @@ -1,180 +0,0 @@ -from typing import Dict - -import strawberry -from starlite import Provide, Starlite -from starlite.testing import TestClient -from strawberry.starlite import BaseContext, make_graphql_controller -from strawberry.types import Info -from tests.starlite.app import create_app - - -def test_base_context(): - base_context = BaseContext() - assert base_context.request is None - - -def test_with_class_context_getter(): - @strawberry.type - class Query: - @strawberry.field - def abc(self, info: Info) -> str: - assert info.context.request is not None - assert info.context.strawberry == "rocks" - return "abc" - - class CustomContext(BaseContext): - def __init__(self, rocks: str): - self.strawberry = rocks - - def custom_context_dependency() -> CustomContext: - return CustomContext(rocks="rocks") - - def get_context(custom_context_dependency: CustomContext): - return custom_context_dependency - - schema = strawberry.Schema(query=Query) - graphql_controller = make_graphql_controller( - path="/graphql", schema=schema, context_getter=get_context - ) - app = Starlite( - route_handlers=[graphql_controller], - dependencies={"custom_context_dependency": Provide(custom_context_dependency)}, - ) - - test_client = TestClient(app) - response = test_client.post("/graphql", json={"query": "{ abc }"}) - - assert response.status_code == 200 - assert response.json() == {"data": {"abc": "abc"}} - - -def test_with_dict_context_getter(): - @strawberry.type - class Query: - @strawberry.field - def abc(self, info: Info) -> str: - assert info.context.get("request") is not None - assert info.context.get("strawberry") == "rocks" - return "abc" - - def custom_context_dependency() -> str: - return "rocks" - - def get_context(custom_context_dependency: str) -> Dict[str, str]: - return {"strawberry": custom_context_dependency} - - schema = strawberry.Schema(query=Query) - graphql_controller = make_graphql_controller( - path="/graphql", schema=schema, context_getter=get_context - ) - app = Starlite( - route_handlers=[graphql_controller], - dependencies={"custom_context_dependency": Provide(custom_context_dependency)}, - ) - test_client = TestClient(app) - response = test_client.post("/graphql", json={"query": "{ abc }"}) - - assert response.status_code == 200 - assert response.json() == {"data": {"abc": "abc"}} - - -def test_without_context_getter(): - @strawberry.type - class Query: - @strawberry.field - def abc(self, info: Info) -> str: - assert info.context.get("request") is not None - assert info.context.get("strawberry") is None - return "abc" - - schema = strawberry.Schema(query=Query) - graphql_controller = make_graphql_controller( - path="/graphql", schema=schema, context_getter=None - ) - app = Starlite(route_handlers=[graphql_controller]) - test_client = TestClient(app) - response = test_client.post("/graphql", json={"query": "{ abc }"}) - - assert response.status_code == 200 - assert response.json() == {"data": {"abc": "abc"}} - - -def test_with_invalid_context_getter(): - @strawberry.type - class Query: - @strawberry.field - def abc(self, info: Info) -> str: - assert info.context.get("request") is not None - assert info.context.get("strawberry") is None - return "abc" - - def custom_context_dependency() -> str: - return "rocks" - - def get_context(custom_context_dependency: str) -> str: - return custom_context_dependency - - schema = strawberry.Schema(query=Query) - graphql_controller = make_graphql_controller( - path="/graphql", schema=schema, context_getter=get_context - ) - app = Starlite( - route_handlers=[graphql_controller], - dependencies={"custom_context_dependency": Provide(custom_context_dependency)}, - ) - test_client = TestClient(app, raise_server_exceptions=True) - # FIXME: -#! assert starlite.exceptions.http_exceptions.InternalServerException is raised - # with pytest.raises( - # InternalServerException, - # r"A dependency failed validation for POST .*" - # ), - # ): - response = test_client.post("/graphql", json={"query": "{ abc }"}) - assert response.status_code == 500 - assert ( - response.json()["detail"] - == "A dependency failed validation for POST http://testserver.local/graphql" - ) - - -def test_custom_context(): - @strawberry.type - class Query: - @strawberry.field - def custom_context_value(self, info: Info) -> str: - return info.context["custom_value"] - - schema = strawberry.Schema(query=Query) - app = create_app(schema=schema) - - test_client = TestClient(app) - response = test_client.post("/graphql", json={"query": "{ customContextValue }"}) - - assert response.status_code == 200 - assert response.json() == {"data": {"customContextValue": "Hi!"}} - - -def test_can_set_background_task(): - task_complete = False - - async def task(): - nonlocal task_complete - task_complete = True - - @strawberry.type - class Query: - @strawberry.field - def something(self, info: Info) -> str: - response = info.context["response"] - response.background.tasks.append(task) - return "foo" - - schema = strawberry.Schema(query=Query) - app = create_app(schema=schema) - - test_client = TestClient(app) - response = test_client.post("/graphql", json={"query": "{ something }"}) - - assert response.json() == {"data": {"something": "foo"}} - assert task_complete diff --git a/tests/starlite/test_graphql_transport_ws.py b/tests/starlite/test_graphql_transport_ws.py deleted file mode 100644 index 322bd39e26..0000000000 --- a/tests/starlite/test_graphql_transport_ws.py +++ /dev/null @@ -1,831 +0,0 @@ -import asyncio -import json -from datetime import timedelta - -import pytest - -from starlite.exceptions import WebSocketDisconnect -from starlite.testing import TestClient -from strawberry.subscriptions import GRAPHQL_TRANSPORT_WS_PROTOCOL -from strawberry.subscriptions.protocols.graphql_transport_ws.types import ( - CompleteMessage, - ConnectionAckMessage, - ConnectionInitMessage, - ErrorMessage, - NextMessage, - PingMessage, - PongMessage, - SubscribeMessage, - SubscribeMessagePayload, -) -from tests.starlite.app import create_app - - -def test_unknown_message_type(test_client): - with test_client.websocket_connect( - "/graphql", [GRAPHQL_TRANSPORT_WS_PROTOCOL] - ) as ws: - ws.send_json({"type": "NOT_A_MESSAGE_TYPE"}) - - with pytest.raises(WebSocketDisconnect) as exc: - ws.receive() - assert exc.value.code == 4400 - - -def test_missing_message_type(test_client): - with test_client.websocket_connect( - "/graphql", [GRAPHQL_TRANSPORT_WS_PROTOCOL] - ) as ws: - ws.send_json({"notType": None}) - - with pytest.raises(WebSocketDisconnect) as exc: - ws.receive() - assert exc.value.code == 4400 - - -def test_parsing_an_invalid_message(test_client): - with test_client.websocket_connect( - "/graphql", [GRAPHQL_TRANSPORT_WS_PROTOCOL] - ) as ws: - ws.send_json({"type": "subscribe", "notPayload": None}) - - with pytest.raises(WebSocketDisconnect) as exc: - ws.receive() - assert exc.value.code == 4400 - - -def test_parsing_an_invalid_payload(test_client): - with test_client.websocket_connect( - "/graphql", [GRAPHQL_TRANSPORT_WS_PROTOCOL] - ) as ws: - ws.send_json({"type": "subscribe", "payload": {"unexpectedField": 42}}) - - with pytest.raises(WebSocketDisconnect) as exc: - ws.receive() - assert exc.value.code == 4400 - - -def test_ws_messages_must_be_text(test_client): - with test_client.websocket_connect( - "/graphql", [GRAPHQL_TRANSPORT_WS_PROTOCOL] - ) as ws: - ws.send_bytes(json.dumps(ConnectionInitMessage().as_dict()).encode()) - - with pytest.raises(WebSocketDisconnect) as exc: - ws.receive() - assert exc.value.code == 4400 - - -async def test_connection_init_timeout(): - app = create_app(connection_init_wait_timeout=timedelta(seconds=0.1)) - test_client = TestClient(app) - - # Hope that the connection init timeout expired - await asyncio.sleep(0.2) - - try: - with test_client.websocket_connect( - "/graphql", [GRAPHQL_TRANSPORT_WS_PROTOCOL] - ) as ws: - data = ws.receive() - assert data["type"] == "websocket.close" - assert data["code"] == 4408 - except WebSocketDisconnect as exc: - assert exc.code == 4408 - - -async def test_connection_init_timeout_cancellation(): - app = create_app(connection_init_wait_timeout=timedelta(milliseconds=500)) - test_client = TestClient(app) - - with test_client.websocket_connect( - "/graphql", [GRAPHQL_TRANSPORT_WS_PROTOCOL] - ) as ws: - ws.send_json(ConnectionInitMessage().as_dict()) - - response = ws.receive_json() - assert response == ConnectionAckMessage().as_dict() - - await asyncio.sleep(1) - - ws.send_json( - SubscribeMessage( - id="sub1", - payload=SubscribeMessagePayload( - query="subscription { debug { isConnectionInitTimeoutTaskDone } }" - ), - ).as_dict() - ) - - response = ws.receive_json() - assert ( - response - == NextMessage( - id="sub1", - payload={"data": {"debug": {"isConnectionInitTimeoutTaskDone": True}}}, - ).as_dict() - ) - - ws.close() - - -def test_too_many_initialisation_requests(test_client): - with test_client.websocket_connect( - "/graphql", [GRAPHQL_TRANSPORT_WS_PROTOCOL] - ) as ws: - ws.send_json(ConnectionInitMessage().as_dict()) - - response = ws.receive_json() - assert response == ConnectionAckMessage().as_dict() - - ws.send_json(ConnectionInitMessage().as_dict()) - - with pytest.raises(WebSocketDisconnect) as exc: - ws.receive() - assert exc.value.code == 4429 - - -def test_ping_pong(test_client): - with test_client.websocket_connect( - "/graphql", [GRAPHQL_TRANSPORT_WS_PROTOCOL] - ) as ws: - ws.send_json(ConnectionInitMessage().as_dict()) - - response = ws.receive_json() - assert response == ConnectionAckMessage().as_dict() - - ws.send_json(PingMessage().as_dict()) - - response = ws.receive_json() - assert response == PongMessage().as_dict() - - ws.close() - - -def test_server_sent_ping(test_client): - with test_client.websocket_connect( - "/graphql", [GRAPHQL_TRANSPORT_WS_PROTOCOL] - ) as ws: - ws.send_json(ConnectionInitMessage().as_dict()) - - response = ws.receive_json() - assert response == ConnectionAckMessage().as_dict() - - ws.send_json( - SubscribeMessage( - id="sub1", - payload=SubscribeMessagePayload(query="subscription { requestPing }"), - ).as_dict() - ) - - response = ws.receive_json() - assert response == PingMessage().as_dict() - - ws.send_json(PongMessage().as_dict()) - - response = ws.receive_json() - assert ( - response - == NextMessage(id="sub1", payload={"data": {"requestPing": True}}).as_dict() - ) - - response = ws.receive_json() - assert response == CompleteMessage(id="sub1").as_dict() - - ws.close() - - -def test_unauthorized_subscriptions(test_client): - with test_client.websocket_connect( - "/graphql", [GRAPHQL_TRANSPORT_WS_PROTOCOL] - ) as ws: - ws.send_json( - SubscribeMessage( - id="sub1", - payload=SubscribeMessagePayload( - query='subscription { echo(message: "Hi") }' - ), - ).as_dict() - ) - - with pytest.raises(WebSocketDisconnect) as exc: - ws.receive() - assert exc.value.code == 4401 - - -def test_duplicated_operation_ids(test_client): - with test_client.websocket_connect( - "/graphql", [GRAPHQL_TRANSPORT_WS_PROTOCOL] - ) as ws: - ws.send_json(ConnectionInitMessage().as_dict()) - - response = ws.receive_json() - assert response == ConnectionAckMessage().as_dict() - - ws.send_json( - SubscribeMessage( - id="sub1", - payload=SubscribeMessagePayload( - query='subscription { echo(message: "Hi", delay: 5) }' - ), - ).as_dict() - ) - - ws.send_json( - SubscribeMessage( - id="sub1", - payload=SubscribeMessagePayload( - query='subscription { echo(message: "Hi", delay: 5) }' - ), - ).as_dict() - ) - - with pytest.raises(WebSocketDisconnect) as exc: - ws.receive() - assert exc.value.code == 4409 - - -def test_reused_operation_ids(test_client): - """ - Test that an operation id can be re-used after it has been - previously used for a completed operation - """ - with test_client.websocket_connect( - "/graphql", [GRAPHQL_TRANSPORT_WS_PROTOCOL] - ) as ws: - ws.send_json(ConnectionInitMessage().as_dict()) - - response = ws.receive_json() - assert response == ConnectionAckMessage().as_dict() - - # Use sub1 as an id for an operation - ws.send_json( - SubscribeMessage( - id="sub1", - payload=SubscribeMessagePayload( - query='subscription { echo(message: "Hi") }' - ), - ).as_dict() - ) - - response = ws.receive_json() - assert ( - response - == NextMessage(id="sub1", payload={"data": {"echo": "Hi"}}).as_dict() - ) - - response = ws.receive_json() - assert response == CompleteMessage(id="sub1").as_dict() - - # operation is now complete. Create a new operation using - # the same ID - ws.send_json( - SubscribeMessage( - id="sub1", - payload=SubscribeMessagePayload( - query='subscription { echo(message: "Hi") }' - ), - ).as_dict() - ) - - response = ws.receive_json() - assert ( - response - == NextMessage(id="sub1", payload={"data": {"echo": "Hi"}}).as_dict() - ) - - -def test_simple_subscription(test_client): - with test_client.websocket_connect( - "/graphql", [GRAPHQL_TRANSPORT_WS_PROTOCOL] - ) as ws: - ws.send_json(ConnectionInitMessage().as_dict()) - - response = ws.receive_json() - assert response == ConnectionAckMessage().as_dict() - - ws.send_json( - SubscribeMessage( - id="sub1", - payload=SubscribeMessagePayload( - query='subscription { echo(message: "Hi") }' - ), - ).as_dict() - ) - - response = ws.receive_json() - assert ( - response - == NextMessage(id="sub1", payload={"data": {"echo": "Hi"}}).as_dict() - ) - - ws.send_json(CompleteMessage(id="sub1").as_dict()) - - ws.close() - - -def test_subscription_syntax_error(test_client): - with test_client.websocket_connect( - "/graphql", [GRAPHQL_TRANSPORT_WS_PROTOCOL] - ) as ws: - ws.send_json(ConnectionInitMessage().as_dict()) - - response = ws.receive_json() - assert response == ConnectionAckMessage().as_dict() - - ws.send_json( - SubscribeMessage( - id="sub1", - payload=SubscribeMessagePayload(query="subscription { INVALID_SYNTAX "), - ).as_dict() - ) - - with pytest.raises(WebSocketDisconnect) as exc: - ws.receive() - assert exc.value.code == 4400 - - -def test_subscription_field_errors(test_client): - with test_client.websocket_connect( - "/graphql", [GRAPHQL_TRANSPORT_WS_PROTOCOL] - ) as ws: - ws.send_json(ConnectionInitMessage().as_dict()) - - response = ws.receive_json() - assert response == ConnectionAckMessage().as_dict() - - ws.send_json( - SubscribeMessage( - id="sub1", - payload=SubscribeMessagePayload( - query="subscription { notASubscriptionField }", - ), - ).as_dict() - ) - - response = ws.receive_json() - assert response["type"] == ErrorMessage.type - assert response["id"] == "sub1" - assert len(response["payload"]) == 1 - assert response["payload"][0].get("path") is None - assert response["payload"][0]["locations"] == [{"line": 1, "column": 16}] - assert ( - response["payload"][0]["message"] - == "The subscription field 'notASubscriptionField' is not defined." - ) - - ws.close() - - -def test_subscription_cancellation(test_client): - with test_client.websocket_connect( - "/graphql", [GRAPHQL_TRANSPORT_WS_PROTOCOL] - ) as ws: - ws.send_json(ConnectionInitMessage().as_dict()) - - response = ws.receive_json() - assert response == ConnectionAckMessage().as_dict() - - ws.send_json( - SubscribeMessage( - id="sub1", - payload=SubscribeMessagePayload( - query='subscription { echo(message: "Hi", delay: 99) }' - ), - ).as_dict() - ) - - ws.send_json( - SubscribeMessage( - id="sub2", - payload=SubscribeMessagePayload( - query="subscription { debug { numActiveResultHandlers } }", - ), - ).as_dict() - ) - - response = ws.receive_json() - assert ( - response - == NextMessage( - id="sub2", payload={"data": {"debug": {"numActiveResultHandlers": 2}}} - ).as_dict() - ) - - response = ws.receive_json() - assert response == CompleteMessage(id="sub2").as_dict() - - ws.send_json(CompleteMessage(id="sub1").as_dict()) - - ws.send_json( - SubscribeMessage( - id="sub3", - payload=SubscribeMessagePayload( - query="subscription { debug { numActiveResultHandlers } }", - ), - ).as_dict() - ) - - response = ws.receive_json() - assert ( - response - == NextMessage( - id="sub3", payload={"data": {"debug": {"numActiveResultHandlers": 1}}} - ).as_dict() - ) - - response = ws.receive_json() - assert response == CompleteMessage(id="sub3").as_dict() - - ws.close() - - -def test_subscription_errors(test_client): - with test_client.websocket_connect( - "/graphql", [GRAPHQL_TRANSPORT_WS_PROTOCOL] - ) as ws: - ws.send_json(ConnectionInitMessage().as_dict()) - - response = ws.receive_json() - assert response == ConnectionAckMessage().as_dict() - - ws.send_json( - SubscribeMessage( - id="sub1", - payload=SubscribeMessagePayload( - query='subscription { error(message: "TEST ERR") }', - ), - ).as_dict() - ) - - response = ws.receive_json() - assert response["type"] == ErrorMessage.type - assert response["id"] == "sub1" - assert len(response["payload"]) == 1 - assert response["payload"][0]["path"] == ["error"] - assert response["payload"][0]["message"] == "TEST ERR" - - ws.close() - - -def test_subscription_exceptions(test_client): - with test_client.websocket_connect( - "/graphql", [GRAPHQL_TRANSPORT_WS_PROTOCOL] - ) as ws: - ws.send_json(ConnectionInitMessage().as_dict()) - - response = ws.receive_json() - assert response == ConnectionAckMessage().as_dict() - - ws.send_json( - SubscribeMessage( - id="sub1", - payload=SubscribeMessagePayload( - query='subscription { exception(message: "TEST EXC") }', - ), - ).as_dict() - ) - - response = ws.receive_json() - assert response["type"] == ErrorMessage.type - assert response["id"] == "sub1" - assert len(response["payload"]) == 1 - assert response["payload"][0].get("path") is None - assert response["payload"][0].get("locations") is None - assert response["payload"][0]["message"] == "TEST EXC" - - ws.close() - - -def test_single_result_query_operation(test_client): - with test_client.websocket_connect( - "/graphql", [GRAPHQL_TRANSPORT_WS_PROTOCOL] - ) as ws: - ws.send_json(ConnectionInitMessage().as_dict()) - - response = ws.receive_json() - assert response == ConnectionAckMessage().as_dict() - - ws.send_json( - SubscribeMessage( - id="sub1", - payload=SubscribeMessagePayload(query="query { hello }"), - ).as_dict() - ) - - response = ws.receive_json() - assert ( - response - == NextMessage( - id="sub1", payload={"data": {"hello": "Hello world"}} - ).as_dict() - ) - - response = ws.receive_json() - assert response == CompleteMessage(id="sub1").as_dict() - - -def test_single_result_query_operation_async(test_client): - """ - Test a single result query operation on an - `async` method in the schema, including an artificial - async delay - """ - with test_client.websocket_connect( - "/graphql", [GRAPHQL_TRANSPORT_WS_PROTOCOL] - ) as ws: - ws.send_json(ConnectionInitMessage().as_dict()) - - response = ws.receive_json() - assert response == ConnectionAckMessage().as_dict() - - ws.send_json( - SubscribeMessage( - id="sub1", - payload=SubscribeMessagePayload( - query='query { asyncHello(name: "Dolly", delay:0.01)}' - ), - ).as_dict() - ) - - response = ws.receive_json() - assert ( - response - == NextMessage( - id="sub1", payload={"data": {"asyncHello": "Hello Dolly"}} - ).as_dict() - ) - - response = ws.receive_json() - assert response == CompleteMessage(id="sub1").as_dict() - - -def test_single_result_query_operation_overlapped(test_client): - """ - Test that two single result queries can be in flight at the same time, - just like regular queries. Start two queries with separate ids. The - first query has a delay, so we expect the response to the second - query to be delivered first. - """ - with test_client.websocket_connect( - "/graphql", [GRAPHQL_TRANSPORT_WS_PROTOCOL] - ) as ws: - ws.send_json(ConnectionInitMessage().as_dict()) - - response = ws.receive_json() - assert response == ConnectionAckMessage().as_dict() - - # first query - ws.send_json( - SubscribeMessage( - id="sub1", - payload=SubscribeMessagePayload( - query='query { asyncHello(name: "Dolly", delay:1)}' - ), - ).as_dict() - ) - # second query - ws.send_json( - SubscribeMessage( - id="sub2", - payload=SubscribeMessagePayload( - query='query { asyncHello(name: "Dolly", delay:0)}' - ), - ).as_dict() - ) - - # we expect the response to the second query to arrive first - response = ws.receive_json() - assert ( - response - == NextMessage( - id="sub2", payload={"data": {"asyncHello": "Hello Dolly"}} - ).as_dict() - ) - response = ws.receive_json() - assert response == CompleteMessage(id="sub2").as_dict() - - -def test_single_result_mutation_operation(test_client): - with test_client.websocket_connect( - "/graphql", [GRAPHQL_TRANSPORT_WS_PROTOCOL] - ) as ws: - ws.send_json(ConnectionInitMessage().as_dict()) - - response = ws.receive_json() - assert response == ConnectionAckMessage().as_dict() - - ws.send_json( - SubscribeMessage( - id="sub1", - payload=SubscribeMessagePayload(query="mutation { hello }"), - ).as_dict() - ) - - response = ws.receive_json() - assert ( - response - == NextMessage( - id="sub1", payload={"data": {"hello": "strawberry"}} - ).as_dict() - ) - - response = ws.receive_json() - assert response == CompleteMessage(id="sub1").as_dict() - - -def test_single_result_operation_selection(test_client): - with test_client.websocket_connect( - "/graphql", [GRAPHQL_TRANSPORT_WS_PROTOCOL] - ) as ws: - ws.send_json(ConnectionInitMessage().as_dict()) - - response = ws.receive_json() - assert response == ConnectionAckMessage().as_dict() - - query = """ - query Query1 { - hello - } - query Query2 { - hello(name: "Strawberry") - } - """ - - ws.send_json( - SubscribeMessage( - id="sub1", - payload=SubscribeMessagePayload(query=query, operationName="Query2"), - ).as_dict() - ) - - response = ws.receive_json() - assert ( - response - == NextMessage( - id="sub1", payload={"data": {"hello": "Hello Strawberry"}} - ).as_dict() - ) - - response = ws.receive_json() - assert response == CompleteMessage(id="sub1").as_dict() - - -def test_single_result_invalid_operation_selection(test_client): - with test_client.websocket_connect( - "/graphql", [GRAPHQL_TRANSPORT_WS_PROTOCOL] - ) as ws: - ws.send_json(ConnectionInitMessage().as_dict()) - - response = ws.receive_json() - assert response == ConnectionAckMessage().as_dict() - - query = """ - query Query1 { - hello - } - """ - - ws.send_json( - SubscribeMessage( - id="sub1", - payload=SubscribeMessagePayload(query=query, operationName="Query2"), - ).as_dict() - ) - - with pytest.raises(WebSocketDisconnect) as exc: - ws.receive() - assert exc.value.code == 4400 - - -def test_single_result_operation_error(test_client): - with test_client.websocket_connect( - "/graphql", [GRAPHQL_TRANSPORT_WS_PROTOCOL] - ) as ws: - ws.send_json(ConnectionInitMessage().as_dict()) - - response = ws.receive_json() - assert response == ConnectionAckMessage().as_dict() - - ws.send_json( - SubscribeMessage( - id="sub1", - payload=SubscribeMessagePayload( - query="query { alwaysFail }", - ), - ).as_dict() - ) - - response = ws.receive_json() - assert response["type"] == ErrorMessage.type - assert response["id"] == "sub1" - assert len(response["payload"]) == 1 - assert response["payload"][0]["message"] == "You are not authorized" - - -def test_single_result_operation_exception(test_client): - """ - Test that single-result-operations which raise exceptions - behave in the same way as streaming operations - """ - with test_client.websocket_connect( - "/graphql", [GRAPHQL_TRANSPORT_WS_PROTOCOL] - ) as ws: - ws.send_json(ConnectionInitMessage().as_dict()) - - response = ws.receive_json() - assert response == ConnectionAckMessage().as_dict() - - ws.send_json( - SubscribeMessage( - id="sub1", - payload=SubscribeMessagePayload( - query='query { exception(message: "bummer") }', - ), - ).as_dict() - ) - - response = ws.receive_json() - assert response["type"] == ErrorMessage.type - assert response["id"] == "sub1" - assert len(response["payload"]) == 1 - assert response["payload"][0].get("path") == ["exception"] - assert response["payload"][0]["message"] == "bummer" - - -def test_single_result_duplicate_ids_sub(test_client): - """ - Test that single-result-operations and streaming operations - share the same ID namespace. Start a regular subscription, - then issue a single-result operation with same ID and expect an - error due to already existing ID - """ - with test_client.websocket_connect( - "/graphql", [GRAPHQL_TRANSPORT_WS_PROTOCOL] - ) as ws: - ws.send_json(ConnectionInitMessage().as_dict()) - - response = ws.receive_json() - assert response == ConnectionAckMessage().as_dict() - - # regular subscription - ws.send_json( - SubscribeMessage( - id="sub1", - payload=SubscribeMessagePayload( - query='subscription { echo(message: "Hi", delay: 5) }' - ), - ).as_dict() - ) - # single result subscription with duplicate id - ws.send_json( - SubscribeMessage( - id="sub1", - payload=SubscribeMessagePayload( - query="query { hello }", - ), - ).as_dict() - ) - - with pytest.raises(WebSocketDisconnect) as exc: - ws.receive() - assert exc.value.code == 4409 - - -def test_single_result_duplicate_ids_query(test_client): - """ - Test that single-result-operations don't allow duplicate - IDs for two asynchronous queries. Issue one async query - with delay, then another with same id. Expect error. - """ - with test_client.websocket_connect( - "/graphql", [GRAPHQL_TRANSPORT_WS_PROTOCOL] - ) as ws: - ws.send_json(ConnectionInitMessage().as_dict()) - - response = ws.receive_json() - assert response == ConnectionAckMessage().as_dict() - - # single result subscription 1 - ws.send_json( - SubscribeMessage( - id="sub1", - payload=SubscribeMessagePayload( - query='query { asyncHello(name: "Hi", delay: 5) }' - ), - ).as_dict() - ) - # single result subscription with duplicate id - ws.send_json( - SubscribeMessage( - id="sub1", - payload=SubscribeMessagePayload( - query="query { hello }", - ), - ).as_dict() - ) - - # We expect the remote to close the socket due to duplicate ID in use - with pytest.raises(WebSocketDisconnect) as exc: - ws.receive() - assert exc.value.code == 4409 diff --git a/tests/starlite/test_graphql_ws.py b/tests/starlite/test_graphql_ws.py deleted file mode 100644 index c06c586a5a..0000000000 --- a/tests/starlite/test_graphql_ws.py +++ /dev/null @@ -1,518 +0,0 @@ -import pytest - -from starlite.exceptions import WebSocketDisconnect -from strawberry.subscriptions import GRAPHQL_WS_PROTOCOL -from strawberry.subscriptions.protocols.graphql_ws import ( - GQL_COMPLETE, - GQL_CONNECTION_ACK, - GQL_CONNECTION_INIT, - GQL_CONNECTION_KEEP_ALIVE, - GQL_CONNECTION_TERMINATE, - GQL_DATA, - GQL_ERROR, - GQL_START, - GQL_STOP, -) - - -def test_simple_subscription(test_client): - with test_client.websocket_connect("/graphql", [GRAPHQL_WS_PROTOCOL]) as ws: - ws.send_json({"type": GQL_CONNECTION_INIT}) - ws.send_json( - { - "type": GQL_START, - "id": "demo", - "payload": { - "query": 'subscription { echo(message: "Hi") }', - }, - } - ) - - response = ws.receive_json() - assert response["type"] == GQL_CONNECTION_ACK - - response = ws.receive_json() - assert response["type"] == GQL_DATA - assert response["id"] == "demo" - assert response["payload"]["data"] == {"echo": "Hi"} - - ws.send_json({"type": GQL_STOP, "id": "demo"}) - response = ws.receive_json() - assert response["type"] == GQL_COMPLETE - assert response["id"] == "demo" - - ws.send_json({"type": GQL_CONNECTION_TERMINATE}) - - # make sure the websocket is disconnected now - with pytest.raises(WebSocketDisconnect): - ws.receive_json() - - -def test_operation_selection(test_client): - with test_client.websocket_connect("/graphql", [GRAPHQL_WS_PROTOCOL]) as ws: - ws.send_json({"type": GQL_CONNECTION_INIT}) - ws.send_json( - { - "type": GQL_START, - "id": "demo", - "payload": { - "query": """ - subscription Subscription1 { echo(message: "Hi1") } - subscription Subscription2 { echo(message: "Hi2") } - """, - "operationName": "Subscription2", - }, - } - ) - - response = ws.receive_json() - assert response["type"] == GQL_CONNECTION_ACK - - response = ws.receive_json() - assert response["type"] == GQL_DATA - assert response["id"] == "demo" - assert response["payload"]["data"] == {"echo": "Hi2"} - - ws.send_json({"type": GQL_STOP, "id": "demo"}) - response = ws.receive_json() - assert response["type"] == GQL_COMPLETE - assert response["id"] == "demo" - - ws.send_json({"type": GQL_CONNECTION_TERMINATE}) - - # make sure the websocket is disconnected now - with pytest.raises(WebSocketDisconnect): - ws.receive_json() - - -def test_sends_keep_alive(test_client_keep_alive): - with test_client_keep_alive.websocket_connect( - "/graphql", [GRAPHQL_WS_PROTOCOL] - ) as ws: - ws.send_json({"type": GQL_CONNECTION_INIT}) - ws.send_json( - { - "type": GQL_START, - "id": "demo", - "payload": { - "query": 'subscription { echo(message: "Hi", delay: 0.15) }' - }, - } - ) - - response = ws.receive_json() - assert response["type"] == GQL_CONNECTION_ACK - - response = ws.receive_json() - assert response["type"] == GQL_CONNECTION_KEEP_ALIVE - - response = ws.receive_json() - assert response["type"] == GQL_CONNECTION_KEEP_ALIVE - - response = ws.receive_json() - assert response["type"] == GQL_DATA - assert response["id"] == "demo" - assert response["payload"]["data"] == {"echo": "Hi"} - - response = ws.receive_json() - assert response["type"] == GQL_COMPLETE - assert response["id"] == "demo" - - ws.send_json({"type": GQL_CONNECTION_TERMINATE}) - - -def test_subscription_cancellation(test_client): - with test_client.websocket_connect("/graphql", [GRAPHQL_WS_PROTOCOL]) as ws: - ws.send_json({"type": GQL_CONNECTION_INIT}) - response = ws.receive_json() - assert response["type"] == GQL_CONNECTION_ACK - - ws.send_json( - { - "type": GQL_START, - "id": "demo", - "payload": {"query": 'subscription { echo(message: "Hi", delay: 99) }'}, - } - ) - - ws.send_json( - { - "type": GQL_START, - "id": "debug1", - "payload": { - "query": "subscription { debug { numActiveResultHandlers } }", - }, - } - ) - - response = ws.receive_json() - assert response["type"] == GQL_DATA - assert response["id"] == "debug1" - assert response["payload"]["data"] == {"debug": {"numActiveResultHandlers": 2}} - - response = ws.receive_json() - assert response["type"] == GQL_COMPLETE - assert response["id"] == "debug1" - - ws.send_json({"type": GQL_STOP, "id": "demo"}) - response = ws.receive_json() - assert response["type"] == GQL_COMPLETE - assert response["id"] == "demo" - - ws.send_json( - { - "type": GQL_START, - "id": "debug2", - "payload": { - "query": "subscription { debug { numActiveResultHandlers} }", - }, - } - ) - - response = ws.receive_json() - assert response["type"] == GQL_DATA - assert response["id"] == "debug2" - assert response["payload"]["data"] == {"debug": {"numActiveResultHandlers": 1}} - - response = ws.receive_json() - assert response["type"] == GQL_COMPLETE - assert response["id"] == "debug2" - - ws.send_json({"type": GQL_CONNECTION_TERMINATE}) - - # make sure the websocket is disconnected now - with pytest.raises(WebSocketDisconnect): - ws.receive_json() - - -def test_subscription_errors(test_client): - with test_client.websocket_connect("/graphql", [GRAPHQL_WS_PROTOCOL]) as ws: - ws.send_json({"type": GQL_CONNECTION_INIT}) - ws.send_json( - { - "type": GQL_START, - "id": "demo", - "payload": {"query": 'subscription { error(message: "TEST ERR") }'}, - } - ) - - response = ws.receive_json() - assert response["type"] == GQL_CONNECTION_ACK - - response = ws.receive_json() - assert response["type"] == GQL_DATA - assert response["id"] == "demo" - assert response["payload"]["data"] is None - assert response["payload"]["errors"][0]["path"] == ["error"] - assert response["payload"]["errors"][0]["message"] == "TEST ERR" - - response = ws.receive_json() - assert response["type"] == GQL_COMPLETE - assert response["id"] == "demo" - - ws.send_json({"type": GQL_CONNECTION_TERMINATE}) - - # make sure the websocket is disconnected now - with pytest.raises(WebSocketDisconnect): - ws.receive_json() - - -def test_subscription_exceptions(test_client): - with test_client.websocket_connect("/graphql", [GRAPHQL_WS_PROTOCOL]) as ws: - ws.send_json({"type": GQL_CONNECTION_INIT}) - ws.send_json( - { - "type": GQL_START, - "id": "demo", - "payload": {"query": 'subscription { exception(message: "TEST EXC") }'}, - } - ) - - response = ws.receive_json() - assert response["type"] == GQL_CONNECTION_ACK - - response = ws.receive_json() - assert response["type"] == GQL_DATA - assert response["id"] == "demo" - assert response["payload"]["data"] is None - assert response["payload"]["errors"] == [{"message": "TEST EXC"}] - - ws.send_json({"type": GQL_STOP, "id": "demo"}) - response = ws.receive_json() - assert response["type"] == GQL_COMPLETE - assert response["id"] == "demo" - - ws.send_json({"type": GQL_CONNECTION_TERMINATE}) - - # make sure the websocket is disconnected now - with pytest.raises(WebSocketDisconnect): - ws.receive_json() - - -def test_subscription_field_error(test_client): - with test_client.websocket_connect("/graphql", [GRAPHQL_WS_PROTOCOL]) as ws: - ws.send_json({"type": GQL_CONNECTION_INIT}) - ws.send_json( - { - "type": GQL_START, - "id": "invalid-field", - "payload": {"query": "subscription { notASubscriptionField }"}, - } - ) - - response = ws.receive_json() - assert response["type"] == GQL_CONNECTION_ACK - - response = ws.receive_json() - assert response["type"] == GQL_ERROR - assert response["id"] == "invalid-field" - assert response["payload"] == { - "locations": [{"line": 1, "column": 16}], - "message": ( - "The subscription field 'notASubscriptionField' is not defined." - ), - } - - ws.send_json({"type": GQL_CONNECTION_TERMINATE}) - - # make sure the websocket is disconnected now - with pytest.raises(WebSocketDisconnect): - ws.receive_json() - - -def test_subscription_syntax_error(test_client): - with test_client.websocket_connect("/graphql", [GRAPHQL_WS_PROTOCOL]) as ws: - ws.send_json({"type": GQL_CONNECTION_INIT}) - ws.send_json( - { - "type": GQL_START, - "id": "syntax-error", - "payload": {"query": "subscription { example "}, - } - ) - - response = ws.receive_json() - assert response["type"] == GQL_CONNECTION_ACK - - response = ws.receive_json() - assert response["type"] == GQL_ERROR - assert response["id"] == "syntax-error" - assert response["payload"] == { - "locations": [{"line": 1, "column": 24}], - "message": "Syntax Error: Expected Name, found .", - } - - ws.send_json({"type": GQL_CONNECTION_TERMINATE}) - - # make sure the websocket is disconnected now - with pytest.raises(WebSocketDisconnect): - ws.receive_json() - - -def test_non_text_ws_messages_are_ignored(test_client): - with test_client.websocket_connect("/graphql", [GRAPHQL_WS_PROTOCOL]) as ws: - ws.send_bytes(b"") - ws.send_json({"type": GQL_CONNECTION_INIT}) - - ws.send_bytes(b"") - ws.send_json( - { - "type": GQL_START, - "id": "demo", - "payload": { - "query": 'subscription { echo(message: "Hi") }', - }, - } - ) - - response = ws.receive_json() - assert response["type"] == GQL_CONNECTION_ACK - - response = ws.receive_json() - assert response["type"] == GQL_DATA - assert response["id"] == "demo" - assert response["payload"]["data"] == {"echo": "Hi"} - - ws.send_bytes(b"") - ws.send_json({"type": GQL_STOP, "id": "demo"}) - response = ws.receive_json() - assert response["type"] == GQL_COMPLETE - assert response["id"] == "demo" - - ws.send_bytes(b"") - ws.send_json({"type": GQL_CONNECTION_TERMINATE}) - - # make sure the websocket is disconnected now - with pytest.raises(WebSocketDisconnect): - ws.receive_json() - - -def test_unknown_protocol_messages_are_ignored(test_client): - with test_client.websocket_connect("/graphql", [GRAPHQL_WS_PROTOCOL]) as ws: - ws.send_json({"type": "NotAProtocolMessage"}) - ws.send_json({"type": GQL_CONNECTION_INIT}) - - ws.send_json({"type": "NotAProtocolMessage"}) - ws.send_json( - { - "type": GQL_START, - "id": "demo", - "payload": { - "query": 'subscription { echo(message: "Hi") }', - }, - } - ) - - response = ws.receive_json() - assert response["type"] == GQL_CONNECTION_ACK - - response = ws.receive_json() - assert response["type"] == GQL_DATA - assert response["id"] == "demo" - assert response["payload"]["data"] == {"echo": "Hi"} - - ws.send_json({"type": "NotAProtocolMessage"}) - ws.send_json({"type": GQL_STOP, "id": "demo"}) - response = ws.receive_json() - assert response["type"] == GQL_COMPLETE - assert response["id"] == "demo" - - ws.send_json({"type": "NotAProtocolMessage"}) - ws.send_json({"type": GQL_CONNECTION_TERMINATE}) - - # make sure the websocket is disconnected now - with pytest.raises(WebSocketDisconnect): - ws.receive_json() - - -def test_custom_context(test_client): - with test_client.websocket_connect("/graphql", [GRAPHQL_WS_PROTOCOL]) as ws: - ws.send_json({"type": GQL_CONNECTION_INIT}) - ws.send_json( - { - "type": GQL_START, - "id": "demo", - "payload": { - "query": "subscription { context }", - }, - } - ) - - response = ws.receive_json() - assert response["type"] == GQL_CONNECTION_ACK - - response = ws.receive_json() - assert response["type"] == GQL_DATA - assert response["id"] == "demo" - assert response["payload"]["data"] == {"context": "Hi!"} - - ws.send_json({"type": GQL_STOP, "id": "demo"}) - response = ws.receive_json() - assert response["type"] == GQL_COMPLETE - assert response["id"] == "demo" - - ws.send_json({"type": GQL_CONNECTION_TERMINATE}) - - # make sure the websocket is disconnected now - with pytest.raises(WebSocketDisconnect): - ws.receive_json() - - -def test_resolving_enums(test_client): - with test_client.websocket_connect("/graphql", [GRAPHQL_WS_PROTOCOL]) as ws: - ws.send_json({"type": GQL_CONNECTION_INIT}) - ws.send_json( - { - "type": GQL_START, - "id": "demo", - "payload": { - "query": "subscription { flavors }", - }, - } - ) - - response = ws.receive_json() - assert response["type"] == GQL_CONNECTION_ACK - - response = ws.receive_json() - assert response["type"] == GQL_DATA - assert response["id"] == "demo" - assert response["payload"]["data"] == {"flavors": "VANILLA"} - - response = ws.receive_json() - assert response["type"] == GQL_DATA - assert response["id"] == "demo" - assert response["payload"]["data"] == {"flavors": "STRAWBERRY"} - - response = ws.receive_json() - assert response["type"] == GQL_DATA - assert response["id"] == "demo" - assert response["payload"]["data"] == {"flavors": "CHOCOLATE"} - - ws.send_json({"type": GQL_STOP, "id": "demo"}) - response = ws.receive_json() - assert response["type"] == GQL_COMPLETE - assert response["id"] == "demo" - - ws.send_json({"type": GQL_CONNECTION_TERMINATE}) - - # make sure the websocket is disconnected now - with pytest.raises(WebSocketDisconnect): - ws.receive_json() - - -def test_task_cancellation_separation(test_client): - connection1 = test_client.websocket_connect("/graphql", [GRAPHQL_WS_PROTOCOL]) - connection2 = test_client.websocket_connect("/graphql", [GRAPHQL_WS_PROTOCOL]) - - with connection1 as ws1, connection2 as ws2: - start_payload = { - "type": GQL_START, - "id": "demo", - "payload": {"query": 'subscription { echo(message: "Hi", delay: 99) }'}, - } - - # 0 active result handler tasks - - ws1.send_json({"type": GQL_CONNECTION_INIT}) - ws1.send_json(start_payload) - ws1.receive_json() - - # 1 active result handler tasks - - ws2.send_json({"type": GQL_CONNECTION_INIT}) - ws2.send_json(start_payload) - ws2.receive_json() - - # 2 active result handler tasks - - ws1.send_json({"type": GQL_STOP, "id": "demo"}) - ws1.receive_json() # complete - - # 1 active result handler tasks - - ws2.send_json({"type": GQL_STOP, "id": "demo"}) - ws2.receive_json() # complete - - # 1 active result handler tasks - - ws1.send_json( - { - "type": GQL_START, - "id": "debug1", - "payload": { - "query": "subscription { debug { numActiveResultHandlers } }", - }, - } - ) - - response = ws1.receive_json() - assert response["type"] == GQL_DATA - assert response["id"] == "debug1" - - # The one active result handler is the one for this debug subscription - assert response["payload"]["data"] == {"debug": {"numActiveResultHandlers": 1}} - - response = ws1.receive_json() - assert response["type"] == GQL_COMPLETE - assert response["id"] == "debug1" diff --git a/tests/starlite/test_response_headers.py b/tests/starlite/test_response_headers.py deleted file mode 100644 index a959f557f2..0000000000 --- a/tests/starlite/test_response_headers.py +++ /dev/null @@ -1,60 +0,0 @@ -import strawberry -from starlite import Starlite -from starlite.testing import TestClient -from strawberry.starlite import make_graphql_controller -from strawberry.types import Info - - -# TODO: move this to common tests -def test_set_response_headers(): - @strawberry.type - class Query: - @strawberry.field - def abc(self, info: Info) -> str: - assert info.context.get("response") is not None - info.context["response"].headers["X-Strawberry"] = "rocks" - return "abc" - - schema = strawberry.Schema(query=Query) - graphql_controller = make_graphql_controller(path="/graphql", schema=schema) - app = Starlite(route_handlers=[graphql_controller]) - - test_client = TestClient(app) - response = test_client.post("/graphql", json={"query": "{ abc }"}) - - assert response.status_code == 200 - assert response.json() == {"data": {"abc": "abc"}} - - assert response.headers["x-strawberry"] == "rocks" - - -def test_set_cookie_headers(): - @strawberry.type - class Query: - @strawberry.field - def abc(self, info: Info) -> str: - assert info.context.get("response") is not None - info.context["response"].set_cookie( - key="strawberry", - value="rocks", - ) - info.context["response"].set_cookie( - key="Starlite", - value="rocks", - ) - return "abc" - - schema = strawberry.Schema(query=Query) - graphql_controller = make_graphql_controller(path="/graphql", schema=schema) - app = Starlite(route_handlers=[graphql_controller]) - - test_client = TestClient(app) - response = test_client.post("/graphql", json={"query": "{ abc }"}) - - assert response.status_code == 200 - assert response.json() == {"data": {"abc": "abc"}} - - assert response.headers["set-cookie"] == ( - "strawberry=rocks; Path=/; SameSite=lax, " - "Starlite=rocks; Path=/; SameSite=lax" - ) diff --git a/tests/starlite/test_response_status.py b/tests/starlite/test_response_status.py deleted file mode 100644 index 239e7b7131..0000000000 --- a/tests/starlite/test_response_status.py +++ /dev/null @@ -1,46 +0,0 @@ -from starlette import status - -import strawberry -from starlite import Starlite -from starlite.testing import TestClient -from strawberry.starlite import make_graphql_controller -from strawberry.types import Info - - -# TODO: move this to common tests -def test_set_custom_http_response_status(): - @strawberry.type - class Query: - @strawberry.field - def abc(self, info: Info) -> str: - assert info.context.get("response") is not None - info.context["response"].status_code = status.HTTP_418_IM_A_TEAPOT - return "abc" - - schema = strawberry.Schema(query=Query) - graphql_controller = make_graphql_controller(path="/graphql", schema=schema) - app = Starlite(route_handlers=[graphql_controller]) - - test_client = TestClient(app) - response = test_client.post("/graphql", json={"query": "{ abc }"}) - - assert response.status_code == 418 - assert response.json() == {"data": {"abc": "abc"}} - - -def test_set_without_setting_http_response_status(): - @strawberry.type - class Query: - @strawberry.field - def abc(self) -> str: - return "abc" - - schema = strawberry.Schema(query=Query) - graphql_controller = make_graphql_controller(path="/graphql", schema=schema) - app = Starlite(route_handlers=[graphql_controller]) - - test_client = TestClient(app) - response = test_client.post("/graphql", json={"query": "{ abc }"}) - - assert response.status_code == 200 - assert response.json() == {"data": {"abc": "abc"}} diff --git a/tests/test_auto.py b/tests/test_auto.py deleted file mode 100644 index f181bc78bd..0000000000 --- a/tests/test_auto.py +++ /dev/null @@ -1,55 +0,0 @@ -from typing import Any, cast -from typing_extensions import Annotated, get_args - -import strawberry -from strawberry.annotation import StrawberryAnnotation -from strawberry.auto import StrawberryAuto, auto -from strawberry.type import StrawberryList - - -@strawberry.type -class ExampleType: - some_var: str - - -def test_singleton(): - assert get_args(auto)[1] is StrawberryAuto() - assert StrawberryAuto() is StrawberryAuto() - - -def test_annotated(): - assert get_args(auto) == (Any, StrawberryAuto()) - some_obj = object() - new_annotated = Annotated[auto, some_obj] - assert get_args(new_annotated) == (Any, StrawberryAuto(), some_obj) - - -def test_str(): - assert str(StrawberryAuto()) == "auto" - - -def test_repr(): - assert repr(StrawberryAuto()) == "" - - -def test_isinstance(): - assert isinstance(auto, StrawberryAuto) - assert not isinstance(object, StrawberryAuto) - assert not isinstance(cast(Any, object()), StrawberryAuto) - - -def test_isinstance_with_annotation(): - annotation = StrawberryAnnotation(auto) - assert isinstance(annotation, StrawberryAuto) - str_annotation = StrawberryAnnotation("auto", namespace=globals()) - assert isinstance(str_annotation, StrawberryAuto) - - -def test_isinstance_with_annotated(): - assert isinstance(Annotated[auto, object()], StrawberryAuto) - assert not isinstance(Annotated[str, auto], StrawberryAuto) - - -def test_isinstance_with_unresolvable_annotation(): - type_ = StrawberryList(of_type=ExampleType) - assert not isinstance(type_, StrawberryAuto) diff --git a/tests/test_dataloaders.py b/tests/test_dataloaders.py index 72717dc060..34eec646bf 100644 --- a/tests/test_dataloaders.py +++ b/tests/test_dataloaders.py @@ -1,11 +1,11 @@ import asyncio -from typing import cast import pytest -from strawberry.dataloader import AbstractCache, DataLoader +from strawberry.dataloader import DataLoader from strawberry.exceptions import WrongNumberOfResultsReturned + pytestmark = pytest.mark.asyncio @@ -217,261 +217,3 @@ async def run(): assert data == 1 mock_loader.assert_called_once_with([1]) - - -async def test_prime(): - async def idx(keys): - assert keys, "At least one key must be specified" - return keys - - loader = DataLoader(load_fn=idx) - - # Basic behavior intact - a1 = loader.load(1) - assert await a1 == 1 - - # Prime doesn't overrides value - loader.prime(1, 1.1) - loader.prime(2, 2.1) - b1 = loader.load(1) - b2 = loader.load(2) - assert await b1 == 1 - assert await b2 == 2.1 - - # Unless you tell it to - loader.prime(1, 1.2, force=True) - loader.prime(2, 2.2, force=True) - b1 = loader.load(1) - b2 = loader.load(2) - assert await b1 == 1.2 - assert await b2 == 2.2 - - # Preset will override pending values, but not cached values - c2 = loader.load(2) # This is in cache - c3 = loader.load(3) # This is pending - loader.prime_many({2: 2.3, 3: 3.3}, force=True) - assert await c2 == 2.2 - assert await c3 == 3.3 - - # If we prime all keys in a batch, the load_fn is never called - # (See assertion in idx) - c4 = loader.load(4) - loader.prime_many({4: 4.4}) - assert await c4 == 4.4 - - # Yield to ensure the last batch has been dispatched, - # despite all values being primed - await asyncio.sleep(0) - - -async def test_prime_nocache(): - async def idx(keys): - assert keys, "At least one key must be specified" - return keys - - loader = DataLoader(load_fn=idx, cache=False) - - # Primed value is ignored - loader.prime(1, 1.1) - a1 = loader.load(1) - assert await a1 == 1 - - # Unless it affects pending value in the current batch - b1 = loader.load(2) - loader.prime(2, 2.2) - assert await b1 == 2.2 - - # Yield to ensure the last batch has been dispatched, - # despite all values being primed - await asyncio.sleep(0) - - -async def test_clear(): - batch_num = 0 - - async def idx(keys): - """Maps key => (key, batch_num)""" - nonlocal batch_num - batch_num += 1 - return [(key, batch_num) for key in keys] - - loader = DataLoader(load_fn=idx) - - assert await loader.load_many([1, 2, 3]) == [(1, 1), (2, 1), (3, 1)] - - loader.clear(1) - - assert await loader.load_many([1, 2, 3]) == [(1, 2), (2, 1), (3, 1)] - - loader.clear_many([1, 2]) - - assert await loader.load_many([1, 2, 3]) == [(1, 3), (2, 3), (3, 1)] - - loader.clear_all() - - assert await loader.load_many([1, 2, 3]) == [(1, 4), (2, 4), (3, 4)] - - -async def test_clear_nocache(): - batch_num = 0 - - async def idx(keys): - """Maps key => (key, batch_num)""" - nonlocal batch_num - batch_num += 1 - return [(key, batch_num) for key in keys] - - loader = DataLoader(load_fn=idx, cache=False) - - assert await loader.load_many([1, 2, 3]) == [(1, 1), (2, 1), (3, 1)] - - loader.clear(1) - - assert await loader.load_many([1, 2, 3]) == [(1, 2), (2, 2), (3, 2)] - - loader.clear_many([1, 2]) - - assert await loader.load_many([1, 2, 3]) == [(1, 3), (2, 3), (3, 3)] - - loader.clear_all() - - assert await loader.load_many([1, 2, 3]) == [(1, 4), (2, 4), (3, 4)] - - -async def test_dont_dispatch_cancelled(): - async def idx(keys): - await asyncio.sleep(0.2) - return keys - - loader = DataLoader(load_fn=idx) - - value_a = await loader.load(1) - # value_b will be cancelled by hand - value_b = cast(asyncio.Future, loader.load(2)) - value_b.cancel() - # value_c will be cancelled by the timeout - with pytest.raises(asyncio.TimeoutError): - value_c = cast(asyncio.Future, loader.load(3)) - await asyncio.wait_for(value_c, 0.1) - value_d = await loader.load(4) - - assert value_a == 1 - assert value_d == 4 - - # 2 can still be used here because a new future will be created for it - values = await loader.load_many([1, 2, 3, 4, 5, 6]) - assert values == [1, 2, 3, 4, 5, 6] - - with pytest.raises(asyncio.CancelledError): - value_b.result() - with pytest.raises(asyncio.CancelledError): - value_c.result() - - # Try single loading results again to make sure the cancelled - # futures are not being reused - value_a = await loader.load(1) - value_b = await loader.load(2) - value_c = await loader.load(3) - value_d = await loader.load(4) - - assert value_a == 1 - assert value_b == 2 - assert value_c == 3 - assert value_d == 4 - - -async def test_cache_override(): - async def idx(keys): - return keys - - class TestCache(AbstractCache): - def __init__(self): - self.cache = {} - - def get(self, key: int) -> int: - return self.cache.get(key) - - def set(self, key: int, value: int) -> None: - self.cache[key] = value - - def delete(self, key: int) -> None: - del self.cache[key] - - def clear(self) -> None: - self.cache.clear() - - custom_cache = TestCache() - loader = DataLoader(load_fn=idx, cache_map=custom_cache) - - await loader.load(1) - await loader.load(2) - await loader.load(3) - - assert len(custom_cache.cache) == 3 - assert await custom_cache.cache[1] == 1 - assert await custom_cache.cache[2] == 2 - assert await custom_cache.cache[3] == 3 - - loader.clear(1) - assert len(custom_cache.cache) == 2 - assert sorted(list(custom_cache.cache.keys())) == [2, 3] - - loader.clear_all() - assert len(custom_cache.cache) == 0 - assert list(custom_cache.cache.keys()) == [] - - await loader.load(1) - await loader.load(2) - await loader.load(3) - - loader.clear_many([1, 2]) - assert len(custom_cache.cache) == 1 - assert list(custom_cache.cache.keys()) == [3] - - data = await loader.load(3) - assert data == 3 - - loader.prime(3, 4) - assert await custom_cache.cache[3] == 3 - - loader.prime(3, 4, True) - assert await custom_cache.cache[3] == 4 - - with pytest.raises(TypeError): - await loader.load([1, 2, 3]) - - data = await loader.load((1, 2, 3)) - assert await custom_cache.get((1, 2, 3)) == data - - -async def test_custom_cache_key_fn(): - async def idx(keys): - return keys - - def custom_cache_key(key): - return ",".join(str(k) for k in key) - - loader = DataLoader(load_fn=idx, cache_key_fn=custom_cache_key) - data = await loader.load([1, 2, "test"]) - assert [1, 2, "test"] == data - - -async def test_user_class_custom_cache_key_fn(): - async def idx(keys): - return keys - - def custom_cache_key(key): - return key.id - - class CustomData: - def __init__(self, custom_id: int, name: str): - self.id: int = custom_id - self.name: str = name - - loader = DataLoader(load_fn=idx, cache_key_fn=custom_cache_key) - data1 = await loader.load(CustomData(1, "Nick")) - data2 = await loader.load(CustomData(1, "Nick")) - assert data1 == data2 - - data2 = await loader.load(CustomData(2, "Jane")) - assert data1 != data2 diff --git a/tests/test_printer/test_basic.py b/tests/test_printer/test_basic.py index 76da620198..82f7458ef2 100644 --- a/tests/test_printer/test_basic.py +++ b/tests/test_printer/test_basic.py @@ -3,10 +3,9 @@ from uuid import UUID import strawberry +from strawberry.arguments import UNSET from strawberry.printer import print_schema -from strawberry.scalars import JSON from strawberry.schema.config import StrawberryConfig -from strawberry.unset import UNSET def test_simple_required_types(): @@ -98,7 +97,6 @@ class MyInput: f: float id: strawberry.ID uid: UUID - s2: str = None # type: ignore - we do this for testing purposes @strawberry.type class Query: @@ -114,7 +112,6 @@ def search(self, input: MyInput) -> str: f: Float! id: ID! uid: UUID! - s2: String! } type Query { @@ -134,21 +131,8 @@ def test_input_defaults(): class MyInput: s: Optional[str] = None i: int = 0 - b: bool = False - f: float = 0.0 - f2: float = 0.1 - id: strawberry.ID = strawberry.ID("some_id") - id_number: strawberry.ID = strawberry.ID(123) # type: ignore - id_number_string: strawberry.ID = strawberry.ID("123") x: Optional[int] = UNSET - l: List[str] = strawberry.field(default_factory=list) # noqa: E741 - list_with_values: List[str] = strawberry.field( - default_factory=lambda: ["a", "b"] - ) - list_from_generator: List[str] = strawberry.field( - default_factory=lambda: (x for x in ["a", "b"]) - ) - list_from_string: List[str] = "ab" # type: ignore - we do this for testing purposes + l: List[str] = strawberry.field(default_factory=list) @strawberry.type class Query: @@ -160,17 +144,8 @@ def search(self, input: MyInput) -> int: input MyInput { s: String = null i: Int! = 0 - b: Boolean! = false - f: Float! = 0 - f2: Float! = 0.1 - id: ID! = "some_id" - idNumber: ID! = 123 - idNumberString: ID! = 123 x: Int l: [String!]! = [] - listWithValues: [String!]! = ["a", "b"] - listFromGenerator: [String!]! = ["a", "b"] - listFromString: [String!]! = "ab" } type Query { @@ -183,124 +158,6 @@ def search(self, input: MyInput) -> int: assert print_schema(schema) == textwrap.dedent(expected_type).strip() -def test_input_other_inputs(): - @strawberry.input - class Nested: - s: str - - @strawberry.input - class MyInput: - nested: Nested - nested2: Nested = strawberry.field(default_factory=lambda: Nested(s="a")) - nested3: Nested = strawberry.field(default_factory=lambda: {"s": "a"}) - nested4: Nested = "abc" # type: ignore - we do this for testing purposes - - @strawberry.type - class Query: - @strawberry.field - def search(self, input: MyInput) -> str: - return input.nested.s - - expected_type = """ - input MyInput { - nested: Nested! - nested2: Nested! = {s: "a"} - nested3: Nested! = {s: "a"} - nested4: Nested! - } - - input Nested { - s: String! - } - - type Query { - search(input: MyInput!): String! - } - """ - - schema = strawberry.Schema(query=Query) - - assert print_schema(schema) == textwrap.dedent(expected_type).strip() - - -def test_input_defaults_scalars(): - @strawberry.input - class MyInput: - j: JSON = strawberry.field(default_factory=dict) - j2: JSON = strawberry.field(default_factory=lambda: {"hello": "world"}) - j3: JSON = strawberry.field( - default_factory=lambda: {"hello": {"nice": "world"}} - ) - - @strawberry.type - class Query: - @strawberry.field - def search(self, input: MyInput) -> JSON: - return input.j - - expected_type = """ - \"\"\" - The `JSON` scalar type represents JSON values as specified by [ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf). - \"\"\" - scalar JSON @specifiedBy(url: "http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf") - - input MyInput { - j: JSON! = {} - j2: JSON! = {hello: "world"} - j3: JSON! = {hello: {nice: "world"}} - } - - type Query { - search(input: MyInput!): JSON! - } - """ - - schema = strawberry.Schema(query=Query) - - assert print_schema(schema) == textwrap.dedent(expected_type).strip() - - -def test_arguments_scalar(): - @strawberry.input - class MyInput: - j: JSON = strawberry.field(default_factory=dict) - j2: JSON = strawberry.field(default_factory=lambda: {"hello": "world"}) - j3: JSON = strawberry.field( - default_factory=lambda: {"hello": {"nice": "world"}} - ) - - @strawberry.type - class Query: - @strawberry.field - def search(self, j: JSON = {}) -> JSON: - return j - - @strawberry.field - def search2(self, j: JSON = {"hello": "world"}) -> JSON: - return j - - @strawberry.field - def search3(self, j: JSON = {"hello": {"nice": "world"}}) -> JSON: - return j - - expected_type = """ - \"\"\" - The `JSON` scalar type represents JSON values as specified by [ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf). - \"\"\" - scalar JSON @specifiedBy(url: "http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf") - - type Query { - search(j: JSON! = {}): JSON! - search2(j: JSON! = {hello: "world"}): JSON! - search3(j: JSON! = {hello: {nice: "world"}}): JSON! - } - """ - - schema = strawberry.Schema(query=Query) - - assert print_schema(schema) == textwrap.dedent(expected_type).strip() - - def test_interface(): @strawberry.interface class Node: @@ -332,43 +189,3 @@ class Query: schema = strawberry.Schema(query=Query) assert print_schema(schema) == textwrap.dedent(expected_type).strip() - - -def test_root_objects_with_different_names(): - @strawberry.type - class Domanda: - name: str - - @strawberry.type - class Mutazione: - name: str - - @strawberry.type - class Abbonamento: - name: str - - expected_type = """ - schema { - query: Domanda - mutation: Mutazione - subscription: Abbonamento - } - - type Abbonamento { - name: String! - } - - type Domanda { - name: String! - } - - type Mutazione { - name: String! - } - """ - - schema = strawberry.Schema( - query=Domanda, mutation=Mutazione, subscription=Abbonamento - ) - - assert print_schema(schema) == textwrap.dedent(expected_type).strip() diff --git a/tests/test_printer/test_schema_directives.py b/tests/test_printer/test_schema_directives.py index 672836ce9c..ad2ecf660c 100644 --- a/tests/test_printer/test_schema_directives.py +++ b/tests/test_printer/test_schema_directives.py @@ -1,13 +1,8 @@ import textwrap -from enum import Enum -from typing import List, Optional -from typing_extensions import Annotated import strawberry from strawberry.printer import print_schema -from strawberry.schema.config import StrawberryConfig from strawberry.schema_directive import Location -from strawberry.unset import UNSET def test_print_simple_directive(): @@ -19,9 +14,7 @@ class Sensitive: class Query: first_name: str = strawberry.field(directives=[Sensitive(reason="GDPR")]) - expected_output = """ - directive @sensitive(reason: String!) on FIELD_DEFINITION - + expected_type = """ type Query { firstName: String! @sensitive(reason: "GDPR") } @@ -29,610 +22,24 @@ class Query: schema = strawberry.Schema(query=Query) - assert print_schema(schema) == textwrap.dedent(expected_output).strip() + assert print_schema(schema) == textwrap.dedent(expected_type).strip() def test_print_directive_with_name(): - @strawberry.schema_directive( - name="sensitive", locations=[Location.FIELD_DEFINITION] - ) - class SensitiveDirective: - reason: str - - @strawberry.type - class Query: - first_name: str = strawberry.field( - directives=[SensitiveDirective(reason="GDPR")] - ) - - expected_output = """ - directive @sensitive(reason: String!) on FIELD_DEFINITION - - type Query { - firstName: String! @sensitive(reason: "GDPR") - } - """ - - schema = strawberry.Schema(query=Query) - - assert print_schema(schema) == textwrap.dedent(expected_output).strip() - - -def test_directive_on_types(): - @strawberry.input - class SensitiveValue: - key: str - value: str - - @strawberry.schema_directive(locations=[Location.OBJECT, Location.FIELD_DEFINITION]) - class SensitiveData: - reason: str - meta: Optional[List[SensitiveValue]] = UNSET - - @strawberry.schema_directive(locations=[Location.INPUT_OBJECT]) - class SensitiveInput: - reason: str - meta: Optional[List[SensitiveValue]] = UNSET - - @strawberry.schema_directive(locations=[Location.INPUT_FIELD_DEFINITION]) - class RangeInput: - min: int - max: int - - @strawberry.input(directives=[SensitiveInput(reason="GDPR")]) - class Input: - first_name: str - age: int = strawberry.field(directives=[RangeInput(min=1, max=100)]) - - @strawberry.type(directives=[SensitiveData(reason="GDPR")]) - class User: - first_name: str - age: int - phone: str = strawberry.field( - directives=[ - SensitiveData( - reason="PRIVATE", - meta=[ - SensitiveValue( - key="can_share_field", value="phone_share_accepted" - ) - ], - ) - ] - ) - phone_share_accepted: bool - - @strawberry.type - class Query: - @strawberry.field - def user(self, input: Input) -> User: - return User( - first_name=input.first_name, - age=input.age, - phone="+551191551234", - phone_share_accepted=False, - ) - - expected_output = """ - directive @rangeInput(min: Int!, max: Int!) on INPUT_FIELD_DEFINITION - - directive @sensitiveData(reason: String!, meta: [SensitiveValue!]) on OBJECT | FIELD_DEFINITION - - directive @sensitiveInput(reason: String!, meta: [SensitiveValue!]) on INPUT_OBJECT - - input Input @sensitiveInput(reason: "GDPR") { - firstName: String! - age: Int! @rangeInput(min: 1, max: 100) - } - - type Query { - user(input: Input!): User! - } - - type User @sensitiveData(reason: "GDPR") { - firstName: String! - age: Int! - phone: String! @sensitiveData(reason: "PRIVATE", meta: [{key: "can_share_field", value: "phone_share_accepted"}]) - phoneShareAccepted: Boolean! - } - - input SensitiveValue { - key: String! - value: String! - } - """ # noqa:E501 - - schema = strawberry.Schema(query=Query) - - assert print_schema(schema) == textwrap.dedent(expected_output).strip() - - -def test_using_different_names_for_directive_field(): - @strawberry.schema_directive(locations=[Location.FIELD_DEFINITION]) - class Sensitive: - reason: str = strawberry.directive_field(name="as") - real_age: str - real_age_2: str = strawberry.directive_field(name="real_age") - - @strawberry.type - class Query: - first_name: str = strawberry.field( - directives=[Sensitive(reason="GDPR", real_age="1", real_age_2="2")] - ) - - expected_output = """ - directive @sensitive(as: String!, realAge: String!, real_age: String!) on FIELD_DEFINITION - - type Query { - firstName: String! @sensitive(as: "GDPR", realAge: "1", real_age: "2") - } - """ # noqa:E501 - - schema = strawberry.Schema(query=Query) - - assert print_schema(schema) == textwrap.dedent(expected_output).strip() - - -def test_respects_schema_config_for_names(): - @strawberry.schema_directive(locations=[Location.FIELD_DEFINITION]) - class Sensitive: - real_age: str - - @strawberry.type - class Query: - first_name: str = strawberry.field(directives=[Sensitive(real_age="42")]) - - expected_output = """ - directive @Sensitive(real_age: String!) on FIELD_DEFINITION - - type Query { - first_name: String! @Sensitive(real_age: "42") - } - """ - - schema = strawberry.Schema( - query=Query, config=StrawberryConfig(auto_camel_case=False) - ) - - assert print_schema(schema) == textwrap.dedent(expected_output).strip() - - -def test_respects_schema_parameter_types_for_arguments_int(): @strawberry.schema_directive(locations=[Location.FIELD_DEFINITION]) - class Sensitive: - real_age: int - - @strawberry.type - class Query: - first_name: str = strawberry.field(directives=[Sensitive(real_age=42)]) - - expected_output = """ - directive @Sensitive(real_age: Int!) on FIELD_DEFINITION - - type Query { - first_name: String! @Sensitive(real_age: 42) - } - """ - - schema = strawberry.Schema( - query=Query, config=StrawberryConfig(auto_camel_case=False) - ) - - assert print_schema(schema) == textwrap.dedent(expected_output).strip() - - -def test_respects_schema_parameter_types_for_arguments_list_of_ints(): - @strawberry.schema_directive(locations=[Location.FIELD_DEFINITION]) - class Sensitive: - real_age: List[int] - - @strawberry.type - class Query: - first_name: str = strawberry.field(directives=[Sensitive(real_age=[42])]) - - expected_output = """ - directive @Sensitive(real_age: [Int!]!) on FIELD_DEFINITION - - type Query { - first_name: String! @Sensitive(real_age: [42]) - } - """ - - schema = strawberry.Schema( - query=Query, config=StrawberryConfig(auto_camel_case=False) - ) - - assert print_schema(schema) == textwrap.dedent(expected_output).strip() - - -def test_respects_schema_parameter_types_for_arguments_list_of_strings(): - @strawberry.schema_directive(locations=[Location.FIELD_DEFINITION]) - class Sensitive: - real_age: List[str] - - @strawberry.type - class Query: - first_name: str = strawberry.field(directives=[Sensitive(real_age=["42"])]) - - expected_output = """ - directive @Sensitive(real_age: [String!]!) on FIELD_DEFINITION - - type Query { - first_name: String! @Sensitive(real_age: ["42"]) - } - """ - - schema = strawberry.Schema( - query=Query, config=StrawberryConfig(auto_camel_case=False) - ) - - assert print_schema(schema) == textwrap.dedent(expected_output).strip() - - -def test_prints_directive_on_schema(): - @strawberry.schema_directive(locations=[Location.SCHEMA]) - class Tag: - name: str - - @strawberry.type - class Query: - first_name: str = strawberry.field(directives=[Tag(name="team-1")]) - - schema = strawberry.Schema(query=Query, schema_directives=[Tag(name="team-1")]) - - expected_output = """ - directive @tag(name: String!) on SCHEMA - - schema @tag(name: "team-1") { - query: Query - } - - type Query { - firstName: String! - } - """ - - assert print_schema(schema) == textwrap.dedent(expected_output).strip() - - -def test_prints_multiple_directives_on_schema(): - @strawberry.schema_directive(locations=[Location.SCHEMA]) - class Tag: - name: str - - @strawberry.type - class Query: - first_name: str - - schema = strawberry.Schema( - query=Query, schema_directives=[Tag(name="team-1"), Tag(name="team-2")] - ) - - expected_output = """ - directive @tag(name: String!) on SCHEMA - - schema @tag(name: "team-1") @tag(name: "team-2") { - query: Query - } - - type Query { - firstName: String! - } - """ - - assert print_schema(schema) == textwrap.dedent(expected_output).strip() - - -def test_prints_with_types(): - @strawberry.input - class SensitiveConfiguration: + class SensitiveField: reason: str - @strawberry.schema_directive(locations=[Location.FIELD_DEFINITION]) - class Sensitive: - config: SensitiveConfiguration - @strawberry.type class Query: - first_name: str = strawberry.field( - directives=[Sensitive(config=SensitiveConfiguration(reason="example"))] - ) - - expected_output = """ - directive @sensitive(config: SensitiveConfiguration!) on FIELD_DEFINITION - - type Query { - firstName: String! @sensitive(config: {reason: "example"}) - } - - input SensitiveConfiguration { - reason: String! - } - """ - - schema = strawberry.Schema(query=Query) - - assert print_schema(schema) == textwrap.dedent(expected_output).strip() - - -def test_prints_with_scalar(): - SensitiveConfiguration = strawberry.scalar(str, name="SensitiveConfiguration") - - @strawberry.schema_directive(locations=[Location.FIELD_DEFINITION]) - class Sensitive: - config: SensitiveConfiguration - - @strawberry.type - class Query: - first_name: str = strawberry.field(directives=[Sensitive(config="Some config")]) - - expected_output = """ - directive @sensitive(config: SensitiveConfiguration!) on FIELD_DEFINITION - - type Query { - firstName: String! @sensitive(config: "Some config") - } - - scalar SensitiveConfiguration - """ - - schema = strawberry.Schema(query=Query) - - assert print_schema(schema) == textwrap.dedent(expected_output).strip() - - -def test_prints_with_enum(): - @strawberry.enum - class Reason(str, Enum): - EXAMPLE = "example" - - @strawberry.schema_directive(locations=[Location.FIELD_DEFINITION]) - class Sensitive: - reason: Reason - - @strawberry.type - class Query: - first_name: str = strawberry.field( - directives=[Sensitive(reason=Reason.EXAMPLE)] - ) - - expected_output = """ - directive @sensitive(reason: Reason!) on FIELD_DEFINITION - - type Query { - firstName: String! @sensitive(reason: EXAMPLE) - } - - enum Reason { - EXAMPLE - } - """ - - schema = strawberry.Schema(query=Query) - - assert print_schema(schema) == textwrap.dedent(expected_output).strip() - - -def test_does_not_print_definition(): - @strawberry.schema_directive( - locations=[Location.FIELD_DEFINITION], print_definition=False - ) - class Sensitive: - reason: str - - @strawberry.type - class Query: - first_name: str = strawberry.field(directives=[Sensitive(reason="GDPR")]) - - expected_output = """ - type Query { - firstName: String! @sensitive(reason: "GDPR") - } - """ - - schema = strawberry.Schema(query=Query) - - assert print_schema(schema) == textwrap.dedent(expected_output).strip() - - -def test_print_directive_on_scalar(): - @strawberry.schema_directive(locations=[Location.SCALAR]) - class Sensitive: - reason: str - - SensitiveString = strawberry.scalar( - str, name="SensitiveString", directives=[Sensitive(reason="example")] - ) - - @strawberry.type - class Query: - first_name: SensitiveString - - expected_output = """ - directive @sensitive(reason: String!) on SCALAR - - type Query { - firstName: SensitiveString! - } - - scalar SensitiveString @sensitive(reason: "example") - """ - - schema = strawberry.Schema(query=Query) - - assert print_schema(schema) == textwrap.dedent(expected_output).strip() - - -def test_print_directive_on_enum(): - @strawberry.schema_directive(locations=[Location.ENUM]) - class Sensitive: - reason: str - - @strawberry.enum(directives=[Sensitive(reason="example")]) - class SomeEnum(str, Enum): - EXAMPLE = "example" - - @strawberry.type - class Query: - first_name: SomeEnum - - expected_output = """ - directive @sensitive(reason: String!) on ENUM - - type Query { - firstName: SomeEnum! - } - - enum SomeEnum @sensitive(reason: "example") { - EXAMPLE - } - """ - - schema = strawberry.Schema(query=Query) - - assert print_schema(schema) == textwrap.dedent(expected_output).strip() - - -def test_print_directive_on_enum_value(): - @strawberry.schema_directive(locations=[Location.ENUM_VALUE]) - class Sensitive: - reason: str - - @strawberry.enum - class SomeEnum(Enum): - EXAMPLE = strawberry.enum_value( - "example", directives=[Sensitive(reason="example")] - ) - - @strawberry.type - class Query: - first_name: SomeEnum - - expected_output = """ - directive @sensitive(reason: String!) on ENUM_VALUE - - type Query { - firstName: SomeEnum! - } - - enum SomeEnum { - EXAMPLE @sensitive(reason: "example") - } - """ - - schema = strawberry.Schema(query=Query) - - assert print_schema(schema) == textwrap.dedent(expected_output).strip() - - -def test_print_directive_on_union(): - @strawberry.type - class A: - a: int - - @strawberry.type - class B: - b: int - - @strawberry.schema_directive(locations=[Location.SCALAR]) - class Sensitive: - reason: str - - Union = strawberry.union("Union", (A, B), directives=[Sensitive(reason="example")]) - - @strawberry.type - class Query: - example: Union - - expected_output = """ - directive @sensitive(reason: String!) on SCALAR - - type A { - a: Int! - } - - type B { - b: Int! - } - - type Query { - example: Union! - } - - union Union @sensitive(reason: "example") = A | B - """ - - schema = strawberry.Schema(query=Query) - - assert print_schema(schema) == textwrap.dedent(expected_output).strip() - - -def test_print_directive_on_argument(): - @strawberry.schema_directive(locations=[Location.ARGUMENT_DEFINITION]) - class Sensitive: - reason: str - - @strawberry.type - class Query: - @strawberry.field - def hello( - self, - name: Annotated[ - str, strawberry.argument(directives=[Sensitive(reason="example")]) - ], - age: Annotated[ - str, strawberry.argument(directives=[Sensitive(reason="example")]) - ], - ) -> str: - return f"Hello {name} of {age}" - - expected_output = """ - directive @sensitive(reason: String!) on ARGUMENT_DEFINITION - - type Query { - hello(name: String! @sensitive(reason: "example"), age: String! @sensitive(reason: "example")): String! - } - """ - - schema = strawberry.Schema(query=Query) - - assert print_schema(schema) == textwrap.dedent(expected_output).strip() - - -def test_print_directive_on_argument_with_description(): - @strawberry.schema_directive(locations=[Location.ARGUMENT_DEFINITION]) - class Sensitive: - reason: str - - @strawberry.type - class Query: - @strawberry.field - def hello( - self, - name: Annotated[ - str, - strawberry.argument( - description="Name", directives=[Sensitive(reason="example")] - ), - ], - age: Annotated[ - str, strawberry.argument(directives=[Sensitive(reason="example")]) - ], - ) -> str: - return f"Hello {name} of {age}" - - expected_output = """ - directive @sensitive(reason: String!) on ARGUMENT_DEFINITION + first_name: str = strawberry.field(directives=[SensitiveField(reason="GDPR")]) + expected_type = """ type Query { - hello( - \"\"\"Name\"\"\" - name: String! @sensitive(reason: "example") - age: String! @sensitive(reason: "example") - ): String! + firstName: String! @sensitiveField(reason: "GDPR") } """ schema = strawberry.Schema(query=Query) - assert print_schema(schema) == textwrap.dedent(expected_output).strip() + assert print_schema(schema) == textwrap.dedent(expected_type).strip() diff --git a/tests/test_repr.py b/tests/test_repr.py index dfb12f9b74..f5c015d51c 100644 --- a/tests/test_repr.py +++ b/tests/test_repr.py @@ -13,7 +13,7 @@ class MyType: id: strawberry.ID assert ( - repr(MyType(s="a", i=1, b=True, f=3.2, id="123")) + repr(MyType("a", 1, True, 3.2, "123")) == "test_repr_type..MyType(s='a', i=1, b=True, f=3.2, id='123')" ) diff --git a/tests/types/cross_module_resolvers/README.md b/tests/types/cross_module_resolvers/README.md index 4d25737fb3..9a084e92fe 100644 --- a/tests/types/cross_module_resolvers/README.md +++ b/tests/types/cross_module_resolvers/README.md @@ -12,11 +12,9 @@ a resolver from a different module: ```python from other_module import generic_resolver - @strawberry.field class Foo: - bar: "Bar" = strawberry.field(resolver=generic_resolver) - + bar: 'Bar' = strawberry.field(resolver=generic_resolver) @strawberry.field class Bar: diff --git a/tests/types/cross_module_resolvers/test_cross_module_resolvers.py b/tests/types/cross_module_resolvers/test_cross_module_resolvers.py index 0bd8b0cb56..e1866bf9cb 100644 --- a/tests/types/cross_module_resolvers/test_cross_module_resolvers.py +++ b/tests/types/cross_module_resolvers/test_cross_module_resolvers.py @@ -113,9 +113,7 @@ class Query: def test_c_composition_resolver(): @strawberry.type class Query: - c: List[c_mod.CComposition] = strawberry.field( - resolver=c_mod.c_composition_resolver - ) + c: c_mod.CComposition = strawberry.field(resolver=c_mod.c_composition_resolver) [field] = Query._type_definition.fields assert field.type == List[c_mod.CComposition] diff --git a/tests/types/resolving/test_lists.py b/tests/types/resolving/test_lists.py index b8014fd5ac..6d0eb27b04 100644 --- a/tests/types/resolving/test_lists.py +++ b/tests/types/resolving/test_lists.py @@ -1,6 +1,5 @@ import sys -from collections.abc import Sequence -from typing import List, Optional, Tuple, Union +from typing import List, Optional, Union import pytest @@ -20,32 +19,6 @@ def test_basic_list(): assert resolved == List[str] -def test_basic_tuple(): - annotation = StrawberryAnnotation(Tuple[str]) - resolved = annotation.resolve() - - assert isinstance(resolved, StrawberryList) - assert resolved.of_type is str - - assert resolved == StrawberryList(of_type=str) - assert resolved == Tuple[str] - - -@pytest.mark.skipif( - sys.version_info < (3, 9), - reason="collections.abc.Sequence supporting [] was added in python 3.9", -) -def test_basic_sequence(): - annotation = StrawberryAnnotation(Sequence[str]) - resolved = annotation.resolve() - - assert isinstance(resolved, StrawberryList) - assert resolved.of_type is str - - assert resolved == StrawberryList(of_type=str) - assert resolved == Sequence[str] - - def test_list_of_optional(): annotation = StrawberryAnnotation(List[Optional[int]]) resolved = annotation.resolve() @@ -57,32 +30,6 @@ def test_list_of_optional(): assert resolved == List[Optional[int]] -@pytest.mark.skipif( - sys.version_info < (3, 9), - reason="collections.abc.Sequence supporting [] was added in python 3.9", -) -def test_sequence_of_optional(): - annotation = StrawberryAnnotation(Sequence[Optional[int]]) - resolved = annotation.resolve() - - assert isinstance(resolved, StrawberryList) - assert resolved.of_type == Optional[int] - - assert resolved == StrawberryList(of_type=Optional[int]) - assert resolved == Sequence[Optional[int]] - - -def test_tuple_of_optional(): - annotation = StrawberryAnnotation(Tuple[Optional[int]]) - resolved = annotation.resolve() - - assert isinstance(resolved, StrawberryList) - assert resolved.of_type == Optional[int] - - assert resolved == StrawberryList(of_type=Optional[int]) - assert resolved == Tuple[Optional[int]] - - def test_list_of_lists(): annotation = StrawberryAnnotation(List[List[float]]) resolved = annotation.resolve() @@ -94,32 +41,6 @@ def test_list_of_lists(): assert resolved == List[List[float]] -@pytest.mark.skipif( - sys.version_info < (3, 9), - reason="collections.abc.Sequence supporting [] was added in python 3.9", -) -def test_sequence_of_sequence(): - annotation = StrawberryAnnotation(Sequence[Sequence[float]]) - resolved = annotation.resolve() - - assert isinstance(resolved, StrawberryList) - assert resolved.of_type == Sequence[float] - - assert resolved == StrawberryList(of_type=Sequence[float]) - assert resolved == Sequence[Sequence[float]] - - -def test_tuple_of_tuple(): - annotation = StrawberryAnnotation(Tuple[Tuple[float]]) - resolved = annotation.resolve() - - assert isinstance(resolved, StrawberryList) - assert resolved.of_type == Tuple[float] - - assert resolved == StrawberryList(of_type=Tuple[float]) - assert resolved == Tuple[Tuple[float]] - - def test_list_of_union(): @strawberry.type class Animal: @@ -139,29 +60,6 @@ class Fungus: assert resolved == List[Union[Animal, Fungus]] -@pytest.mark.skipif( - sys.version_info < (3, 9), - reason="collections.abc.Sequence supporting [] was added in python 3.9", -) -def test_sequence_of_union(): - @strawberry.type - class Animal: - feet: bool - - @strawberry.type - class Fungus: - spore: bool - - annotation = StrawberryAnnotation(Sequence[Union[Animal, Fungus]]) - resolved = annotation.resolve() - - assert isinstance(resolved, StrawberryList) - assert resolved.of_type == Union[Animal, Fungus] - - assert resolved == StrawberryList(of_type=Union[Animal, Fungus]) - assert resolved == Sequence[Union[Animal, Fungus]] - - @pytest.mark.skipif( sys.version_info < (3, 9), reason="built-in generic annotations where added in python 3.9", @@ -176,19 +74,3 @@ def test_list_builtin(): assert resolved == StrawberryList(of_type=str) assert resolved == List[str] assert resolved == list[str] - - -@pytest.mark.skipif( - sys.version_info < (3, 9), - reason="built-in generic annotations where added in python 3.9", -) -def test_tuple_builtin(): - annotation = StrawberryAnnotation(tuple[str]) - resolved = annotation.resolve() - - assert isinstance(resolved, StrawberryList) - assert resolved.of_type is str - - assert resolved == StrawberryList(of_type=str) - assert resolved == Tuple[str] - assert resolved == tuple[str] diff --git a/tests/types/resolving/test_optionals.py b/tests/types/resolving/test_optionals.py index d9ed2f75d0..429cefc27f 100644 --- a/tests/types/resolving/test_optionals.py +++ b/tests/types/resolving/test_optionals.py @@ -3,7 +3,7 @@ import strawberry from strawberry.annotation import StrawberryAnnotation from strawberry.type import StrawberryOptional -from strawberry.unset import UnsetType +from strawberry.unset import _Unset def test_basic_optional(): @@ -18,7 +18,7 @@ def test_basic_optional(): def test_optional_with_unset(): - annotation = StrawberryAnnotation(Union[UnsetType, Optional[str]]) + annotation = StrawberryAnnotation(Union[_Unset, Optional[str]]) resolved = annotation.resolve() assert isinstance(resolved, StrawberryOptional) @@ -29,7 +29,7 @@ def test_optional_with_unset(): def test_optional_with_unset_as_union(): - annotation = StrawberryAnnotation(Union[UnsetType, None, str]) + annotation = StrawberryAnnotation(Union[_Unset, None, str]) resolved = annotation.resolve() assert isinstance(resolved, StrawberryOptional) @@ -40,7 +40,7 @@ def test_optional_with_unset_as_union(): def test_optional_union_containing_a_real_union_and_unset(): - annotation = StrawberryAnnotation(Union[str, int, None, UnsetType]) + annotation = StrawberryAnnotation(Union[str, int, None, _Unset]) resolved = annotation.resolve() assert isinstance(resolved, StrawberryOptional) diff --git a/tests/types/resolving/test_parse_annotated.py b/tests/types/resolving/test_parse_annotated.py deleted file mode 100644 index 16ff8d5c99..0000000000 --- a/tests/types/resolving/test_parse_annotated.py +++ /dev/null @@ -1,65 +0,0 @@ -from typing import List, Optional, Union -from typing_extensions import Annotated - -from strawberry.annotation import StrawberryAnnotation - - -def test_parse_annotated(): - assert StrawberryAnnotation.parse_annotated(str) == str - assert StrawberryAnnotation.parse_annotated(Annotated[str, "foo"]) == str - - -def test_parse_annotated_optional(): - assert StrawberryAnnotation.parse_annotated(Optional[str]) == Optional[str] - assert ( - StrawberryAnnotation.parse_annotated(Annotated[Optional[str], "foo"]) - == Optional[str] - ) - - -def test_parse_annotated_list(): - assert StrawberryAnnotation.parse_annotated(List[str]) == List[str] - assert ( - StrawberryAnnotation.parse_annotated(Annotated[List[str], "foo"]) == List[str] - ) - - -def test_parse_annotated_union(): - assert StrawberryAnnotation.parse_annotated(Union[str, int]) == Union[str, int] - assert ( - StrawberryAnnotation.parse_annotated(Annotated[Union[str, int], "foo"]) - == Union[str, int] - ) - - -def test_parse_annotated_optional_union(): - assert ( - StrawberryAnnotation.parse_annotated(Optional[Union[str, int]]) - == Optional[Union[str, int]] - ) - assert ( - StrawberryAnnotation.parse_annotated( - Annotated[Optional[Union[str, int]], "foo"] - ) - == Optional[Union[str, int]] - ) - - -def test_parse_annotated_list_union(): - assert ( - StrawberryAnnotation.parse_annotated(List[Union[str, int]]) - == List[Union[str, int]] - ) - assert ( - StrawberryAnnotation.parse_annotated(Annotated[List[Union[str, int]], "foo"]) - == List[Union[str, int]] - ) - - -def test_parse_annotated_recursive(): - assert ( - StrawberryAnnotation.parse_annotated( - Annotated[List[Annotated[Union[str, int], "bar"]], "foo"] - ) - == List[Union[str, int]] - ) diff --git a/tests/types/resolving/test_union_pipe.py b/tests/types/resolving/test_union_pipe.py index 2f22674916..246696b6c9 100644 --- a/tests/types/resolving/test_union_pipe.py +++ b/tests/types/resolving/test_union_pipe.py @@ -5,12 +5,11 @@ import strawberry from strawberry.annotation import StrawberryAnnotation -from strawberry.exceptions import InvalidTypeForUnionMergeError -from strawberry.exceptions.invalid_union_type import InvalidUnionTypeError -from strawberry.schema.types.base_scalars import Date, DateTime +from strawberry.exceptions import InvalidUnionType from strawberry.type import StrawberryOptional from strawberry.union import StrawberryUnion + pytestmark = pytest.mark.skipif( sys.version_info < (3, 10), reason="pipe syntax for union is only available on python 3.10+", @@ -79,10 +78,6 @@ class Error: ) -@pytest.mark.raises_strawberry_exception( - InvalidTypeForUnionMergeError, - match="`int` cannot be used when merging GraphQL Unions", -) def test_raises_error_when_piping_with_scalar(): @strawberry.type class User: @@ -94,12 +89,5 @@ class Error: UserOrError = strawberry.union("UserOrError", (User, Error)) - StrawberryAnnotation(UserOrError | int) - - -@pytest.mark.raises_strawberry_exception( - InvalidUnionTypeError, - match="Type `date` cannot be used in a GraphQL Union", -) -def test_raises_error_when_piping_with_custom_scalar(): - StrawberryAnnotation(Date | DateTime) + with pytest.raises(InvalidUnionType): + StrawberryAnnotation(UserOrError | int) diff --git a/tests/types/resolving/test_unions.py b/tests/types/resolving/test_unions.py index d200eef34c..fe3ebf6cec 100644 --- a/tests/types/resolving/test_unions.py +++ b/tests/types/resolving/test_unions.py @@ -1,12 +1,12 @@ import sys from dataclasses import dataclass -from typing import Generic, NewType, TypeVar, Union +from typing import Generic, TypeVar, Union import pytest import strawberry from strawberry.annotation import StrawberryAnnotation -from strawberry.exceptions import InvalidUnionTypeError +from strawberry.exceptions import InvalidUnionType from strawberry.union import StrawberryUnion, union @@ -131,7 +131,7 @@ class B: Result = strawberry.union("Result", (A, B)) with pytest.raises(ValueError, match=r"Cannot use union type directly"): - Result() # type: ignore + Result() def test_error_with_empty_type_list(): @@ -139,40 +139,19 @@ def test_error_with_empty_type_list(): strawberry.union("Result", ()) -@pytest.mark.raises_strawberry_exception( - InvalidUnionTypeError, match="Type `int` cannot be used in a GraphQL Union" -) def test_error_with_scalar_types(): - strawberry.union( - "Result", - ( - int, - str, - float, - bool, - ), - ) - - -@pytest.mark.raises_strawberry_exception( - InvalidUnionTypeError, match="Type `CustomScalar` cannot be used in a GraphQL Union" -) -def test_error_with_custom_scalar_types(): - CustomScalar = strawberry.scalar( - NewType("CustomScalar", str), - serialize=lambda v: str(v), - parse_value=lambda v: str(v), - ) + with pytest.raises( + InvalidUnionType, match="Type `int` cannot be used in a GraphQL Union" + ): + strawberry.union("Result", (int,)) - strawberry.union("Result", (CustomScalar,)) - -@pytest.mark.raises_strawberry_exception( - InvalidUnionTypeError, match="Type `A` cannot be used in a GraphQL Union" -) def test_error_with_non_strawberry_type(): @dataclass class A: a: int - strawberry.union("Result", (A,)) + with pytest.raises( + InvalidUnionType, match="Type `A` cannot be used in a GraphQL Union" + ): + strawberry.union("Result", (A,)) diff --git a/tests/types/test_argument_types.py b/tests/types/test_argument_types.py index 2db47cf5c3..431a165452 100644 --- a/tests/types/test_argument_types.py +++ b/tests/types/test_argument_types.py @@ -1,11 +1,7 @@ -import warnings from enum import Enum -from typing import Any, List, Optional, TypeVar - -import pytest +from typing import List, Optional, TypeVar import strawberry -from strawberry.types.info import Info def test_enum(): @@ -97,52 +93,3 @@ def set_value(value: T) -> bool: argument = set_value.arguments[0] assert argument.type == T - - -ContextType = TypeVar("ContextType") -RootValueType = TypeVar("RootValueType") - - -class CustomInfo(Info[ContextType, RootValueType]): - """Subclassed Info type used to test dependency injection.""" - - -@pytest.mark.parametrize( - "annotation", - [CustomInfo, CustomInfo[Any, Any], Info, Info[Any, Any]], -) -def test_custom_info(annotation): - """Test to ensure that subclassed Info does not raise warning.""" - with warnings.catch_warnings(): - warnings.filterwarnings("error") - - def get_info(info) -> bool: - _ = info - return True - - get_info.__annotations__["info"] = annotation - get_info_field = strawberry.field(get_info) - - assert not get_info_field.arguments # Should have no arguments matched - - info_parameter = get_info_field.base_resolver.info_parameter - assert info_parameter is not None - assert info_parameter.name == "info" - - -def test_custom_info_negative(): - """Test to ensure deprecation warning is emitted.""" - with pytest.warns( - DeprecationWarning, match=r"Argument name-based matching of 'info'" - ): - - @strawberry.field - def get_info(info) -> bool: - _ = info - return True - - assert not get_info.arguments # Should have no arguments matched - - info_parameter = get_info.base_resolver.info_parameter - assert info_parameter is not None - assert info_parameter.name == "info" diff --git a/tests/types/test_convert_to_dictionary.py b/tests/types/test_convert_to_dictionary.py deleted file mode 100644 index 4281e30712..0000000000 --- a/tests/types/test_convert_to_dictionary.py +++ /dev/null @@ -1,65 +0,0 @@ -from enum import Enum -from typing import List, Optional - -import strawberry -from strawberry import asdict - - -def test_convert_simple_type_to_dictionary(): - @strawberry.type - class People: - name: str - age: int - - lorem = People(name="Alex", age=30) - - assert asdict(lorem) == { - "name": "Alex", - "age": 30, - } - - -def test_convert_complex_type_to_dictionary(): - @strawberry.enum - class Count(Enum): - TWO = "two" - FOUR = "four" - - @strawberry.type - class Animal: - legs: Count - - @strawberry.type - class People: - name: str - animals: List[Animal] - - lorem = People( - name="Kevin", animals=[Animal(legs=Count.TWO), Animal(legs=Count.FOUR)] - ) - - assert asdict(lorem) == { - "name": "Kevin", - "animals": [ - {"legs": Count.TWO}, - {"legs": Count.FOUR}, - ], - } - - -def test_convert_input_to_dictionary(): - @strawberry.input - class QnaInput: - title: str - description: str - tags: Optional[List[str]] = strawberry.field(default=None) - - title = "Where is the capital of United Kingdom?" - description = "London is the capital of United Kingdom." - qna = QnaInput(title=title, description=description) - - assert asdict(qna) == { - "title": title, - "description": description, - "tags": None, - } diff --git a/tests/types/test_deferred_annotations.py b/tests/types/test_deferred_annotations.py index 73abf3bb2e..708c7e847e 100644 --- a/tests/types/test_deferred_annotations.py +++ b/tests/types/test_deferred_annotations.py @@ -3,6 +3,7 @@ import strawberry + deferred_module_source = """ from __future__ import annotations diff --git a/tests/types/test_execution.py b/tests/types/test_execution.py index 559c0f5a22..9746b4e4f1 100644 --- a/tests/types/test_execution.py +++ b/tests/types/test_execution.py @@ -23,7 +23,7 @@ def on_request_end(self): execution_context = self.execution_context operation_name = execution_context.operation_name - operation_type = execution_context.operation_type.value + operation_type = execution_context.operation_type schema = strawberry.Schema(Query, extensions=[MyExtension]) @@ -31,14 +31,14 @@ def on_request_end(self): assert not result.errors assert operation_name is None - assert operation_type == "query" + assert operation_type == "QUERY" # Try again with an operation_name result = schema.execute_sync("query MyOperation { ping }") assert not result.errors assert operation_name == "MyOperation" - assert operation_type == "query" + assert operation_type == "QUERY" # Try again with an operation_name override result = schema.execute_sync( @@ -51,7 +51,7 @@ def on_request_end(self): assert not result.errors assert operation_name == "MyOperation2" - assert operation_type == "query" + assert operation_type == "QUERY" def test_execution_context_operation_type_mutation(): @@ -66,7 +66,7 @@ def on_request_end(self): execution_context = self.execution_context operation_name = execution_context.operation_name - operation_type = execution_context.operation_type.value + operation_type = execution_context.operation_type @strawberry.type class Mutation: @@ -80,14 +80,14 @@ def my_mutation(self) -> str: assert not result.errors assert operation_name is None - assert operation_type == "mutation" + assert operation_type == "MUTATION" # Try again with an operation_name result = schema.execute_sync("mutation MyMutation { myMutation }") assert not result.errors assert operation_name == "MyMutation" - assert operation_type == "mutation" + assert operation_type == "MUTATION" # Try again with an operation_name override result = schema.execute_sync( @@ -100,10 +100,10 @@ def my_mutation(self) -> str: assert not result.errors assert operation_name == "MyMutation2" - assert operation_type == "mutation" + assert operation_type == "MUTATION" -def test_execution_context_operation_name_and_type_with_fragments(): +def test_execution_context_operation_name_and_type_with_fragmenets(): operation_name = None operation_type = None @@ -115,7 +115,7 @@ def on_request_end(self): execution_context = self.execution_context operation_name = execution_context.operation_name - operation_type = execution_context.operation_type.value + operation_type = execution_context.operation_type schema = strawberry.Schema(Query, extensions=[MyExtension]) @@ -134,7 +134,7 @@ def on_request_end(self): assert not result.errors assert operation_name == "MyOperation" - assert operation_type == "query" + assert operation_type == "QUERY" def test_error_when_accessing_operation_type_before_parsing(): diff --git a/tests/types/test_lazy_types.py b/tests/types/test_lazy_types.py index 4fa2c875db..b4e1079ab7 100644 --- a/tests/types/test_lazy_types.py +++ b/tests/types/test_lazy_types.py @@ -23,7 +23,7 @@ class LazyEnum(enum.Enum): def test_lazy_type(): # Module path is short and relative because of the way pytest runs the file - LazierType = LazyType("LaziestType", "test_lazy_types") + LazierType = LazyType["LaziestType", "test_lazy_types"] annotation = StrawberryAnnotation(LazierType) resolved = annotation.resolve() @@ -38,7 +38,7 @@ def test_lazy_type(): def test_lazy_type_enum(): # Module path is short and relative because of the way pytest runs the file - LazierType = LazyType("LazyEnum", "test_lazy_types") + LazierType = LazyType["LazyEnum", "test_lazy_types"] annotation = StrawberryAnnotation(LazierType) resolved = annotation.resolve() @@ -53,7 +53,7 @@ def test_lazy_type_enum(): def test_lazy_type_argument(): # Module path is short and relative because of the way pytest runs the file - LazierType = LazyType("LaziestType", "test_lazy_types") + LazierType = LazyType["LaziestType", "test_lazy_types"] @strawberry.mutation def slack_off(emotion: LazierType) -> bool: @@ -63,19 +63,19 @@ def slack_off(emotion: LazierType) -> bool: argument = slack_off.arguments[0] assert isinstance(argument.type, LazyType) assert argument.type is LazierType - assert argument.type.resolve_type() is LaziestType + assert argument.type.resolve_type() is LaziestType # type: ignore def test_lazy_type_field(): # Module path is short and relative because of the way pytest runs the file - LazierType = LazyType("LaziestType", "test_lazy_types") + LazierType = LazyType["LaziestType", "test_lazy_types"] annotation = StrawberryAnnotation(LazierType) field = StrawberryField(type_annotation=annotation) assert isinstance(field.type, LazyType) assert field.type is LazierType - assert field.type.resolve_type() is LaziestType + assert field.type.resolve_type() is LaziestType # type: ignore def test_lazy_type_generic(): @@ -86,7 +86,7 @@ class GenericType(Generic[T]): item: T # Module path is short and relative because of the way pytest runs the file - LazierType = LazyType("LaziestType", "test_lazy_types") + LazierType = LazyType["LaziestType", "test_lazy_types"] ResolvedType = GenericType[LazierType] annotation = StrawberryAnnotation(ResolvedType) @@ -104,7 +104,7 @@ class GenericType(Generic[T]): def test_lazy_type_object(): # Module path is short and relative because of the way pytest runs the file - LazierType = LazyType("LaziestType", "test_lazy_types") + LazierType = LazyType["LaziestType", "test_lazy_types"] @strawberry.type class WaterParkFeature: @@ -115,12 +115,12 @@ class WaterParkFeature: assert isinstance(field.type, LazyType) assert field.type is LazierType - assert field.type.resolve_type() is LaziestType + assert field.type.resolve_type() is LaziestType # type: ignore def test_lazy_type_resolver(): # Module path is short and relative because of the way pytest runs the file - LazierType = LazyType("LaziestType", "test_lazy_types") + LazierType = LazyType["LaziestType", "test_lazy_types"] def slaking_pokemon() -> LazierType: raise NotImplementedError @@ -128,4 +128,4 @@ def slaking_pokemon() -> LazierType: resolver = StrawberryResolver(slaking_pokemon) assert isinstance(resolver.type, LazyType) assert resolver.type is LazierType - assert resolver.type.resolve_type() is LaziestType + assert resolver.type.resolve_type() is LaziestType # type: ignore diff --git a/tests/types/test_object_types.py b/tests/types/test_object_types.py index 35ed36d8c5..e6989e96d2 100644 --- a/tests/types/test_object_types.py +++ b/tests/types/test_object_types.py @@ -1,11 +1,7 @@ # type: ignore -import dataclasses -import re from enum import Enum from typing import List, Optional, TypeVar -import pytest - import strawberry from strawberry.field import StrawberryField @@ -126,62 +122,3 @@ class WishfulThinking: field: StrawberryField = WishfulThinking._type_definition.fields[0] assert field.type is EU - - -def test_fields_with_defaults(): - @strawberry.type - class Country: - name: str = "United Kingdom" - currency_code: str - - country = Country(currency_code="GBP") - assert country.name == "United Kingdom" - assert country.currency_code == "GBP" - - country = Country(name="United States of America", currency_code="USD") - assert country.name == "United States of America" - assert country.currency_code == "USD" - - -def test_fields_with_defaults_inheritance(): - @strawberry.interface - class A: - text: str - delay: Optional[int] = None - - @strawberry.type - class B(A): - attachments: Optional[List[A]] = None - - @strawberry.type - class C(A): - fields: List[B] - - c_inst = C( - text="some text", - fields=[B(text="more text")], - ) - - assert dataclasses.asdict(c_inst) == { - "text": "some text", - "delay": None, - "fields": [ - { - "text": "more text", - "attachments": None, - "delay": None, - } - ], - } - - -def test_positional_args_not_allowed(): - @strawberry.type - class Thing: - name: str - - with pytest.raises( - TypeError, - match=re.escape("__init__() takes 1 positional argument but 2 were given"), - ): - Thing("something") diff --git a/tests/utils/test_arguments_converter.py b/tests/utils/test_arguments_converter.py index 7d64e10a00..bcf92fb22c 100644 --- a/tests/utils/test_arguments_converter.py +++ b/tests/utils/test_arguments_converter.py @@ -1,17 +1,12 @@ from enum import Enum from typing import List, Optional -from typing_extensions import Annotated - -import pytest import strawberry from strawberry.annotation import StrawberryAnnotation -from strawberry.arguments import StrawberryArgument, convert_arguments -from strawberry.exceptions import UnsupportedTypeError +from strawberry.arguments import UNSET, StrawberryArgument, convert_arguments from strawberry.lazy_type import LazyType from strawberry.schema.config import StrawberryConfig from strawberry.schema.types.scalar import DEFAULT_SCALAR_REGISTRY -from strawberry.unset import UNSET def test_simple_types(): @@ -111,29 +106,6 @@ def test_lazy(): ) == {"lazy_arg": LaziestType(something=True)} -def test_annotated(): - LazierType = Annotated["LaziestType", strawberry.lazy(__name__)] - - args = { - "lazyArg": {"something": True}, - } - - arguments = [ - StrawberryArgument( - graphql_name="lazyArg", - python_name="lazy_arg", - type_annotation=StrawberryAnnotation(LazierType), - ), - ] - - assert convert_arguments( - args, - arguments, - scalar_registry=DEFAULT_SCALAR_REGISTRY, - config=StrawberryConfig(), - ) == {"lazy_arg": LaziestType(something=True)} - - def test_input_types(): @strawberry.input class MyInput: @@ -257,9 +229,9 @@ class AddReleaseFileCommentInput: args = { "input": { "prNumber": 12, - "status": ReleaseFileStatus.OK, + "status": ReleaseFileStatus.OK.value, "releaseInfo": { - "changeType": ChangeType.MAJOR, + "changeType": ChangeType.MAJOR.value, "changelog": "example", }, } @@ -289,7 +261,7 @@ class AddReleaseFileCommentInput: args = { "input": { "prNumber": 12, - "status": ReleaseFileStatus.OK, + "status": ReleaseFileStatus.OK.value, "releaseInfo": None, } } @@ -338,7 +310,7 @@ class Input: arguments, scalar_registry=DEFAULT_SCALAR_REGISTRY, config=StrawberryConfig(), - ) == {"input": Input(numbers=[Number(value=1), Number(value=2)])} + ) == {"input": Input(numbers=[Number(1), Number(2)])} def test_uses_default_for_optional_types_when_nothing_is_passed(): @@ -367,7 +339,7 @@ class Input: arguments, scalar_registry=DEFAULT_SCALAR_REGISTRY, config=StrawberryConfig(), - ) == {"input": Input(numbers=UNSET, numbers_second=UNSET)} + ) == {"input": Input(UNSET, UNSET)} # case 2 args = {"input": {"numbersSecond": None}} @@ -385,7 +357,7 @@ class Input: arguments, scalar_registry=DEFAULT_SCALAR_REGISTRY, config=StrawberryConfig(), - ) == {"input": Input(numbers=UNSET, numbers_second=None)} + ) == {"input": Input(UNSET, None)} def test_when_optional(): @@ -417,36 +389,3 @@ class Input: ) == {} ) - - -@pytest.mark.raises_strawberry_exception( - UnsupportedTypeError, - match=r" conversion is not supported", -) -def test_fails_when_passing_non_strawberry_classes(): - class Input: - numbers: List[int] - - args = { - "input": { - "numbers": [1, 2], - } - } - - arguments = [ - StrawberryArgument( - graphql_name=None, - python_name="input", - type_annotation=StrawberryAnnotation(Optional[Input]), - ) - ] - - assert ( - convert_arguments( - args, - arguments, - scalar_registry=DEFAULT_SCALAR_REGISTRY, - config=StrawberryConfig(), - ) - == {} - ) diff --git a/tests/utils/test_get_first_operation.py b/tests/utils/test_get_first_operation.py deleted file mode 100644 index 5ef9c3e57e..0000000000 --- a/tests/utils/test_get_first_operation.py +++ /dev/null @@ -1,41 +0,0 @@ -from graphql import OperationType, parse - -from strawberry.utils.operation import get_first_operation - - -def test_document_without_operation_definition_notes(): - document = parse( - """ - fragment Test on Query { - hello - } - """ - ) - assert get_first_operation(document) is None - - -def test_single_operation_definition_note(): - document = parse( - """ - query Operation1 { - hello - } - """ - ) - assert get_first_operation(document) is not None - assert get_first_operation(document).operation == OperationType.QUERY - - -def test_multiple_operation_definition_notes(): - document = parse( - """ - mutation Operation1 { - hello - } - query Operation2 { - hello - } - """ - ) - assert get_first_operation(document) is not None - assert get_first_operation(document).operation == OperationType.MUTATION