pax_global_header00006660000000000000000000000064145512716550014525gustar00rootroot0000000000000052 comment=068f2660ab207cbbb452114a525d64a4bc962ee8 graphene-directives-0.4.6/000077500000000000000000000000001455127165500154645ustar00rootroot00000000000000graphene-directives-0.4.6/.github/000077500000000000000000000000001455127165500170245ustar00rootroot00000000000000graphene-directives-0.4.6/.github/workflows/000077500000000000000000000000001455127165500210615ustar00rootroot00000000000000graphene-directives-0.4.6/.github/workflows/lint.yml000066400000000000000000000015551455127165500225600ustar00rootroot00000000000000# This workflow will install Python dependencies, run tests and lint with a single version of Python # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python name: Lint on: [push, pull_request] permissions: contents: read concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: build: strategy: matrix: python: ["3.12"] runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Set up Python uses: actions/setup-python@v3 with: python-version: ${{ matrix.python }} - name: Install dependencies run: | python -m pip install --upgrade pip poetry poetry install --only dev - name: ruff check run: | poetry run ruff check . poetry run ruff format --diff . graphene-directives-0.4.6/.github/workflows/publish-release.yml000066400000000000000000000027351455127165500246770ustar00rootroot00000000000000# This workflow will upload a Python Package using Twine when a release is created # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries name: Publish to PyPI on: release: types: [published] permissions: contents: read jobs: build: strategy: matrix: os: [ubuntu-latest, macos-latest, windows-latest] python: ["3.9", "3.10", "3.11", "3.12"] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v3 - name: Set up Python uses: actions/setup-python@v3 with: python-version: ${{ matrix.python }} - name: Install dependencies run: | python -m pip install --upgrade pip poetry poetry install --with dev - name: Ruff check run: | poetry run ruff check . poetry run ruff format --diff . - name: Test with pytest run: | poetry run pytest deploy: needs: build runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Set up Python uses: actions/setup-python@v3 with: python-version: '3.12' - name: Install dependencies run: | python -m pip install --upgrade pip pip install poetry - name: Build & Release package env: POETRY_PYPI_TOKEN_PYPI: ${{ secrets.PYPI_TOKEN }} run: | poetry publish --build graphene-directives-0.4.6/.github/workflows/test.yml000066400000000000000000000033201455127165500225610ustar00rootroot00000000000000# This workflow will install Python dependencies, run tests and lint with a single version of Python # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python name: Test Package on: push: branches: [ "main" ] pull_request: branches: [ "main" ] workflow_dispatch: concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true permissions: contents: read jobs: test: strategy: matrix: os: [ubuntu-latest, macos-latest, windows-latest] python: ["3.9", "3.10", "3.11", "3.12"] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python }} uses: actions/setup-python@v3 with: python-version: ${{ matrix.python }} - name: Install dependencies run: | python -m pip install --upgrade pip poetry poetry install --with dev - name: Ruff check run: | poetry run ruff check . poetry run ruff format --diff . - name: Test with pytest run: | poetry run pytest build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Set up Python 3.12 uses: actions/setup-python@v3 with: python-version: "3.12" - name: Install dependencies run: | python -m pip install --upgrade poetry - name: Build package run: | poetry version "$(poetry version --short | cut -f1 -d + )+build-$(date +'%Y%m%d%H%M%S')" poetry build - name: Upload to GitHub Artifacts uses: actions/upload-artifact@v3 with: name: build-dist path: dist/ graphene-directives-0.4.6/.gitignore000066400000000000000000000060321455127165500174550ustar00rootroot00000000000000### Python template # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ share/python-wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .nox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover *.py,cover .hypothesis/ .pytest_cache/ cover/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 db.sqlite3-journal # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder .pybuilder/ target/ # Jupyter Notebook .ipynb_checkpoints # IPython profile_default/ ipython_config.py # pyenv # For a library or package, you might want to ignore these files since the code is # intended to run in multiple environments; otherwise, check them in: # .python-version # pipenv # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. # However, in case of collaboration, if having platform-specific dependencies or dependencies # having no cross-platform support, pipenv may install dependencies that don't work, or not # install all needed dependencies. #Pipfile.lock # poetry # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. # This is especially recommended for binary packages to ensure reproducibility, and is more # commonly ignored for libraries. # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control #poetry.lock # pdm # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. #pdm.lock # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it # in version control. # https://pdm.fming.dev/#use-with-ide .pdm.toml # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm __pypackages__/ # Celery stuff celerybeat-schedule celerybeat.pid # SageMath parsed files *.sage.py # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ .dmypy.json dmypy.json # Pyre type checker .pyre/ # pytype static type analyzer .pytype/ # Cython debug symbols cython_debug/ # PyCharm # JetBrains specific template is maintained in a separate JetBrains.gitignore that can # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ graphene-directives-0.4.6/.pre-commit-config.yaml000066400000000000000000000017631455127165500217540ustar00rootroot00000000000000repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.5.0 hooks: - id: check-yaml - id: check-added-large-files - id: end-of-file-fixer exclude: .*\.graphql - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.1.12 hooks: - id: ruff args: [ --fix, --preview ] stages: [ commit, push ] - id: ruff-format stages: [ commit, push ] - repo: https://github.com/zricethezav/gitleaks rev: v8.18.0 hooks: - id: gitleaks - repo: https://github.com/PyCQA/bandit rev: '1.7.5' hooks: - id: bandit args: [ -ll ] # - repo: https://github.com/pylint-dev/pylint # rev: 23.9.1 # hooks: # - id: pylint # name: pylint # entry: pylint # language: system # types: [ python ] # args: # [ # "-rn", # Only display messages # "-sn", # Don't display the score # ] # language_version: python3.12 graphene-directives-0.4.6/LICENSE.md000066400000000000000000000020511455127165500170660ustar00rootroot00000000000000MIT License Copyright (c) 2023 Strollby Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. graphene-directives-0.4.6/README.md000066400000000000000000000154321455127165500167500ustar00rootroot00000000000000# Graphene Directives Schema Directives implementation for graphene [![PyPI version][pypi-image]][pypi-url] [![PyPI pyversions][pypi-version-image]][pypi-version-url] [![Downloads][pypi-downloads-image]][pypi-downloads-url] [![Test Status][tests-image]][tests-url] [![Coverage Status][coveralls-image]][coveralls-url] [pypi-image]: https://badge.fury.io/py/graphene-directives.svg [pypi-url]: https://pypi.org/project/graphene-directives/ [pypi-version-image]: https://img.shields.io/pypi/pyversions/graphene-directives.svg [pypi-version-url]: https://pypi.python.org/pypi/graphene-directives/ [pypi-downloads-image]: https://pepy.tech/badge/graphene-directives [pypi-downloads-url]: https://pepy.tech/project/graphene-directives [tests-image]: https://github.com/strollby/graphene-directives/actions/workflows/test.yml/badge.svg?branch=main [tests-url]: https://github.com/strollby/graphene-directives/actions/workflows/test.yml [coveralls-image]: https://coveralls.io/repos/github/strollby/graphene-directives/badge.svg?branch=main [coveralls-url]: https://coveralls.io/github/strollby/graphene-directives?branch=main ------------------------ ## Directive Locations Supported - [x] DirectiveLocation.SCHEMA - [x] DirectiveLocation.OBJECT - [x] DirectiveLocation.ENUM - [x] DirectiveLocation.INTERFACE - [x] DirectiveLocation.UNION - [x] DirectiveLocation.SCALAR - [x] DirectiveLocation.FIELD_DEFINITION - [x] DirectiveLocation.INPUT_FIELD_DEFINITION - [x] DirectiveLocation.INPUT_OBJECT - [x] DirectiveLocation.ENUM_VALUE - [x] DirectiveLocation.ARGUMENT_DEFINITION, ------------------------ ## Example ### Using `@directive` ```python import graphene from graphql import ( GraphQLArgument, GraphQLInt, GraphQLNonNull, GraphQLString, ) from graphene_directives import CustomDirective, DirectiveLocation, build_schema, directive CacheDirective = CustomDirective( name="cache", locations=[DirectiveLocation.FIELD_DEFINITION, DirectiveLocation.OBJECT], args={ "max_age": GraphQLArgument( GraphQLNonNull(GraphQLInt), description="Specifies the maximum age for cache in seconds.", ), "swr": GraphQLArgument( GraphQLInt, description="Stale-while-revalidate value in seconds. Optional." ), "scope": GraphQLArgument( GraphQLString, description="Scope of the cache. Optional." ), }, description="Caching directive to control cache behavior of fields or fragments.", ) @directive(CacheDirective, max_age=200) class SomeType(graphene.ObjectType): field_1 = directive(CacheDirective, field=graphene.String(), max_age=300) field_2 = directive(CacheDirective, field=graphene.String(), max_age=300, swr=2) field_3 = graphene.String() class Query(graphene.ObjectType): some_query = graphene.Field(SomeType) schema = build_schema( query=Query, directives=[CacheDirective] ) ``` ### Using `directive_decorator` ```python import graphene from graphql import ( GraphQLArgument, GraphQLInt, GraphQLNonNull, GraphQLString, ) from graphene_directives import CustomDirective, DirectiveLocation, build_schema, directive_decorator CacheDirective = CustomDirective( name="cache", locations=[DirectiveLocation.FIELD_DEFINITION, DirectiveLocation.OBJECT], args={ "max_age": GraphQLArgument( GraphQLNonNull(GraphQLInt), description="Specifies the maximum age for cache in seconds.", ), "swr": GraphQLArgument( GraphQLInt, description="Stale-while-revalidate value in seconds. Optional." ), "scope": GraphQLArgument( GraphQLString, description="Scope of the cache. Optional." ), }, description="Caching directive to control cache behavior of fields or fragments.", ) # This returns a partial of directive function cache = directive_decorator(target_directive=CacheDirective) @cache(max_age=200) class SomeType(graphene.ObjectType): field_1 = cache(field=graphene.String(), max_age=300) field_2 = cache(field=graphene.String(), max_age=300, swr=2) field_3 = graphene.String() class Query(graphene.ObjectType): some_query = graphene.Field(SomeType) schema = build_schema( query=Query, directives=[CacheDirective] ) ``` ### Custom Input Validation ```python from typing import Any import graphene from graphql import ( GraphQLArgument, GraphQLInt, GraphQLNonNull, GraphQLString, ) from graphene_directives import CustomDirective, DirectiveLocation, Schema, build_schema, directive_decorator def input_transform(inputs: dict, _schema: Schema) -> dict: """ def input_transform (inputs: Any, schema: Schema) -> dict, """ if inputs.get("max_age") > 200: inputs["swr"] = 30 return inputs def validate_non_field_input(_type: Any, inputs: dict, _schema: Schema) -> bool: """ def validator (type_: graphene type, inputs: Any, schema: Schema) -> bool, if validator returns False, library raises DirectiveCustomValidationError """ if inputs.get("max_age") > 2500: return False return True def validate_field_input( _parent_type: Any, _field_type: Any, inputs: dict, _schema: Schema ) -> bool: """ def validator (parent_type_: graphene_type, field_type_: graphene type, inputs: Any, schema: Schema) -> bool, if validator returns False, library raises DirectiveCustomValidationError """ if inputs.get("max_age") > 2500: return False return True CacheDirective = CustomDirective( name="cache", locations=[DirectiveLocation.FIELD_DEFINITION, DirectiveLocation.OBJECT], args={ "max_age": GraphQLArgument( GraphQLNonNull(GraphQLInt), description="Specifies the maximum age for cache in seconds.", ), "swr": GraphQLArgument( GraphQLInt, description="Stale-while-revalidate value in seconds. Optional." ), "scope": GraphQLArgument( GraphQLString, description="Scope of the cache. Optional." ), }, description="Caching directive to control cache behavior of fields or fragments.", non_field_validator=validate_non_field_input, field_validator=validate_field_input, input_transform=input_transform, ) # This returns a partial of directive function cache = directive_decorator(target_directive=CacheDirective) @cache(max_age=200) class SomeType(graphene.ObjectType): field_1 = cache(field=graphene.String(), max_age=300) field_2 = cache(field=graphene.String(), max_age=300, swr=2) field_3 = graphene.String() class Query(graphene.ObjectType): some_query = graphene.Field(SomeType) schema = build_schema( query=Query, directives=[CacheDirective] ) ``` ### Complex Use Cases Refer [`Code`](./example/complex_uses.py) and [`Graphql Output`](./example/complex_uses.graphql) graphene-directives-0.4.6/example/000077500000000000000000000000001455127165500171175ustar00rootroot00000000000000graphene-directives-0.4.6/example/complex_uses.graphql000066400000000000000000000054201455127165500232060ustar00rootroot00000000000000extend schema @compose(directiveName: "lowercase") @compose(directiveName: "uppercase") @compose(directiveName: "pascalcase") """Caching directive to control cache behavior of fields or fragments.""" directive @cache( """Specifies the maximum age for cache in seconds.""" maxAge: Int! """Stale-while-revalidate value in seconds. Optional.""" swr: Int """Scope of the cache. Optional.""" scope: String ) on OBJECT | INTERFACE | ENUM | UNION | INPUT_OBJECT | FIELD_DEFINITION | INPUT_FIELD_DEFINITION | SCALAR """Auth directive to control authorization behavior.""" directive @authenticated( """Auth required""" required: Boolean! ) on OBJECT | INTERFACE | ENUM | ENUM_VALUE | UNION | INPUT_OBJECT | FIELD_DEFINITION | SCALAR | ARGUMENT_DEFINITION """Directive to indicate this is a internal field.""" directive @internal on OBJECT | FIELD_DEFINITION | ARGUMENT_DEFINITION """A repeatable directive.""" directive @repeatable_directive( """Service Name required""" serviceName: String! ) repeatable on OBJECT | FIELD_DEFINITION union SearchResult @cache(maxAge: 500, swr: 30) @authenticated(required: true) = Human | Droid | Starship type Human @cache(maxAge: 60) { name: String bornIn: String } type Droid @cache(maxAge: 200) { """Test Description""" name: String @deprecated(reason: "Deprecated use born in") @cache(maxAge: 300, swr: 30) primaryFunction: String } type Starship @cache(maxAge: 200) { name: String length: Int @deprecated(reason: "Use another field") @cache(maxAge: 60) } interface Animal @cache(maxAge: 100) @authenticated(required: true) { age: Int! kind: Int! @cache(maxAge: 60) } type Admin @internal @key { name: String password: String } input HumanInput @cache(maxAge: 60) @authenticated(required: true) { bornIn: String """Test Description""" name: String @deprecated(reason: "Deprecated use born in") @cache(maxAge: 300, swr: 30) } enum TruthEnum @cache(maxAge: 100) @authenticated(required: true) { A @authenticated(required: true) B } scalar DateNewScalar @cache(maxAge: 500, swr: 30) @authenticated(required: true) type User @authenticated(required: true) { name: String password: String price(currency: Int @internal, country: Int @authenticated(required: true) @internal): String } type Company @authenticated(required: true) @repeatable_directive(serviceName: "CompanyService") @repeatable_directive(serviceName: "ProductService") { established: Int! name: String! @deprecated(reason: "This field is deprecated and will be removed in future") @repeatable_directive(serviceName: "CompanyService Field") @repeatable_directive(serviceName: "ProductService Field") } type Query { position: Position @deprecated(reason: "unused field") } type Position @cache(maxAge: 100) { x: Int! y: Int! @cache(maxAge: 60) }graphene-directives-0.4.6/example/complex_uses.py000066400000000000000000000227111455127165500222020ustar00rootroot00000000000000import os from typing import Any import graphene from graphql import ( GraphQLArgument, GraphQLBoolean, GraphQLInt, GraphQLNonNull, GraphQLString, ) from graphene_directives import ( CustomDirective, DirectiveLocation, Schema, SchemaDirective, build_schema, directive, ) curr_dir = os.path.dirname(os.path.realpath(__file__)) def input_transform(inputs: dict, _schema: Schema) -> dict: """ def input_transform (inputs: Any, schema: Schema) -> dict, """ if inputs.get("max_age") > 200: inputs["swr"] = 30 return inputs def validate_non_field_input(_type: Any, inputs: dict, _schema: Schema) -> bool: """ def validator (type_: graphene type, inputs: Any, schema: Schema) -> bool, if validator returns False, library raises DirectiveCustomValidationError """ if inputs.get("max_age") > 2500: return False return True def validate_field_input( _parent_type: Any, _field_type: Any, inputs: dict, _schema: Schema ) -> bool: """ def validator (parent_type_: graphene_type, field_type_: graphene type, inputs: Any, schema: Schema) -> bool, if validator returns False, library raises DirectiveCustomValidationError """ if inputs.get("max_age") > 2500: return False return True CacheDirective = CustomDirective( name="cache", locations=[ DirectiveLocation.OBJECT, DirectiveLocation.INTERFACE, DirectiveLocation.ENUM, DirectiveLocation.UNION, DirectiveLocation.INPUT_OBJECT, DirectiveLocation.FIELD_DEFINITION, DirectiveLocation.INPUT_FIELD_DEFINITION, DirectiveLocation.SCALAR, ], args={ "max_age": GraphQLArgument( GraphQLNonNull(GraphQLInt), description="Specifies the maximum age for cache in seconds.", ), "swr": GraphQLArgument( GraphQLInt, description="Stale-while-revalidate value in seconds. Optional." ), "scope": GraphQLArgument( GraphQLString, description="Scope of the cache. Optional." ), }, description="Caching directive to control cache behavior of fields or fragments.", non_field_validator=validate_non_field_input, field_validator=validate_field_input, input_transform=input_transform, ) AuthenticatedDirective = CustomDirective( name="authenticated", locations=[ DirectiveLocation.OBJECT, DirectiveLocation.INTERFACE, DirectiveLocation.ENUM, DirectiveLocation.ENUM_VALUE, DirectiveLocation.UNION, DirectiveLocation.INPUT_OBJECT, DirectiveLocation.FIELD_DEFINITION, DirectiveLocation.SCALAR, DirectiveLocation.ARGUMENT_DEFINITION, ], args={ "required": GraphQLArgument( GraphQLNonNull(GraphQLBoolean), description="Auth required" ) }, description="Auth directive to control authorization behavior.", ) # This will prevent @key definition from being added to the schema, but decorator will work KeyDirective = CustomDirective( name="key", locations=[DirectiveLocation.OBJECT, DirectiveLocation.INTERFACE], description="Declaring an entity", add_definition_to_schema=False, ) # No argument directive InternalDirective = CustomDirective( name="internal", locations=[ DirectiveLocation.OBJECT, DirectiveLocation.FIELD_DEFINITION, DirectiveLocation.ARGUMENT_DEFINITION, ], description="Directive to indicate this is a internal field.", ) # A Repeatable directive RepeatableDirective = CustomDirective( name="repeatable_directive", locations=[DirectiveLocation.OBJECT, DirectiveLocation.FIELD_DEFINITION], description="A repeatable directive.", args={ "service_name": GraphQLArgument( GraphQLNonNull(GraphQLString), description="Service Name required" ) }, is_repeatable=True, ) # A Schema directive ComposeDirective = CustomDirective( name="compose", locations=[DirectiveLocation.SCHEMA], description="A schema directive.", args={ "directive_name": GraphQLArgument( GraphQLNonNull(GraphQLString), description="Directive Name required" ) }, is_repeatable=True, ) @directive(target_directive=CacheDirective, max_age=100) @directive(target_directive=AuthenticatedDirective, required=True) class Animal(graphene.Interface): # DirectiveLocation.INTERFACE age = graphene.Int(required=True) kind = directive( target_directive=CacheDirective, field=graphene.Int(required=True), max_age=60 ) # DirectiveLocation.FIELD_DEFINITION @directive(target_directive=CacheDirective, max_age=100) @directive(target_directive=AuthenticatedDirective, required=True) class TruthEnum(graphene.Enum): # DirectiveLocation.ENUM A = 1 B = 2 # Add directives to enum values [DirectiveLocation.ENUM_VALUE] directive(field=TruthEnum.A, target_directive=AuthenticatedDirective, required=True) @directive(target_directive=CacheDirective, max_age=100) class Position(graphene.ObjectType): # DirectiveLocation.OBJECT x = graphene.Int(required=True) y = directive( target_directive=CacheDirective, field=graphene.Int(required=True), max_age=60 ) @directive(target_directive=CacheDirective, max_age=60) # DirectiveLocation.OBJECT class Human(graphene.ObjectType): name = graphene.String() born_in = graphene.String() @directive(target_directive=CacheDirective, max_age=60) @directive(target_directive=AuthenticatedDirective, required=True) class HumanInput(graphene.InputObjectType): # DirectiveLocation.INPUT_OBJECT born_in = graphene.String() name = directive( # DirectiveLocation.INPUT_FIELD_DEFINITION CacheDirective, field=graphene.String( description="Test Description", deprecation_reason="Deprecated use born in" ), max_age=300, ) @directive(CacheDirective, max_age=200) class Droid(graphene.ObjectType): name = directive( CacheDirective, field=graphene.String( description="Test Description", deprecation_reason="Deprecated use born in" ), max_age=300, ) primary_function = graphene.String() @directive(CacheDirective, max_age=200) class Starship(graphene.ObjectType): name = graphene.String() length = directive( target_directive=CacheDirective, field=graphene.Int(deprecation_reason="Use another field"), max_age=60, ) @directive(target_directive=CacheDirective, max_age=500) @directive(target_directive=AuthenticatedDirective, required=True) class SearchResult(graphene.Union): # DirectiveLocation.UNION class Meta: types = (Human, Droid, Starship) @directive(target_directive=CacheDirective, max_age=500) @directive(target_directive=AuthenticatedDirective, required=True) class DateNewScalar(graphene.Scalar): # DirectiveLocation.SCALAR pass @directive(target_directive=KeyDirective) @directive(target_directive=InternalDirective) class Admin(graphene.ObjectType): name = graphene.String() password = graphene.String() @directive(target_directive=AuthenticatedDirective, required=True) class User(graphene.ObjectType): name = graphene.String() password = graphene.String() price = graphene.Field( graphene.String, currency=directive( # DirectiveLocation.ARGUMENT_DEFINITION InternalDirective, field=graphene.Argument(graphene.Int) ), # Argument country=directive( # Multiple DirectiveLocation.ARGUMENT_DEFINITION target_directive=AuthenticatedDirective, field=directive(InternalDirective, field=graphene.Argument(graphene.Int)), required=True, ), ) @directive(target_directive=RepeatableDirective, service_name="ProductService") @directive(target_directive=RepeatableDirective, service_name="CompanyService") @directive(target_directive=AuthenticatedDirective, required=True) class Company(graphene.ObjectType): established = graphene.Int(required=True) name = directive( target_directive=RepeatableDirective, field=directive( target_directive=RepeatableDirective, field=graphene.String( required=True, deprecation_reason="This field is deprecated and will be removed in future", ), service_name="CompanyService Field", ), service_name="ProductService Field", ) class Query(graphene.ObjectType): position = graphene.Field(Position, deprecation_reason="unused field") schema = build_schema( query=Query, types=( SearchResult, Animal, Admin, HumanInput, TruthEnum, DateNewScalar, User, Company, ), directives=( CacheDirective, AuthenticatedDirective, InternalDirective, KeyDirective, RepeatableDirective, ), schema_directives=( # extend schema directives SchemaDirective( target_directive=ComposeDirective, arguments={"directive_name": "lowercase"} ), SchemaDirective( target_directive=ComposeDirective, arguments={"directive_name": "uppercase"} ), SchemaDirective( target_directive=ComposeDirective, arguments={"directive_name": "pascalcase"}, ), ), ) def generate_schema() -> None: with open(f"{curr_dir}/complex_uses.graphql", "w") as f: f.write(str(schema)) if __name__ == "__main__": generate_schema() graphene-directives-0.4.6/graphene_directives/000077500000000000000000000000001455127165500214765ustar00rootroot00000000000000graphene-directives-0.4.6/graphene_directives/__init__.py000066400000000000000000000011121455127165500236020ustar00rootroot00000000000000from .constants import DirectiveLocation from .data_models import SchemaDirective from .directive import ACCEPTED_TYPES from .directive import CustomDirective, directive, directive_decorator from .exceptions import DirectiveCustomValidationError, DirectiveValidationError from .main import build_schema from .schema import Schema __all__ = [ "build_schema", "Schema", "CustomDirective", "SchemaDirective", "directive_decorator", "directive", "ACCEPTED_TYPES", "DirectiveLocation", "DirectiveCustomValidationError", "DirectiveValidationError", ] graphene-directives-0.4.6/graphene_directives/constants.py000066400000000000000000000034361455127165500240720ustar00rootroot00000000000000from enum import Enum import graphene from graphql import DirectiveLocation as GrapheneDirectiveLocation LOCATION_NON_FIELD_VALIDATOR = { GrapheneDirectiveLocation.SCHEMA: lambda t: issubclass(t, graphene.Schema), GrapheneDirectiveLocation.SCALAR: lambda t: issubclass(t, graphene.Scalar), GrapheneDirectiveLocation.OBJECT: lambda t: issubclass(t, graphene.ObjectType), GrapheneDirectiveLocation.INTERFACE: lambda t: issubclass(t, graphene.Interface), GrapheneDirectiveLocation.UNION: lambda t: issubclass(t, graphene.Union), GrapheneDirectiveLocation.ENUM: lambda t: issubclass(t, graphene.Enum), GrapheneDirectiveLocation.INPUT_OBJECT: lambda t: issubclass( t, graphene.InputObjectType ), } FIELD_TYPES = { GrapheneDirectiveLocation.FIELD_DEFINITION, GrapheneDirectiveLocation.INPUT_FIELD_DEFINITION, GrapheneDirectiveLocation.ENUM_VALUE, GrapheneDirectiveLocation.ARGUMENT_DEFINITION, } ACCEPTED_TYPES = FIELD_TYPES.union(set(LOCATION_NON_FIELD_VALIDATOR.keys())) TYPE_STRING_MAPPING = { graphene.Scalar: "scalar", graphene.Union: "union", graphene.ObjectType: "type", graphene.Interface: "interface", graphene.Enum: "enum", graphene.InputObjectType: "input", } class DirectiveLocation(Enum): """The enum type representing the directive location values.""" SCHEMA = "schema" SCALAR = "scalar" OBJECT = "object" FIELD_DEFINITION = "field definition" ARGUMENT_DEFINITION = "argument definition" INTERFACE = "interface" UNION = "union" ENUM = "enum" ENUM_VALUE = "enum value" INPUT_OBJECT = "input object" INPUT_FIELD_DEFINITION = "input field definition" def to_graphene_directive_location(self) -> GrapheneDirectiveLocation: return GrapheneDirectiveLocation(self.value) graphene-directives-0.4.6/graphene_directives/data_models/000077500000000000000000000000001455127165500237525ustar00rootroot00000000000000graphene-directives-0.4.6/graphene_directives/data_models/__init__.py000066400000000000000000000002331455127165500260610ustar00rootroot00000000000000from .custom_directive_meta import CustomDirectiveMeta from .schema_directive import SchemaDirective __all__ = ["SchemaDirective", "CustomDirectiveMeta"] graphene-directives-0.4.6/graphene_directives/data_models/custom_directive_meta.py000066400000000000000000000015171455127165500307060ustar00rootroot00000000000000from dataclasses import dataclass from typing import Any, Callable, Union from graphql import DirectiveLocation as GrapheneDirectiveLocation @dataclass class CustomDirectiveMeta: allow_all_directive_locations: bool add_definition_to_schema: bool has_no_argument: bool valid_types: set[GrapheneDirectiveLocation] non_field_types: set[GrapheneDirectiveLocation] supports_field_types: bool supports_non_field_types: bool input_transform: Union[ Callable[[dict[str, Any], Any], dict[str, Any]], None ] # (args, schema) -> args non_field_validator: Union[ Callable[[Any, dict[str, Any], Any], bool], None ] # (type, args, schema) -> valid field_validator: Union[ Callable[[Any, Any, dict[str, Any], Any], bool], None ] # (parent_type, field_type, args, schema) -> valid graphene-directives-0.4.6/graphene_directives/data_models/schema_directive.py000066400000000000000000000013431455127165500276230ustar00rootroot00000000000000from dataclasses import dataclass from typing import Any from graphql import DirectiveLocation as GrapheneDirectiveLocation from graphql import GraphQLDirective from ..exceptions import DirectiveValidationError @dataclass class SchemaDirective: target_directive: GraphQLDirective arguments: dict[str, Any] def __post_init__(self): if GrapheneDirectiveLocation.SCHEMA not in self.target_directive.locations: raise DirectiveValidationError( ". ".join( [ f"{self.target_directive} cannot be used as schema directive", "Missing DirectiveLocation.SCHEMA in locations", ] ) ) graphene-directives-0.4.6/graphene_directives/directive.py000066400000000000000000000176151455127165500240400ustar00rootroot00000000000000from functools import partial from typing import Any, Callable, Collection, Dict, Optional from graphene.utils.str_converters import to_camel_case from graphql import GraphQLArgument, GraphQLDirective from graphql.language import ast from .constants import ACCEPTED_TYPES, FIELD_TYPES, LOCATION_NON_FIELD_VALIDATOR from .constants import DirectiveLocation from .data_models import CustomDirectiveMeta from .exceptions import ( DirectiveInvalidArgTypeError, DirectiveInvalidTypeError, DirectiveValidationError, ) from .parsers import parse_argument_values from .utils import field_attribute_name, non_field_attribute_name, set_attribute_value def CustomDirective( # noqa name: str, locations: Collection[DirectiveLocation], args: Optional[Dict[str, GraphQLArgument]] = None, is_repeatable: bool = False, description: Optional[str] = None, extensions: Optional[Dict[str, Any]] = None, ast_node: Optional[ast.DirectiveDefinitionNode] = None, allow_all_directive_locations: bool = False, add_definition_to_schema: bool = True, non_field_validator: Callable[[Any, dict[str, Any], Any], bool] = None, field_validator: Callable[[Any, Any, dict[str, Any], Any], bool] = None, input_transform: Callable[[dict[str, Any], Any], dict[str, Any]] = None, ) -> GraphQLDirective: """ Creates a GraphQLDirective :param name: (GraphQLDirective param) :param args: (GraphQLDirective param) :param is_repeatable: (GraphQLDirective param) :param description: (GraphQLDirective param) :param extensions: (GraphQLDirective param) :param ast_node: (GraphQLDirective param) :param locations: list[DirectiveLocation], if need to use unsupported locations, set allow_all_directive_locations True :param allow_all_directive_locations: Allow other DirectiveLocation other than the ones supported by library :param add_definition_to_schema: If false, the @directive definition is not added to the graphql schema :param non_field_validator: a non field validator function def validator (type_: graphene type, inputs: Any, schema: Schema) -> bool, if validator returns False, library raises DirectiveCustomValidationError :param field_validator: a field validator function def validator (parent_type_: graphene_type, field_type_: graphene type, inputs: Any, schema: Schema) -> bool, if validator returns False, library raises DirectiveCustomValidationError :param input_transform: a function to transform the input arg's values before usage def input_transform (inputs: dict[str, Any], schema: Schema) -> dict[str, Any] """ if not isinstance(allow_all_directive_locations, bool): raise DirectiveInvalidArgTypeError( f"directive @{name} allow_all_directive_locations type invalid expected bool" ) if not isinstance(add_definition_to_schema, bool): raise DirectiveInvalidArgTypeError( f"directive @{name} add_definition_to_schema type invalid expected bool" ) if not (isinstance(non_field_validator, Callable) or non_field_validator is None): raise DirectiveInvalidArgTypeError( f"directive @{name} validator type invalid expected Callable[[GraphQLDirective, Any], bool] " ) if ( any(not isinstance(location, DirectiveLocation) for location in locations) and not allow_all_directive_locations ): raise DirectiveInvalidArgTypeError( f"directive @{name} location type invalid expected {DirectiveLocation} from graphene_directives" ) if args is not None: camel_cased_args = {} for arg_name, arg_value in args.items(): camel_cased_args[to_camel_case(arg_name)] = arg_value else: camel_cased_args = args target_directive = GraphQLDirective( name=name, locations=[ location.to_graphene_directive_location() if isinstance(location, DirectiveLocation) else location for location in locations ], args=camel_cased_args, is_repeatable=is_repeatable, description=description, extensions=extensions, ast_node=ast_node, ) valid_types = set(target_directive.locations).intersection(ACCEPTED_TYPES) non_field_types = set(valid_types).difference(FIELD_TYPES) supports_field_types = FIELD_TYPES.intersection(valid_types) != 0 supports_non_field_types = ( set(LOCATION_NON_FIELD_VALIDATOR.values()).intersection(valid_types) != 0 ) target_directive._graphene_directive = CustomDirectiveMeta( allow_all_directive_locations=allow_all_directive_locations, add_definition_to_schema=add_definition_to_schema, has_no_argument=target_directive.args == {}, valid_types=valid_types, non_field_types=non_field_types, supports_field_types=supports_field_types, supports_non_field_types=supports_non_field_types, non_field_validator=non_field_validator, field_validator=field_validator, input_transform=input_transform, ) # Check if target_directive.locations have accepted types if ( len(valid_types) != len(target_directive.locations) and not allow_all_directive_locations ): invalid_types = [ str(i) for i in set(target_directive.locations).difference(ACCEPTED_TYPES) ] raise DirectiveValidationError( ", ".join( [ f"{str(target_directive)}: Directives don't support types: {invalid_types}", f"allowed types: {[str(i) for i in ACCEPTED_TYPES]}", ] ) ) return target_directive def directive( target_directive: GraphQLDirective, *, field: Optional[Any] = None, **_kwargs: Any ) -> Callable: """ Decorator to use to add directive a given type of field. """ if not hasattr(target_directive, "_graphene_directive"): raise DirectiveInvalidTypeError(target_directive) meta_data: CustomDirectiveMeta = getattr(target_directive, "_graphene_directive") # Converting inputs to camel_case kwargs = {to_camel_case(field): value for (field, value) in _kwargs.items()} directive_name = str(target_directive) kwargs = parse_argument_values(target_directive, kwargs) def decorator(type_: Any) -> Any: if not meta_data.supports_non_field_types: raise DirectiveValidationError( f"{directive_name} cannot be used at non field level" ) if not any( LOCATION_NON_FIELD_VALIDATOR[valid_type](type_) for valid_type in meta_data.non_field_types ): raise DirectiveValidationError( f"{directive_name} cannot be used for {type_}, valid levels are: {[str(i) for i in meta_data.valid_types]}" ) set_attribute_value( type_=type_, attribute_name=non_field_attribute_name(target_directive), target_directive=target_directive, data={} if meta_data.has_no_argument else kwargs, ) return type_ if field: if not meta_data.supports_field_types: raise DirectiveValidationError( f"{directive_name} cannot be used at field level" ) set_attribute_value( type_=field, attribute_name=field_attribute_name(target_directive), target_directive=target_directive, data={} if meta_data.has_no_argument else kwargs, ) return field return decorator def directive_decorator(target_directive: GraphQLDirective) -> directive: """ Build a decorator for given target directive :param target_directive: GraphQLDirective Returns partial of directive(target_directive=target_directive) """ return partial(directive, target_directive=target_directive) graphene-directives-0.4.6/graphene_directives/exceptions.py000066400000000000000000000013701455127165500242320ustar00rootroot00000000000000from graphql import GraphQLDirective class DirectiveValidationError(Exception): def __init__(self, message: str): super().__init__(message) class DirectiveCustomValidationError(Exception): def __init__(self, message: str): super().__init__(message) class DirectiveInvalidTypeError(Exception): def __init__(self, directive: GraphQLDirective): message = f"Directive {str(directive)} must be build from CustomDirective(...)" super().__init__(message) class DirectiveInvalidArgTypeError(Exception): def __init__(self, message: str): super().__init__(message) class DirectiveInvalidArgValueTypeError(Exception): def __init__(self, errors: list[str]): super().__init__("\n".join(errors)) graphene-directives-0.4.6/graphene_directives/main.py000066400000000000000000000071741455127165500230050ustar00rootroot00000000000000from typing import Collection, Type, Union import graphene from graphene import Schema as GrapheneSchema from graphql import GraphQLDirective from graphql import specified_directives from . import DirectiveValidationError from .data_models import SchemaDirective from .schema import Schema def build_schema( query: Union[graphene.ObjectType, Type[graphene.ObjectType]] = None, mutation: Union[graphene.ObjectType, Type[graphene.ObjectType]] = None, subscription: Union[graphene.ObjectType, Type[graphene.ObjectType]] = None, types: Collection[Union[graphene.ObjectType, Type[graphene.ObjectType]]] = None, directives: Union[Collection[GraphQLDirective], None] = None, auto_camelcase: bool = True, schema_directives: Collection[SchemaDirective] = None, include_graphql_spec_directives: bool = True, ) -> GrapheneSchema: """ Build Schema. Args: query (Type[ObjectType]): Root query *ObjectType*. Describes entry point for fields to *read* data in your Schema. mutation (Optional[Type[ObjectType]]): Root mutation *ObjectType*. Describes entry point for fields to *create, update or delete* data in your API. subscription (Optional[Type[ObjectType]]): Root subscription *ObjectType*. Describes entry point for fields to receive continuous updates. types (Optional[Collection[Type[ObjectType]]]): List of any types to include in schema that may not be introspected through root types. directives (List[GraphQLDirective], optional): List of custom directives to include in the GraphQL schema. auto_camelcase (bool): Fieldnames will be transformed in Schema's TypeMap from snake_case to camelCase (preferred by GraphQL standard). Default True. schema_directives (Collection[SchemaDirective]): Directives that can be defined at DIRECTIVE_LOCATION.SCHEMA with their argument values. include_graphql_spec_directives (bool): Includes directives defined by GraphQL spec (@include, @skip, @deprecated, @specifiedBy) """ _schema_directive_set: set[str] = set() for schema_directive in schema_directives or []: if schema_directive.target_directive.name in _schema_directive_set: if not schema_directive.target_directive.is_repeatable: raise DirectiveValidationError( f"{schema_directive.target_directive} is not repeatable on schema" ) else: _schema_directive_set.add(schema_directive.target_directive.name) directives = list(directives) if directives is not None else [] _directive_set: set[str] = set() for directive in directives: if directive.name in _directive_set: raise DirectiveValidationError(f"Duplicate {directive} found") _directive_set.add(directive.name) # Validate if custom directive conflicts with graphql spec default directives _duplicate_default_directives = _directive_set.intersection( {directive.name for directive in specified_directives} ) if _duplicate_default_directives: formatted_directive_str = [f"@{str(i)}" for i in _duplicate_default_directives] raise DirectiveValidationError( f"{formatted_directive_str} are reserved directives for client queries." ) return Schema( query=query, mutation=mutation, subscription=subscription, types=types, directives=directives, auto_camelcase=auto_camelcase, include_graphql_spec_directives=include_graphql_spec_directives, schema_directives=schema_directives, ) graphene-directives-0.4.6/graphene_directives/parsers.py000066400000000000000000000117101455127165500235270ustar00rootroot00000000000000import json import re from typing import Any, Collection, Dict, Union, cast from graphene.utils.str_converters import to_camel_case, to_snake_case from graphql import ( GraphQLDirective, GraphQLEnumType, GraphQLError, GraphQLInputObjectType, GraphQLInputType, GraphQLInterfaceType, GraphQLObjectType, is_non_null_type, value_from_ast, ) from graphql.pyutils import inspect, print_path_list from graphql.utilities import coerce_input_value from graphql.utilities.print_schema import ( print_description, print_enum, print_fields, print_input_object, ) from .data_models import SchemaDirective from .exceptions import DirectiveInvalidArgValueTypeError def _remove_block(str_fields: str) -> str: # Remove blocks added by `print_block` block_match = re.match(r" \{\n(?P.*)\n}", str_fields, flags=re.DOTALL) if block_match: str_fields = block_match.groups()[0] return str_fields def entity_type_to_fields_string( field: Union[GraphQLObjectType, GraphQLInterfaceType], ) -> str: return _remove_block(print_fields(field)) def enum_type_to_fields_string(graphene_type: GraphQLEnumType) -> str: fields = print_enum(graphene_type).replace( print_description(graphene_type) + f"enum {graphene_type.name}", "" ) return _remove_block(fields) def input_type_to_fields_string(graphene_type: GraphQLInputObjectType) -> str: fields = print_input_object(graphene_type).replace( print_description(graphene_type) + f"input {graphene_type.name}", "" ) return _remove_block(fields) def decorator_string(directive: GraphQLDirective, **kwargs: dict) -> str: directive_name = str(directive) if len(directive.args) == 0: return directive_name # Format each keyword argument as a string, considering its type formatted_args = [ ( f"{to_camel_case(key)}: " + (f'"{value}"' if isinstance(value, str) else json.dumps(value)) ) for key, value in kwargs.items() if value is not None and to_camel_case(key) in directive.args ] # Construct the directive string return f"{directive_name}({', '.join(formatted_args)})" def extend_schema_string( string_schema: str, schema_directives: Collection[SchemaDirective] ) -> str: schema_directives_strings = [] for schema_directive in schema_directives: args = parse_argument_values( schema_directive.target_directive, { to_camel_case(field): value for (field, value) in schema_directive.arguments.items() }, ) schema_directives_strings.append( "\t" + decorator_string(schema_directive.target_directive, **args) ) if len(schema_directives_strings) != 0: string_schema += ( "extend schema\n" + "\n".join(schema_directives_strings) + "\n\n" ) return string_schema def parse_argument_values( directive: GraphQLDirective, inputs: Dict[str, Any] ) -> dict[str, Any]: coerced_values: Dict[str, Any] = {} errors = [] for var_name, var_arg_type in directive.args.items(): var_type = var_arg_type.type snake_cased_var_name = to_snake_case(var_name) var_type = cast(GraphQLInputType, var_type) if var_name not in inputs: if var_arg_type.default_value: coerced_values[var_name] = value_from_ast( var_arg_type.default_value, var_type ) elif is_non_null_type(var_type): var_type_str = inspect(var_type) errors.append( f"Variable '{snake_cased_var_name}' of required type '{var_type_str}'" " was not provided." ) continue value = inputs[var_name] if value is None and is_non_null_type(var_type): var_type_str = inspect(var_type) errors.append( f"Variable '{snake_cased_var_name}' of non-null type '{var_type_str}'" " must not be null." ) continue def on_input_value_error( path: list[Union[str, int]], invalid_value: Any, error: GraphQLError ) -> None: invalid_str = inspect(invalid_value) prefix = ( f"Variable '{snake_cased_var_name}' got invalid value {invalid_str}" ) if path: prefix += f" at '{snake_cased_var_name}{print_path_list(path)}'" errors.append(prefix + "; " + error.message) coerced_values[var_name] = coerce_input_value( value, var_type, on_input_value_error ) if errors: raise DirectiveInvalidArgValueTypeError(errors=errors) return coerced_values def arg_camel_case(inputs: dict) -> dict: return {to_camel_case(k): v for k, v in inputs.items()} def arg_snake_case(inputs: dict) -> dict: return {to_snake_case(k): v for k, v in inputs.items()} graphene-directives-0.4.6/graphene_directives/schema.py000066400000000000000000000603611455127165500233160ustar00rootroot00000000000000import re from typing import Callable, Collection, Union import graphene from graphene import Schema as GrapheneSchema from graphene.types.scalars import ScalarOptions from graphene.types.union import UnionOptions from graphene.utils.str_converters import to_camel_case, to_snake_case from graphql import ( DirectiveLocation, GraphQLArgument, GraphQLDirective, GraphQLField, GraphQLInputField, GraphQLNamedType, is_enum_type, is_input_type, is_interface_type, is_object_type, is_scalar_type, is_union_type, print_schema, ) from graphql import specified_directives from graphql.utilities.print_schema import ( print_args, print_description, print_directive, print_input_value, ) from .data_models.schema_directive import SchemaDirective from .directive import CustomDirectiveMeta from .exceptions import DirectiveCustomValidationError, DirectiveValidationError from .parsers import ( arg_camel_case, arg_snake_case, decorator_string, entity_type_to_fields_string, enum_type_to_fields_string, extend_schema_string, input_type_to_fields_string, ) from .utils import ( get_field_attribute_value, get_non_field_attribute_value, get_single_field_type, has_field_attribute, has_non_field_attribute, ) class Schema(GrapheneSchema): def __init__( self, query: graphene.ObjectType = None, mutation: graphene.ObjectType = None, subscription: graphene.ObjectType = None, types: list[graphene.ObjectType] = None, directives: Union[Collection[GraphQLDirective], None] = None, auto_camelcase: bool = True, schema_directives: Collection[SchemaDirective] = None, include_graphql_spec_directives: bool = True, ): """ Schema Definition. Args: query (Type[ObjectType]): Root query *ObjectType*. Describes entry point for fields to *read* data in your Schema. mutation (Optional[Type[ObjectType]]): Root mutation *ObjectType*. Describes entry point for fields to *create, update or delete* data in your API. subscription (Optional[Type[ObjectType]]): Root subscription *ObjectType*. Describes entry point for fields to receive continuous updates. types (Optional[Collection[Type[ObjectType]]]): List of any types to include in schema that may not be introspected through root types. directives (List[GraphQLDirective], optional): List of custom directives to include in the GraphQL schema. auto_camelcase (bool): Fieldnames will be transformed in Schema's TypeMap from snake_case to camelCase (preferred by GraphQL standard). Default True. schema_directives (Collection[SchemaDirective]): Directives that can be defined at DIRECTIVE_LOCATION.SCHEMA with their argument values. include_graphql_spec_directives (bool): Includes directives defined by GraphQL spec (@include, @skip, @deprecated, @specifiedBy) """ self.custom_directives = directives or [] self.schema_directives = schema_directives or [] self.auto_camelcase = auto_camelcase self.directives_used: dict[str, GraphQLDirective] = {} directives = tuple(self.custom_directives) + ( tuple(specified_directives) if include_graphql_spec_directives else () ) super().__init__( query=query, mutation=mutation, subscription=subscription, types=types, directives=directives, auto_camelcase=auto_camelcase, ) def field_name_to_type_attribute( self, model: graphene.ObjectType ) -> Callable[[str], str]: """ Create field name conversion method (from schema name to actual graphene_type attribute name). Args: model (ObjectType): model whose field name is to be converted Returns: (str) -> (str) """ field_names = {} if self.auto_camelcase: field_names = { to_camel_case(attr_name): attr_name for attr_name in getattr(model._meta, "fields", []) # noqa } return lambda schema_field_name: field_names.get( schema_field_name, schema_field_name ) def type_attribute_to_field_name(self, attribute: str) -> str: """ Create a conversion method to convert from graphene_type attribute name to the schema field name. """ if self.auto_camelcase: return to_camel_case(attribute) return attribute def _add_argument_decorators( self, entity_name: str, required_directive_field_types: set[DirectiveLocation], args: dict[str, GraphQLArgument], ) -> str: """ For a given field, go through all its args and see if any directive decorator needs to be added. """ if not args: return "" # If every arg does not have a description, print them on one line. print_single_line = not any(arg.description for arg in args.values()) indentation: str = " " new_args = [] str_field = "(" if print_single_line else "(\n" for i, (name, arg) in enumerate(args.items()): if print_single_line: base_str = f"{print_input_value(name, arg)} " else: base_str = ( print_description(arg, f" {indentation}", not i) + f" {indentation}" + f"{print_input_value(name, arg)} " ) directives = [] for directive in self.custom_directives: if has_field_attribute(arg, directive): directive_values = get_field_attribute_value(arg, directive) meta_data: CustomDirectiveMeta = getattr( directive, "_graphene_directive" ) if ( not required_directive_field_types.intersection( set(directive.locations) ) and len(required_directive_field_types) != 0 ): raise DirectiveValidationError( "\n".join( [ f"{str(directive)} cannot be used at argument {name} level", f"\tat {entity_name}", f"\tallowed: {directive.locations}", f"\trequired: {required_directive_field_types}", ] ) ) for directive_value in directive_values: if meta_data.input_transform is not None: directive_value = arg_camel_case( meta_data.input_transform( arg_snake_case(directive_value), self ) ) directive_str = decorator_string(directive, **directive_value) directives.append(directive_str) new_args.append(base_str + " ".join(directives)) if print_single_line: str_field += ", ".join(new_args) + ")" else: str_field += "\n".join(new_args) + f"\n{indentation})" return str_field def _add_field_decorators(self, graphene_types: set, string_schema: str) -> str: """ For a given entity, go through all its fields and see if any directive decorator needs to be added. This method simply goes through the fields that need to be modified and replace them with their annotated version in the schema string representation. """ for graphene_type in graphene_types: entity_name = graphene_type._meta.name # noqa entity_type = self.graphql_schema.get_type(entity_name) get_field_graphene_type = self.field_name_to_type_attribute(graphene_type) required_directive_locations = set() if is_object_type(entity_type) or is_interface_type(entity_type): required_directive_locations.union( { DirectiveLocation.FIELD_DEFINITION, DirectiveLocation.ARGUMENT_DEFINITION, } ) elif is_enum_type(entity_type): required_directive_locations.add(DirectiveLocation.ENUM_VALUE) elif is_input_type(entity_type): required_directive_locations.add( DirectiveLocation.INPUT_FIELD_DEFINITION ) else: continue if is_enum_type(entity_type): fields: dict = entity_type.values else: fields: dict = entity_type.fields str_fields = [] for field_name, field in fields.items(): if is_enum_type(entity_type): str_field = enum_type_to_fields_string( get_single_field_type( entity_type, field_name, field, is_enum_type=True ) ) elif isinstance(field, GraphQLInputField): str_field = input_type_to_fields_string( get_single_field_type(entity_type, field_name, field) ) elif isinstance(field, GraphQLField): str_field = entity_type_to_fields_string( get_single_field_type(entity_type, field_name, field) ) # Replace Arguments with directives if hasattr(entity_type, "_fields"): _arg = entity_type._fields.args[0] # noqa if hasattr(_arg, self.type_attribute_to_field_name(field_name)): arg_field = getattr( _arg, self.type_attribute_to_field_name(field_name) ) else: arg_field = {} if ( hasattr(arg_field, "args") and arg_field.args is not None and isinstance(arg_field.args, dict) ): original_args = print_args( args=field.args, indentation=" " ) replacement_args = self._add_argument_decorators( entity_name=entity_name, required_directive_field_types=required_directive_locations, args=arg_field.args, ) str_field = str_field.replace( original_args, replacement_args ) else: continue # Check if we need to annotate the field by checking if it has the decorator attribute set on the field. field = getattr( graphene_type, get_field_graphene_type(field_name), None ) if field is None: # Append the string, but skip the directives str_fields.append(str_field) continue for directive in self.custom_directives: if not has_field_attribute(field, directive): continue directive_values = get_field_attribute_value(field, directive) meta_data: CustomDirectiveMeta = getattr( directive, "_graphene_directive" ) if ( not required_directive_locations.intersection( set(directive.locations) ) and len(required_directive_locations) != 0 ): raise DirectiveValidationError( "\n".join( [ f"{str(directive)} cannot be used at field level", f"\tat {entity_name}", f"\tallowed: {directive.locations}", f"\trequired: {required_directive_locations}", ] ) ) for directive_value in directive_values: if ( meta_data.field_validator is not None and not meta_data.field_validator( entity_type, field, arg_snake_case(directive_value), self, ) ): raise DirectiveCustomValidationError( ", ".join( [ f"Custom Validation Failed for {str(directive)} with args: ({directive_value})" f"at field level {entity_name}:{field}" ] ) ) if meta_data.input_transform is not None: directive_value = arg_camel_case( meta_data.input_transform( arg_snake_case(directive_value), self ) ) str_field += ( f" {decorator_string(directive, **directive_value)}" ) str_fields.append(str_field) str_fields_annotated = "\n".join(str_fields) # Replace the original field declaration by the annotated one if is_object_type(entity_type): entity_type_name = "type" str_fields_original = entity_type_to_fields_string(entity_type) elif is_interface_type(entity_type): entity_type_name = "interface" str_fields_original = entity_type_to_fields_string(entity_type) elif is_enum_type(entity_type): entity_type_name = "enum" str_fields_original = enum_type_to_fields_string(entity_type) elif is_input_type(entity_type): entity_type_name = "input" str_fields_original = input_type_to_fields_string(entity_type) else: continue pattern = re.compile( r"(%s\s%s\s[^\{]*)\{\s*%s\s*\}" # noqa % (entity_type_name, entity_name, re.escape(str_fields_original)) ) string_schema = pattern.sub( r"\g<1> {\n%s\n}" % str_fields_annotated, string_schema ) return string_schema def add_non_field_decorators( self, non_fields_type: set[GraphQLNamedType], string_schema: str ) -> str: for non_field in non_fields_type: entity_name = non_field._meta.name # noqa entity_type = self.graphql_schema.get_type(entity_name) required_directive_locations = set() if is_scalar_type(entity_type): non_field_pattern = rf"(scalar {entity_name})" required_directive_locations.add(DirectiveLocation.SCALAR) elif is_union_type(entity_type): non_field_pattern = rf"(union {entity_name} )" required_directive_locations.add(DirectiveLocation.UNION) elif is_object_type(entity_type): non_field_pattern = rf"(type {entity_name} [^\{{]*)" required_directive_locations.add(DirectiveLocation.OBJECT) elif is_interface_type(entity_type): non_field_pattern = rf"(interface {entity_name} [^\{{]*)" required_directive_locations.add(DirectiveLocation.INTERFACE) elif is_enum_type(entity_type): non_field_pattern = rf"(enum {entity_name} [^\{{]*)" required_directive_locations.add(DirectiveLocation.ENUM) elif is_input_type(entity_type): non_field_pattern = rf"(input {entity_name} [^\{{]*)" required_directive_locations.add(DirectiveLocation.INPUT_OBJECT) else: continue directive_annotations = [] for directive in self.custom_directives: if has_non_field_attribute(non_field, directive): meta_data: CustomDirectiveMeta = getattr( directive, "_graphene_directive" ) directive_values = get_non_field_attribute_value( non_field, directive ) if ( not required_directive_locations.intersection( set(directive.locations) ) and len(required_directive_locations) != 0 ): raise DirectiveValidationError( "\n".join( [ f"{str(directive)} cannot be used at non field level", f"\tat {entity_name}", f"\tallowed: {directive.locations}", f"\trequired: {required_directive_locations}", ] ) ) for directive_value in directive_values: if ( meta_data.non_field_validator is not None and not meta_data.non_field_validator( non_field, arg_snake_case(directive_value), self ) ): raise DirectiveCustomValidationError( ", ".join( [ f"Custom Validation Failed for {str(directive)} with args: ({directive_value})" f"at non-field level {entity_name}" ] ) ) if meta_data.input_transform is not None: directive_value = arg_camel_case( meta_data.input_transform( arg_snake_case(directive_value), self ) ) directive_annotations.append( f"{decorator_string(directive, **directive_value)}" ) annotation = " ".join(directive_annotations) annotation = ( f" {annotation}" if is_scalar_type(entity_type) else f"{annotation} " ) replace_str = rf"\1{annotation}" pattern = re.compile(non_field_pattern) string_schema = pattern.sub(replace_str, string_schema) return string_schema def _get_directive_applied_non_field_types(self) -> set: """ Find all the directive applied non-field types from the schema. """ directives_types = set() schema_types = { **self.graphql_schema.type_map, **{ "Query": self.graphql_schema.query_type, "Mutation": self.graphql_schema.mutation_type, }, } for schema_type in schema_types.values(): if not hasattr(schema_type, "graphene_type"): continue for directive in self.custom_directives: if has_non_field_attribute(schema_type.graphene_type, directive): self.directives_used[directive.name] = directive directives_types.add(schema_type.graphene_type) return directives_types def _get_directive_applied_field_types(self) -> set: """ Find all the directive applied field types from the schema. """ directives_fields = set() schema_types = { **self.graphql_schema.type_map, **{ "Query": self.graphql_schema.query_type, # noqa "Mutation": self.graphql_schema.mutation_type, # noqa }, } for _, entity_type in schema_types.items(): if ( not hasattr(entity_type, "graphene_type") # noqa:SIM101 or isinstance(entity_type.graphene_type._meta, UnionOptions) # noqa or isinstance(entity_type.graphene_type._meta, ScalarOptions) # noqa ): continue fields = ( list(entity_type.values.values()) # Enum class fields if is_enum_type(entity_type) else list(entity_type.fields) # noqa ) for field in fields: field_type = ( # auto-camelcasing can cause problems getattr(entity_type.graphene_type, to_camel_case(field), None) or getattr(entity_type.graphene_type, to_snake_case(field), None) if not is_enum_type(entity_type) else field.value ) for directive_ in self.custom_directives: if has_field_attribute(field_type, directive_): self.directives_used[directive_.name] = directive_ directives_fields.add(entity_type.graphene_type) # Handle Argument Decorators if ( hasattr(field_type, "args") and field_type.args is not None and isinstance(field_type.args, dict) ): for arg_name, arg_type in field_type.args.items(): if has_field_attribute(arg_type, directive_): if ( DirectiveLocation.ARGUMENT_DEFINITION not in directive_.locations ): raise DirectiveValidationError( f"{directive_} cannot be used at argument level at {entity_type}->{field}" ) self.directives_used[directive_.name] = directive_ directives_fields.add(entity_type.graphene_type) return directives_fields def get_directives_used(self) -> list[GraphQLDirective]: """ Returns a list of directives used in the schema """ self._get_directive_applied_field_types() self._get_directive_applied_non_field_types() return list(self.directives_used.values()) def __str__(self): string_schema = "" string_schema += extend_schema_string(string_schema, self.schema_directives) string_schema += print_schema(self.graphql_schema) field_types = self._get_directive_applied_field_types() non_field_types = self._get_directive_applied_non_field_types() string_schema = self._add_field_decorators(field_types, string_schema) string_schema = self.add_non_field_decorators(non_field_types, string_schema) for directive in self.custom_directives: meta_data: CustomDirectiveMeta = getattr(directive, "_graphene_directive") if not meta_data.add_definition_to_schema: string_schema = string_schema.replace( print_directive(directive) + "\n\n", "" ) return string_schema.strip() graphene-directives-0.4.6/graphene_directives/utils.py000066400000000000000000000046241455127165500232160ustar00rootroot00000000000000from copy import deepcopy from typing import Any from typing import Union from graphql import ( GraphQLDirective, GraphQLEnumType, GraphQLField, GraphQLInputField, GraphQLInputObjectType, GraphQLObjectType, ) from .exceptions import DirectiveValidationError # ruff: noqa: ANN401 def get_single_field_type( entity: Union[GraphQLEnumType, GraphQLInputObjectType, GraphQLObjectType], field_name: str, field_type: Union[GraphQLInputField, GraphQLField], is_enum_type: bool = False, ) -> Union[GraphQLEnumType, GraphQLInputObjectType, GraphQLObjectType]: """ Generates the schema for a type with just one given field """ new_entity = deepcopy(entity) setattr( new_entity, "values" if is_enum_type else "fields", {field_name: field_type} ) return new_entity def field_attribute_name(target_directive: GraphQLDirective) -> str: return f"_directive_{target_directive.name}_field" def non_field_attribute_name(target_directive: GraphQLDirective) -> str: return f"_directive_{target_directive.name}_non_field" def has_field_attribute(type_: Any, target_directive: GraphQLDirective) -> bool: return hasattr(type_, field_attribute_name(target_directive)) def has_non_field_attribute(type_: Any, target_directive: GraphQLDirective) -> bool: return hasattr(type_, non_field_attribute_name(target_directive)) def set_attribute_value( type_: Any, attribute_name: str, target_directive: GraphQLDirective, data: dict ) -> None: if hasattr(type_, attribute_name): if not target_directive.is_repeatable: raise DirectiveValidationError( f"{target_directive} is not repeatable, at: {type_}" ) kwargs_list: list = getattr(type_, attribute_name) for prev_data in kwargs_list: if prev_data == data: raise DirectiveValidationError( f"{target_directive} is got duplicate values {data}, at: {type_}" ) kwargs_list.append(data) else: setattr(type_, attribute_name, [data]) def get_field_attribute_value( type_: Any, target_directive: GraphQLDirective ) -> list[dict]: return getattr(type_, field_attribute_name(target_directive)) def get_non_field_attribute_value( type_: Any, target_directive: GraphQLDirective ) -> list[dict]: return getattr(type_, non_field_attribute_name(target_directive)) graphene-directives-0.4.6/noxfile.py000066400000000000000000000004511455127165500175020ustar00rootroot00000000000000from nox import parametrize, session @session(python=["3.9", "3.10", "3.11", "3.12"]) @parametrize("graphene", ("3.0", "3.1", "3.2", "3.3")) def tests(session, graphene): # noqa session.install(f"graphene=={graphene}") session.install("pytest", ".") session.run("pytest", "tests/") graphene-directives-0.4.6/poetry.lock000066400000000000000000000741071455127165500176710ustar00rootroot00000000000000# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. [[package]] name = "aniso8601" version = "9.0.1" description = "A library for parsing ISO 8601 strings." optional = false python-versions = "*" files = [ {file = "aniso8601-9.0.1-py2.py3-none-any.whl", hash = "sha256:1d2b7ef82963909e93c4f24ce48d4de9e66009a21bf1c1e1c85bdd0812fe412f"}, {file = "aniso8601-9.0.1.tar.gz", hash = "sha256:72e3117667eedf66951bb2d93f4296a56b94b078a8a95905a052611fb3f1b973"}, ] [package.extras] dev = ["black", "coverage", "isort", "pre-commit", "pyenchant", "pylint"] [[package]] name = "argcomplete" version = "3.2.1" description = "Bash tab completion for argparse" optional = false python-versions = ">=3.8" files = [ {file = "argcomplete-3.2.1-py3-none-any.whl", hash = "sha256:30891d87f3c1abe091f2142613c9d33cac84a5e15404489f033b20399b691fec"}, {file = "argcomplete-3.2.1.tar.gz", hash = "sha256:437f67fb9b058da5a090df505ef9be0297c4883993f3f56cb186ff087778cfb4"}, ] [package.extras] test = ["coverage", "mypy", "pexpect", "ruff", "wheel"] [[package]] name = "bandit" version = "1.7.6" description = "Security oriented static analyser for python code." optional = false python-versions = ">=3.8" files = [ {file = "bandit-1.7.6-py3-none-any.whl", hash = "sha256:36da17c67fc87579a5d20c323c8d0b1643a890a2b93f00b3d1229966624694ff"}, {file = "bandit-1.7.6.tar.gz", hash = "sha256:72ce7bc9741374d96fb2f1c9a8960829885f1243ffde743de70a19cee353e8f3"}, ] [package.dependencies] colorama = {version = ">=0.3.9", markers = "platform_system == \"Windows\""} GitPython = ">=3.1.30" PyYAML = ">=5.3.1" rich = "*" stevedore = ">=1.20.0" [package.extras] test = ["beautifulsoup4 (>=4.8.0)", "coverage (>=4.5.4)", "fixtures (>=3.0.0)", "flake8 (>=4.0.0)", "pylint (==1.9.4)", "stestr (>=2.5.0)", "testscenarios (>=0.5.0)", "testtools (>=2.3.0)", "tomli (>=1.1.0)"] toml = ["tomli (>=1.1.0)"] yaml = ["PyYAML"] [[package]] name = "cfgv" version = "3.4.0" description = "Validate configuration and produce human readable error messages." optional = false python-versions = ">=3.8" files = [ {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, ] [[package]] name = "colorama" version = "0.4.6" description = "Cross-platform colored terminal text." optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] [[package]] name = "colorlog" version = "6.8.0" description = "Add colours to the output of Python's logging module." optional = false python-versions = ">=3.6" files = [ {file = "colorlog-6.8.0-py3-none-any.whl", hash = "sha256:4ed23b05a1154294ac99f511fabe8c1d6d4364ec1f7fc989c7fb515ccc29d375"}, {file = "colorlog-6.8.0.tar.gz", hash = "sha256:fbb6fdf9d5685f2517f388fb29bb27d54e8654dd31f58bc2a3b217e967a95ca6"}, ] [package.dependencies] colorama = {version = "*", markers = "sys_platform == \"win32\""} [package.extras] development = ["black", "flake8", "mypy", "pytest", "types-colorama"] [[package]] name = "distlib" version = "0.3.8" description = "Distribution utilities" optional = false python-versions = "*" files = [ {file = "distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784"}, {file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"}, ] [[package]] name = "exceptiongroup" version = "1.2.0" description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" files = [ {file = "exceptiongroup-1.2.0-py3-none-any.whl", hash = "sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14"}, {file = "exceptiongroup-1.2.0.tar.gz", hash = "sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68"}, ] [package.extras] test = ["pytest (>=6)"] [[package]] name = "filelock" version = "3.13.1" description = "A platform independent file lock." optional = false python-versions = ">=3.8" files = [ {file = "filelock-3.13.1-py3-none-any.whl", hash = "sha256:57dbda9b35157b05fb3e58ee91448612eb674172fab98ee235ccb0b5bee19a1c"}, {file = "filelock-3.13.1.tar.gz", hash = "sha256:521f5f56c50f8426f5e03ad3b281b490a87ef15bc6c526f168290f0c7148d44e"}, ] [package.extras] docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.24)"] testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)"] typing = ["typing-extensions (>=4.8)"] [[package]] name = "gitdb" version = "4.0.11" description = "Git Object Database" optional = false python-versions = ">=3.7" files = [ {file = "gitdb-4.0.11-py3-none-any.whl", hash = "sha256:81a3407ddd2ee8df444cbacea00e2d038e40150acfa3001696fe0dcf1d3adfa4"}, {file = "gitdb-4.0.11.tar.gz", hash = "sha256:bf5421126136d6d0af55bc1e7c1af1c397a34f5b7bd79e776cd3e89785c2b04b"}, ] [package.dependencies] smmap = ">=3.0.1,<6" [[package]] name = "gitpython" version = "3.1.41" description = "GitPython is a Python library used to interact with Git repositories" optional = false python-versions = ">=3.7" files = [ {file = "GitPython-3.1.41-py3-none-any.whl", hash = "sha256:c36b6634d069b3f719610175020a9aed919421c87552185b085e04fbbdb10b7c"}, {file = "GitPython-3.1.41.tar.gz", hash = "sha256:ed66e624884f76df22c8e16066d567aaa5a37d5b5fa19db2c6df6f7156db9048"}, ] [package.dependencies] gitdb = ">=4.0.1,<5" [package.extras] test = ["black", "coverage[toml]", "ddt (>=1.1.1,!=1.4.3)", "mock", "mypy", "pre-commit", "pytest (>=7.3.1)", "pytest-cov", "pytest-instafail", "pytest-mock", "pytest-sugar", "sumtypes"] [[package]] name = "graphene" version = "3.3" description = "GraphQL Framework for Python" optional = false python-versions = "*" files = [ {file = "graphene-3.3-py2.py3-none-any.whl", hash = "sha256:bb3810be33b54cb3e6969506671eb72319e8d7ba0d5ca9c8066472f75bf35a38"}, {file = "graphene-3.3.tar.gz", hash = "sha256:529bf40c2a698954217d3713c6041d69d3f719ad0080857d7ee31327112446b0"}, ] [package.dependencies] aniso8601 = ">=8,<10" graphql-core = ">=3.1,<3.3" graphql-relay = ">=3.1,<3.3" [package.extras] dev = ["black (==22.3.0)", "coveralls (>=3.3,<4)", "flake8 (>=4,<5)", "iso8601 (>=1,<2)", "mock (>=4,<5)", "pytest (>=6,<7)", "pytest-asyncio (>=0.16,<2)", "pytest-benchmark (>=3.4,<4)", "pytest-cov (>=3,<4)", "pytest-mock (>=3,<4)", "pytz (==2022.1)", "snapshottest (>=0.6,<1)"] test = ["coveralls (>=3.3,<4)", "iso8601 (>=1,<2)", "mock (>=4,<5)", "pytest (>=6,<7)", "pytest-asyncio (>=0.16,<2)", "pytest-benchmark (>=3.4,<4)", "pytest-cov (>=3,<4)", "pytest-mock (>=3,<4)", "pytz (==2022.1)", "snapshottest (>=0.6,<1)"] [[package]] name = "graphql-core" version = "3.2.3" description = "GraphQL implementation for Python, a port of GraphQL.js, the JavaScript reference implementation for GraphQL." optional = false python-versions = ">=3.6,<4" files = [ {file = "graphql-core-3.2.3.tar.gz", hash = "sha256:06d2aad0ac723e35b1cb47885d3e5c45e956a53bc1b209a9fc5369007fe46676"}, {file = "graphql_core-3.2.3-py3-none-any.whl", hash = "sha256:5766780452bd5ec8ba133f8bf287dc92713e3868ddd83aee4faab9fc3e303dc3"}, ] [[package]] name = "graphql-relay" version = "3.2.0" description = "Relay library for graphql-core" optional = false python-versions = ">=3.6,<4" files = [ {file = "graphql-relay-3.2.0.tar.gz", hash = "sha256:1ff1c51298356e481a0be009ccdff249832ce53f30559c1338f22a0e0d17250c"}, {file = "graphql_relay-3.2.0-py3-none-any.whl", hash = "sha256:c9b22bd28b170ba1fe674c74384a8ff30a76c8e26f88ac3aa1584dd3179953e5"}, ] [package.dependencies] graphql-core = ">=3.2,<3.3" [[package]] name = "identify" version = "2.5.33" description = "File identification library for Python" optional = false python-versions = ">=3.8" files = [ {file = "identify-2.5.33-py2.py3-none-any.whl", hash = "sha256:d40ce5fcd762817627670da8a7d8d8e65f24342d14539c59488dc603bf662e34"}, {file = "identify-2.5.33.tar.gz", hash = "sha256:161558f9fe4559e1557e1bff323e8631f6a0e4837f7497767c1782832f16b62d"}, ] [package.extras] license = ["ukkonen"] [[package]] name = "iniconfig" version = "2.0.0" description = "brain-dead simple config-ini parsing" optional = false python-versions = ">=3.7" files = [ {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, ] [[package]] name = "markdown-it-py" version = "3.0.0" description = "Python port of markdown-it. Markdown parsing, done right!" optional = false python-versions = ">=3.8" files = [ {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, ] [package.dependencies] mdurl = ">=0.1,<1.0" [package.extras] benchmarking = ["psutil", "pytest", "pytest-benchmark"] code-style = ["pre-commit (>=3.0,<4.0)"] compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"] linkify = ["linkify-it-py (>=1,<3)"] plugins = ["mdit-py-plugins"] profiling = ["gprof2dot"] rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] [[package]] name = "mdurl" version = "0.1.2" description = "Markdown URL utilities" optional = false python-versions = ">=3.7" files = [ {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, ] [[package]] name = "nodeenv" version = "1.8.0" description = "Node.js virtual environment builder" optional = false python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" files = [ {file = "nodeenv-1.8.0-py2.py3-none-any.whl", hash = "sha256:df865724bb3c3adc86b3876fa209771517b0cfe596beff01a92700e0e8be4cec"}, {file = "nodeenv-1.8.0.tar.gz", hash = "sha256:d51e0c37e64fbf47d017feac3145cdbb58836d7eee8c6f6d3b6880c5456227d2"}, ] [package.dependencies] setuptools = "*" [[package]] name = "nox" version = "2023.4.22" description = "Flexible test automation." optional = false python-versions = ">=3.7" files = [ {file = "nox-2023.4.22-py3-none-any.whl", hash = "sha256:0b1adc619c58ab4fa57d6ab2e7823fe47a32e70202f287d78474adcc7bda1891"}, {file = "nox-2023.4.22.tar.gz", hash = "sha256:46c0560b0dc609d7d967dc99e22cb463d3c4caf54a5fda735d6c11b5177e3a9f"}, ] [package.dependencies] argcomplete = ">=1.9.4,<4.0" colorlog = ">=2.6.1,<7.0.0" packaging = ">=20.9" virtualenv = ">=14" [package.extras] tox-to-nox = ["jinja2", "tox (<4)"] [[package]] name = "packaging" version = "23.2" description = "Core utilities for Python packages" optional = false python-versions = ">=3.7" files = [ {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"}, {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, ] [[package]] name = "pbr" version = "6.0.0" description = "Python Build Reasonableness" optional = false python-versions = ">=2.6" files = [ {file = "pbr-6.0.0-py2.py3-none-any.whl", hash = "sha256:4a7317d5e3b17a3dccb6a8cfe67dab65b20551404c52c8ed41279fa4f0cb4cda"}, {file = "pbr-6.0.0.tar.gz", hash = "sha256:d1377122a5a00e2f940ee482999518efe16d745d423a670c27773dfbc3c9a7d9"}, ] [[package]] name = "platformdirs" version = "4.1.0" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." optional = false python-versions = ">=3.8" files = [ {file = "platformdirs-4.1.0-py3-none-any.whl", hash = "sha256:11c8f37bcca40db96d8144522d925583bdb7a31f7b0e37e3ed4318400a8e2380"}, {file = "platformdirs-4.1.0.tar.gz", hash = "sha256:906d548203468492d432bcb294d4bc2fff751bf84971fbb2c10918cc206ee420"}, ] [package.extras] docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.1)", "sphinx-autodoc-typehints (>=1.24)"] test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)"] [[package]] name = "pluggy" version = "1.3.0" description = "plugin and hook calling mechanisms for python" optional = false python-versions = ">=3.8" files = [ {file = "pluggy-1.3.0-py3-none-any.whl", hash = "sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7"}, {file = "pluggy-1.3.0.tar.gz", hash = "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12"}, ] [package.extras] dev = ["pre-commit", "tox"] testing = ["pytest", "pytest-benchmark"] [[package]] name = "pre-commit" version = "3.6.0" description = "A framework for managing and maintaining multi-language pre-commit hooks." optional = false python-versions = ">=3.9" files = [ {file = "pre_commit-3.6.0-py2.py3-none-any.whl", hash = "sha256:c255039ef399049a5544b6ce13d135caba8f2c28c3b4033277a788f434308376"}, {file = "pre_commit-3.6.0.tar.gz", hash = "sha256:d30bad9abf165f7785c15a21a1f46da7d0677cb00ee7ff4c579fd38922efe15d"}, ] [package.dependencies] cfgv = ">=2.0.0" identify = ">=1.0.0" nodeenv = ">=0.11.1" pyyaml = ">=5.1" virtualenv = ">=20.10.0" [[package]] name = "pygments" version = "2.17.2" description = "Pygments is a syntax highlighting package written in Python." optional = false python-versions = ">=3.7" files = [ {file = "pygments-2.17.2-py3-none-any.whl", hash = "sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c"}, {file = "pygments-2.17.2.tar.gz", hash = "sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367"}, ] [package.extras] plugins = ["importlib-metadata"] windows-terminal = ["colorama (>=0.4.6)"] [[package]] name = "pytest" version = "7.4.4" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.7" files = [ {file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"}, {file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"}, ] [package.dependencies] colorama = {version = "*", markers = "sys_platform == \"win32\""} exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} iniconfig = "*" packaging = "*" pluggy = ">=0.12,<2.0" tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} [package.extras] testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] [[package]] name = "pytest-dependency" version = "0.6.0" description = "Manage dependencies of tests" optional = false python-versions = ">=3.4" files = [ {file = "pytest-dependency-0.6.0.tar.gz", hash = "sha256:934b0e6a39d95995062c193f7eaeed8a8ffa06ff1bcef4b62b0dc74a708bacc1"}, ] [package.dependencies] pytest = ">=3.7.0" setuptools = "*" [[package]] name = "pyyaml" version = "6.0.1" description = "YAML parser and emitter for Python" optional = false python-versions = ">=3.6" files = [ {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"}, {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"}, {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"}, {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"}, {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"}, {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"}, {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"}, {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"}, {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"}, {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, ] [[package]] name = "rich" version = "13.7.0" description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" optional = false python-versions = ">=3.7.0" files = [ {file = "rich-13.7.0-py3-none-any.whl", hash = "sha256:6da14c108c4866ee9520bbffa71f6fe3962e193b7da68720583850cd4548e235"}, {file = "rich-13.7.0.tar.gz", hash = "sha256:5cb5123b5cf9ee70584244246816e9114227e0b98ad9176eede6ad54bf5403fa"}, ] [package.dependencies] markdown-it-py = ">=2.2.0" pygments = ">=2.13.0,<3.0.0" [package.extras] jupyter = ["ipywidgets (>=7.5.1,<9)"] [[package]] name = "ruff" version = "0.1.13" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ {file = "ruff-0.1.13-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:e3fd36e0d48aeac672aa850045e784673449ce619afc12823ea7868fcc41d8ba"}, {file = "ruff-0.1.13-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9fb6b3b86450d4ec6a6732f9f60c4406061b6851c4b29f944f8c9d91c3611c7a"}, {file = "ruff-0.1.13-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b13ba5d7156daaf3fd08b6b993360a96060500aca7e307d95ecbc5bb47a69296"}, {file = "ruff-0.1.13-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9ebb40442f7b531e136d334ef0851412410061e65d61ca8ce90d894a094feb22"}, {file = "ruff-0.1.13-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:226b517f42d59a543d6383cfe03cccf0091e3e0ed1b856c6824be03d2a75d3b6"}, {file = "ruff-0.1.13-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:5f0312ba1061e9b8c724e9a702d3c8621e3c6e6c2c9bd862550ab2951ac75c16"}, {file = "ruff-0.1.13-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2f59bcf5217c661254bd6bc42d65a6fd1a8b80c48763cb5c2293295babd945dd"}, {file = "ruff-0.1.13-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e6894b00495e00c27b6ba61af1fc666f17de6140345e5ef27dd6e08fb987259d"}, {file = "ruff-0.1.13-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a1600942485c6e66119da294c6294856b5c86fd6df591ce293e4a4cc8e72989"}, {file = "ruff-0.1.13-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:ee3febce7863e231a467f90e681d3d89210b900d49ce88723ce052c8761be8c7"}, {file = "ruff-0.1.13-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:dcaab50e278ff497ee4d1fe69b29ca0a9a47cd954bb17963628fa417933c6eb1"}, {file = "ruff-0.1.13-py3-none-musllinux_1_2_i686.whl", hash = "sha256:f57de973de4edef3ad3044d6a50c02ad9fc2dff0d88587f25f1a48e3f72edf5e"}, {file = "ruff-0.1.13-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:7a36fa90eb12208272a858475ec43ac811ac37e91ef868759770b71bdabe27b6"}, {file = "ruff-0.1.13-py3-none-win32.whl", hash = "sha256:a623349a505ff768dad6bd57087e2461be8db58305ebd5577bd0e98631f9ae69"}, {file = "ruff-0.1.13-py3-none-win_amd64.whl", hash = "sha256:f988746e3c3982bea7f824c8fa318ce7f538c4dfefec99cd09c8770bd33e6539"}, {file = "ruff-0.1.13-py3-none-win_arm64.whl", hash = "sha256:6bbbc3042075871ec17f28864808540a26f0f79a4478c357d3e3d2284e832998"}, {file = "ruff-0.1.13.tar.gz", hash = "sha256:e261f1baed6291f434ffb1d5c6bd8051d1c2a26958072d38dfbec39b3dda7352"}, ] [[package]] name = "setuptools" version = "69.0.3" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false python-versions = ">=3.8" files = [ {file = "setuptools-69.0.3-py3-none-any.whl", hash = "sha256:385eb4edd9c9d5c17540511303e39a147ce2fc04bc55289c322b9e5904fe2c05"}, {file = "setuptools-69.0.3.tar.gz", hash = "sha256:be1af57fc409f93647f2e8e4573a142ed38724b8cdd389706a867bb4efcf1e78"}, ] [package.extras] docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.1)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] [[package]] name = "smmap" version = "5.0.1" description = "A pure Python implementation of a sliding window memory map manager" optional = false python-versions = ">=3.7" files = [ {file = "smmap-5.0.1-py3-none-any.whl", hash = "sha256:e6d8668fa5f93e706934a62d7b4db19c8d9eb8cf2adbb75ef1b675aa332b69da"}, {file = "smmap-5.0.1.tar.gz", hash = "sha256:dceeb6c0028fdb6734471eb07c0cd2aae706ccaecab45965ee83f11c8d3b1f62"}, ] [[package]] name = "stevedore" version = "5.1.0" description = "Manage dynamic plugins for Python applications" optional = false python-versions = ">=3.8" files = [ {file = "stevedore-5.1.0-py3-none-any.whl", hash = "sha256:8cc040628f3cea5d7128f2e76cf486b2251a4e543c7b938f58d9a377f6694a2d"}, {file = "stevedore-5.1.0.tar.gz", hash = "sha256:a54534acf9b89bc7ed264807013b505bf07f74dbe4bcfa37d32bd063870b087c"}, ] [package.dependencies] pbr = ">=2.0.0,<2.1.0 || >2.1.0" [[package]] name = "tomli" version = "2.0.1" description = "A lil' TOML parser" optional = false python-versions = ">=3.7" files = [ {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, ] [[package]] name = "virtualenv" version = "20.25.0" description = "Virtual Python Environment builder" optional = false python-versions = ">=3.7" files = [ {file = "virtualenv-20.25.0-py3-none-any.whl", hash = "sha256:4238949c5ffe6876362d9c0180fc6c3a824a7b12b80604eeb8085f2ed7460de3"}, {file = "virtualenv-20.25.0.tar.gz", hash = "sha256:bf51c0d9c7dd63ea8e44086fa1e4fb1093a31e963b86959257378aef020e1f1b"}, ] [package.dependencies] distlib = ">=0.3.7,<1" filelock = ">=3.12.2,<4" platformdirs = ">=3.9.1,<5" [package.extras] docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] [metadata] lock-version = "2.0" python-versions = ">=3.9,<4" content-hash = "69f999e5e63b82cb253ae06f04bc6d3345b18fcaff43b3ceb7c117d1a445517d" graphene-directives-0.4.6/pyproject.toml000066400000000000000000000044301455127165500204010ustar00rootroot00000000000000[tool.poetry] name = "graphene-directives" version = "0.4.6" packages = [{include = "graphene_directives"}] description = "Schema Directives implementation for graphene" authors = ["Strollby "] readme = "README.md" license = "MIT" homepage = "https://github.com/strollby/graphene-directives" repository = "https://github.com/strollby/graphene-directives" documentation = "https://github.com/strollby/graphene-directives" keywords = [ "graphene-directives", "graphene", "graphql", "graphql-directives", "graphql-custom-directives", ] classifiers = [ "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12" ] [tool.poetry.dependencies] python = ">=3.9,<4" graphene = ">=3" [tool.poetry.group.dev.dependencies] bandit = "*" pre-commit = "*" pytest = "*" pytest-dependency = "*" ruff = "*" nox = "*" [build-system] requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" # Test Tools [tool.pytest.ini_options] minversion = "6.0" addopts = "-v -s" testpaths = [ "tests/test_cache.py", ] # Linters & Type Checkers [tool.ruff.lint] select = [ # "ALL" "E", # pycodestyle (E) "W", # pycodestyle (W) "F", # Pyflakes (F) "ANN", # flake8-annotations (ANN) "A", # flake8-builtins (A) "C4", # flake8-comprehensions (C4) "UP", # pyupgrade (UP) "Q", # flake8-quotes (Q) "RET", # flake8-return (RET) "SIM", # flake8-simplify (SIM) "ARG", # flake8-unused-arguments (ARG) "ISC", # flake8-implicit-str-concat (ISC) ] ignore = [ "ANN401", # Any "ANN101", # Missing type annotation for self in method "ANN102", # Missing type annotation for cls in classmethod "ANN204", # Missing return type annotation for special method `__init__` "E501", #Line too long (99 > 88) ] [tool.ruff.format] quote-style = "double" # Like Black, use double quotes for strings. indent-style = "space" # Like Black, indent with spaces, rather than tabs. skip-magic-trailing-comma = true line-ending = "auto" # Like Black, automatically detect the appropriate line ending. [tool.ruff.per-file-ignores] "__init__.py" = [ "F401", # imported but unused ] graphene-directives-0.4.6/tests/000077500000000000000000000000001455127165500166265ustar00rootroot00000000000000graphene-directives-0.4.6/tests/schema_files/000077500000000000000000000000001455127165500212505ustar00rootroot00000000000000graphene-directives-0.4.6/tests/schema_files/test_arg_add_definition_to_schema.graphql000066400000000000000000000023711455127165500315050ustar00rootroot00000000000000"""Caching directive to control cache behavior of fields or fragments.""" directive @cache( """Specifies the maximum age for cache in seconds.""" maxAge: Int! """Stale-while-revalidate value in seconds. Optional.""" swr: Int """Scope of the cache. Optional.""" scope: String ) on OBJECT | INTERFACE | ENUM | UNION | INPUT_OBJECT | FIELD_DEFINITION | INPUT_FIELD_DEFINITION | SCALAR """Auth directive to control authorization behavior.""" directive @authenticated( """Auth required""" required: Boolean! ) on OBJECT | INTERFACE | ENUM | ENUM_VALUE | UNION | INPUT_OBJECT | FIELD_DEFINITION | ARGUMENT_DEFINITION | SCALAR interface Animal @cache(maxAge: 100) @authenticated(required: true) { age: Int! kind: Int! @cache(maxAge: 60) } enum TruthEnum @cache(maxAge: 100) @authenticated(required: true) { A @authenticated(required: true) B } input HumanInput @cache(maxAge: 60) @authenticated(required: true) { bornIn: String """Test Description""" name: String @deprecated(reason: "Deprecated use born in") @cache(maxAge: 300) } type Human @cache(maxAge: 60) { name: String bornIn: String } type Position @cache(maxAge: 100) { x: Int! y: Int! @cache(maxAge: 60) } type Query { position: Position @deprecated(reason: "Koo") }graphene-directives-0.4.6/tests/schema_files/test_directive.graphql000066400000000000000000000047701455127165500256550ustar00rootroot00000000000000extend schema @link(url: "https://spec.graphql.org/v1.0") @compose(directiveName: "lowercase") @compose(directiveName: "uppercase") @compose(directiveName: "lowercase") """Caching directive to control cache behavior of fields or fragments.""" directive @cache( """Specifies the maximum age for cache in seconds.""" maxAge: Int! """Stale-while-revalidate value in seconds. Optional.""" swr: Int """Scope of the cache. Optional.""" scope: String ) on OBJECT | INTERFACE | ENUM | UNION | INPUT_OBJECT | FIELD_DEFINITION | INPUT_FIELD_DEFINITION | SCALAR """Auth directive to control authorization behavior.""" directive @authenticated( """Auth required""" required: Boolean! ) on OBJECT | INTERFACE | ENUM | ENUM_VALUE | UNION | INPUT_OBJECT | FIELD_DEFINITION | ARGUMENT_DEFINITION | SCALAR """Auth directive to control authorization behavior.""" directive @hidden on OBJECT | ARGUMENT_DEFINITION """Schema directive to link files""" directive @link( """Url required""" url: String! ) on SCHEMA union SearchResult @cache(maxAge: 500) @authenticated(required: true) = Human | Droid | Starship type Human @cache(maxAge: 60) { name: String bornIn: String } type Droid @cache(maxAge: 200) { """Test Description""" name: String @deprecated(reason: "Deprecated use born in") @cache(maxAge: 300) primaryFunction: String } type Starship @cache(maxAge: 20) { name: String length: Int @deprecated(reason: "Koo") @cache(maxAge: 60) } interface Animal @cache(maxAge: 100) @authenticated(required: true) { age: Int! kind: Int! @cache(maxAge: 60) } type Admin @authenticated(required: true) { name: String password: String price( currency: Int @deprecated(reason: "Use country") @hidden """Country""" country: Int @authenticated(required: true) @hidden ): String } input HumanInput @cache(maxAge: 60) @authenticated(required: true) { bornIn: String """Test Description""" name: String @deprecated(reason: "Deprecated use born in") @cache(maxAge: 300) } enum TruthEnum @cache(maxAge: 100) @authenticated(required: true) { A @authenticated(required: true) B } scalar DateNewScalar @cache(maxAge: 500) @authenticated(required: true) type User @authenticated(required: true) { name: String password: String camelCased: String @hidden price(currency: Int @hidden, country: Int @authenticated(required: true) @hidden): String } type Query { position: Position @deprecated(reason: "Koo") } type Position @cache(maxAge: 100) { x: Int! y: Int! @cache(maxAge: 60) }graphene-directives-0.4.6/tests/schema_files/test_directive_include_graphql_spec_directives.graphql000066400000000000000000000010601455127165500343160ustar00rootroot00000000000000"""Caching directive to control cache behavior of fields or fragments.""" directive @cache( """Specifies the maximum age for cache in seconds.""" maxAge: Int! """Stale-while-revalidate value in seconds. Optional.""" swr: Int """Scope of the cache. Optional.""" scope: String ) on OBJECT | INTERFACE | ENUM | UNION | INPUT_OBJECT | FIELD_DEFINITION | INPUT_FIELD_DEFINITION | SCALAR type _TestClass @cache(maxAge: 500) { age: Int! kind: Int! @deprecated(reason: "This field is deprecated and will be removed in future") @cache(maxAge: 500) }test_directive_include_graphql_spec_directives_disable.graphql000066400000000000000000000010601455127165500357220ustar00rootroot00000000000000graphene-directives-0.4.6/tests/schema_files"""Caching directive to control cache behavior of fields or fragments.""" directive @cache( """Specifies the maximum age for cache in seconds.""" maxAge: Int! """Stale-while-revalidate value in seconds. Optional.""" swr: Int """Scope of the cache. Optional.""" scope: String ) on OBJECT | INTERFACE | ENUM | UNION | INPUT_OBJECT | FIELD_DEFINITION | INPUT_FIELD_DEFINITION | SCALAR type _TestClass @cache(maxAge: 500) { age: Int! kind: Int! @deprecated(reason: "This field is deprecated and will be removed in future") @cache(maxAge: 500) }graphene-directives-0.4.6/tests/schema_files/test_directive_input_transform.graphql000066400000000000000000000015141455127165500311600ustar00rootroot00000000000000"""Caching directive to control cache behavior of fields or fragments.""" directive @cache( """Specifies the maximum age for cache in seconds.""" maxAge: Int! """Stale-while-revalidate value in seconds. Optional.""" swr: Int """Scope of the cache. Optional.""" scope: String ) on FIELD_DEFINITION | OBJECT | UNION union SearchResult @cache(maxAge: 500, swr: 30) = Human | Droid | Starship type Human @cache(maxAge: 60) { name: String bornIn: String } type Droid @cache(maxAge: 200) { name: String @cache(maxAge: 300, swr: 30) primaryFunction: String } type Starship @cache(maxAge: 200) { name: String length: Int @deprecated(reason: "Koo") @cache(maxAge: 60) } type Query { position: Position @deprecated(reason: "Koo") } type Position @cache(maxAge: 500, swr: 30) { x: Int! y: Int! @cache(maxAge: 60) }graphene-directives-0.4.6/tests/schema_files/test_directive_using_helper.graphql000066400000000000000000000014611455127165500304130ustar00rootroot00000000000000"""Caching directive to control cache behavior of fields or fragments.""" directive @cache( """Specifies the maximum age for cache in seconds.""" maxAge: Int! """Stale-while-revalidate value in seconds. Optional.""" swr: Int """Scope of the cache. Optional.""" scope: String ) on FIELD_DEFINITION | OBJECT | UNION union SearchResult @cache(maxAge: 500) = Human | Droid | Starship type Human @cache(maxAge: 60) { name: String bornIn: String } type Droid @cache(maxAge: 200) { name: String @cache(maxAge: 300) primaryFunction: String } type Starship @cache(maxAge: 200) { name: String length: Int @deprecated(reason: "Koo") @cache(maxAge: 60) } type Query { position: Position @deprecated(reason: "Koo") } type Position @cache(maxAge: 100) { x: Int! y: Int! @cache(maxAge: 60) }graphene-directives-0.4.6/tests/schema_files/test_directive_with_repeatable.graphql000066400000000000000000000016601455127165500310670ustar00rootroot00000000000000"""Caching directive to control cache behavior of fields or fragments.""" directive @cache( """Specifies the maximum age for cache in seconds.""" maxAge: Int! """Stale-while-revalidate value in seconds. Optional.""" swr: Int """Scope of the cache. Optional.""" scope: String ) repeatable on OBJECT | INTERFACE | ENUM | UNION | INPUT_OBJECT | FIELD_DEFINITION | INPUT_FIELD_DEFINITION | SCALAR """Auth directive to control authorization behavior.""" directive @authenticated( """Auth required""" required: Boolean! ) on OBJECT | INTERFACE | ENUM | ENUM_VALUE | UNION | INPUT_OBJECT | FIELD_DEFINITION | ARGUMENT_DEFINITION | SCALAR type Query { animals: Animal @deprecated(reason: "Koo") } interface Animal @cache(maxAge: 30) @cache(maxAge: 100) @authenticated(required: true) { age: Int! kind: Int! @deprecated(reason: "This field is deprecated and will be removed in future") @cache(maxAge: 20) @cache(maxAge: 60) }graphene-directives-0.4.6/tests/test_arg_add_definition_to_schema.py000066400000000000000000000076221455127165500260610ustar00rootroot00000000000000from pathlib import Path import graphene from graphql import ( GraphQLArgument, GraphQLBoolean, GraphQLInt, GraphQLNonNull, GraphQLString, ) from graphene_directives import ( CustomDirective, DirectiveLocation, build_schema, directive, ) curr_dir = Path(__file__).parent CacheDirective = CustomDirective( name="cache", locations=[ DirectiveLocation.OBJECT, DirectiveLocation.INTERFACE, DirectiveLocation.ENUM, DirectiveLocation.UNION, DirectiveLocation.INPUT_OBJECT, DirectiveLocation.FIELD_DEFINITION, DirectiveLocation.INPUT_FIELD_DEFINITION, DirectiveLocation.SCALAR, ], args={ "max_age": GraphQLArgument( GraphQLNonNull(GraphQLInt), description="Specifies the maximum age for cache in seconds.", ), "swr": GraphQLArgument( GraphQLInt, description="Stale-while-revalidate value in seconds. Optional." ), "scope": GraphQLArgument( GraphQLString, description="Scope of the cache. Optional." ), }, description="Caching directive to control cache behavior of fields or fragments.", ) AuthenticatedDirective = CustomDirective( name="authenticated", locations=[ DirectiveLocation.OBJECT, DirectiveLocation.INTERFACE, DirectiveLocation.ENUM, DirectiveLocation.ENUM_VALUE, DirectiveLocation.UNION, DirectiveLocation.INPUT_OBJECT, DirectiveLocation.FIELD_DEFINITION, DirectiveLocation.ARGUMENT_DEFINITION, DirectiveLocation.SCALAR, ], args={ "required": GraphQLArgument( GraphQLNonNull(GraphQLBoolean), description="Auth required" ) }, description="Auth directive to control authorization behavior.", ) # No argument directive HiddenDirective = CustomDirective( name="hidden", locations=[DirectiveLocation.OBJECT, DirectiveLocation.ARGUMENT_DEFINITION], description="Auth directive to control authorization behavior.", add_definition_to_schema=False, ) @directive(target_directive=CacheDirective, max_age=100) @directive(target_directive=AuthenticatedDirective, required=True) class Animal(graphene.Interface): age = graphene.Int(required=True) kind = directive( target_directive=CacheDirective, field=graphene.Int(required=True), max_age=60 ) @directive(target_directive=CacheDirective, max_age=100) @directive(target_directive=AuthenticatedDirective, required=True) class TruthEnum(graphene.Enum): A = 1 B = 2 # Add directives to enum values directive(field=TruthEnum.A, target_directive=AuthenticatedDirective, required=True) @directive(target_directive=CacheDirective, max_age=100) class Position(graphene.ObjectType): x = graphene.Int(required=True) y = directive( target_directive=CacheDirective, field=graphene.Int(required=True), max_age=60 ) @directive(target_directive=CacheDirective, max_age=60) class Human(graphene.ObjectType): name = graphene.String() born_in = graphene.String() @directive(target_directive=CacheDirective, max_age=60) @directive(target_directive=AuthenticatedDirective, required=True) class HumanInput(graphene.InputObjectType): born_in = graphene.String() name = directive( CacheDirective, field=graphene.String( description="Test Description", deprecation_reason="Deprecated use born in" ), max_age=300, ) class Query(graphene.ObjectType): position = graphene.Field(Position, deprecation_reason="Koo") schema = build_schema( query=Query, types=(Animal, TruthEnum, HumanInput, Human, Position), directives=(CacheDirective, AuthenticatedDirective, HiddenDirective), ) def test_generate_schema() -> None: with open( f"{curr_dir}/schema_files/test_arg_add_definition_to_schema.graphql" ) as f: assert str(schema) == f.read() graphene-directives-0.4.6/tests/test_arg_allow_all_directive_locations.py000066400000000000000000000070111455127165500271460ustar00rootroot00000000000000from pathlib import Path import pytest from graphql import ( DirectiveLocation as GrapheneDirectiveLocation, GraphQLArgument, GraphQLInt, GraphQLNonNull, GraphQLString, ) from graphene_directives import CustomDirective, DirectiveLocation from graphene_directives.exceptions import DirectiveInvalidArgTypeError curr_dir = Path(__file__).parent def test_invalid_location_types() -> None: with pytest.raises(Exception) as e_info: CustomDirective( name="cache", locations=[GrapheneDirectiveLocation.FIELD, DirectiveLocation.OBJECT], args={ "max_age": GraphQLArgument( GraphQLNonNull(GraphQLInt), description="Specifies the maximum age for cache in seconds.", ), "swr": GraphQLArgument( GraphQLInt, description="Stale-while-revalidate value in seconds. Optional.", ), "scope": GraphQLArgument( GraphQLString, description="Scope of the cache. Optional." ), }, description="Caching directive to control cache behavior of fields or fragments.", ) assert e_info.type == DirectiveInvalidArgTypeError def test_invalid_location_types_allow() -> None: CustomDirective( name="cache", locations=[GrapheneDirectiveLocation.FIELD, DirectiveLocation.OBJECT], args={ "max_age": GraphQLArgument( GraphQLNonNull(GraphQLInt), description="Specifies the maximum age for cache in seconds.", ), "swr": GraphQLArgument( GraphQLInt, description="Stale-while-revalidate value in seconds. Optional.", ), "scope": GraphQLArgument( GraphQLString, description="Scope of the cache. Optional." ), }, description="Caching directive to control cache behavior of fields or fragments.", allow_all_directive_locations=True, ) def test_correct_location() -> None: CustomDirective( name="cache", locations=list(DirectiveLocation), args={ "max_age": GraphQLArgument( GraphQLNonNull(GraphQLInt), description="Specifies the maximum age for cache in seconds.", ), "swr": GraphQLArgument( GraphQLInt, description="Stale-while-revalidate value in seconds. Optional.", ), "scope": GraphQLArgument( GraphQLString, description="Scope of the cache. Optional." ), }, description="Caching directive to control cache behavior of fields or fragments.", allow_all_directive_locations=False, ) CustomDirective( name="cache2", locations=list(GrapheneDirectiveLocation), args={ "max_age": GraphQLArgument( GraphQLNonNull(GraphQLInt), description="Specifies the maximum age for cache in seconds.", ), "swr": GraphQLArgument( GraphQLInt, description="Stale-while-revalidate value in seconds. Optional.", ), "scope": GraphQLArgument( GraphQLString, description="Scope of the cache. Optional." ), }, description="Caching directive to control cache behavior of fields or fragments.", allow_all_directive_locations=True, ) graphene-directives-0.4.6/tests/test_directive.py000066400000000000000000000202651455127165500222220ustar00rootroot00000000000000from pathlib import Path from typing import Any import graphene from graphql import ( GraphQLArgument, GraphQLBoolean, GraphQLInt, GraphQLNonNull, GraphQLString, ) from graphene_directives import ( CustomDirective, DirectiveLocation, Schema, SchemaDirective, build_schema, directive, ) curr_dir = Path(__file__).parent def validate_non_field_input(_type: Any, inputs: dict, _schema: Schema) -> bool: """ def validator (type_: graphene type, inputs: Any, schema: Schema) -> bool, if validator returns False, library raises DirectiveCustomValidationError """ if inputs.get("max_age") > 2500: return False return True def validate_field_input( _parent_type: Any, _field_type: Any, inputs: dict, _schema: Schema ) -> bool: """ def validator (parent_type_: graphene_type, field_type_: graphene type, inputs: Any, schema: Schema) -> bool, if validator returns False, library raises DirectiveCustomValidationError """ if inputs.get("max_age") > 2500: return False return True CacheDirective = CustomDirective( name="cache", locations=[ DirectiveLocation.OBJECT, DirectiveLocation.INTERFACE, DirectiveLocation.ENUM, DirectiveLocation.UNION, DirectiveLocation.INPUT_OBJECT, DirectiveLocation.FIELD_DEFINITION, DirectiveLocation.INPUT_FIELD_DEFINITION, DirectiveLocation.SCALAR, ], args={ "max_age": GraphQLArgument( GraphQLNonNull(GraphQLInt), description="Specifies the maximum age for cache in seconds.", ), "swr": GraphQLArgument( GraphQLInt, description="Stale-while-revalidate value in seconds. Optional." ), "scope": GraphQLArgument( GraphQLString, description="Scope of the cache. Optional." ), }, description="Caching directive to control cache behavior of fields or fragments.", non_field_validator=validate_non_field_input, field_validator=validate_field_input, ) AuthenticatedDirective = CustomDirective( name="authenticated", locations=[ DirectiveLocation.OBJECT, DirectiveLocation.INTERFACE, DirectiveLocation.ENUM, DirectiveLocation.ENUM_VALUE, DirectiveLocation.UNION, DirectiveLocation.INPUT_OBJECT, DirectiveLocation.FIELD_DEFINITION, DirectiveLocation.ARGUMENT_DEFINITION, DirectiveLocation.SCALAR, ], args={ "required": GraphQLArgument( GraphQLNonNull(GraphQLBoolean), description="Auth required" ) }, description="Auth directive to control authorization behavior.", ) # No argument directive HiddenDirective = CustomDirective( name="hidden", locations=[DirectiveLocation.OBJECT, DirectiveLocation.ARGUMENT_DEFINITION], description="Auth directive to control authorization behavior.", ) # No Schema directive LinkDirective = CustomDirective( name="link", locations=[DirectiveLocation.SCHEMA], description="Schema directive to link files", args={ "url": GraphQLArgument( GraphQLNonNull(GraphQLString), description="Url required" ) }, ) # A Schema directive ComposeDirective = CustomDirective( name="compose", locations=[DirectiveLocation.SCHEMA], description="A schema directive.", args={ "directive_name": GraphQLArgument( GraphQLNonNull(GraphQLString), description="Directive Name required" ) }, is_repeatable=True, ) @directive(target_directive=CacheDirective, max_age=100) @directive(target_directive=AuthenticatedDirective, required=True) class Animal(graphene.Interface): age = graphene.Int(required=True) kind = directive( target_directive=CacheDirective, field=graphene.Int(required=True), max_age=60 ) @directive(target_directive=CacheDirective, max_age=100) @directive(target_directive=AuthenticatedDirective, required=True) class TruthEnum(graphene.Enum): A = 1 B = 2 # Add directives to enum values directive(field=TruthEnum.A, target_directive=AuthenticatedDirective, required=True) @directive(target_directive=CacheDirective, max_age=100) class Position(graphene.ObjectType): x = graphene.Int(required=True) y = directive( target_directive=CacheDirective, field=graphene.Int(required=True), max_age=60 ) @directive(target_directive=CacheDirective, max_age=60) class Human(graphene.ObjectType): name = graphene.String() born_in = graphene.String() @directive(target_directive=CacheDirective, max_age=60) @directive(target_directive=AuthenticatedDirective, required=True) class HumanInput(graphene.InputObjectType): born_in = graphene.String() name = directive( CacheDirective, field=graphene.String( description="Test Description", deprecation_reason="Deprecated use born in" ), max_age=300, ) @directive(CacheDirective, max_age=200) class Droid(graphene.ObjectType): name = directive( CacheDirective, field=graphene.String( description="Test Description", deprecation_reason="Deprecated use born in" ), max_age=300, ) primary_function = graphene.String() @directive(CacheDirective, max_age=20) class Starship(graphene.ObjectType): name = graphene.String() length = directive( target_directive=CacheDirective, field=graphene.Int(deprecation_reason="Koo"), max_age=60, ) @directive(target_directive=CacheDirective, max_age=500) @directive(target_directive=AuthenticatedDirective, required=True) class SearchResult(graphene.Union): class Meta: types = (Human, Droid, Starship) @directive(target_directive=CacheDirective, max_age=500) @directive(target_directive=AuthenticatedDirective, required=True) class DateNewScalar(graphene.Scalar): pass @directive(target_directive=AuthenticatedDirective, required=True) class Admin(graphene.ObjectType): name = graphene.String() password = graphene.String() price = graphene.Field( graphene.String, currency=directive( HiddenDirective, field=graphene.Argument(graphene.Int, deprecation_reason="Use country"), ), # Argument country=directive( target_directive=AuthenticatedDirective, field=directive( HiddenDirective, field=graphene.Argument(graphene.Int, description="Country"), ), required=True, ), # Argument Definition (Multiple directives) ) @directive(target_directive=AuthenticatedDirective, required=True) class User(graphene.ObjectType): name = graphene.String() password = graphene.String() camel_cased = directive(HiddenDirective, field=graphene.String()) price = graphene.Field( graphene.String, currency=directive( HiddenDirective, field=graphene.Argument(graphene.Int) ), # Argument country=directive( target_directive=AuthenticatedDirective, field=directive(HiddenDirective, field=graphene.Argument(graphene.Int)), required=True, ), # Argument Definition (Multiple directives) ) class Query(graphene.ObjectType): position = graphene.Field(Position, deprecation_reason="Koo") schema = build_schema( query=Query, types=(SearchResult, Animal, Admin, HumanInput, TruthEnum, DateNewScalar, User), directives=(CacheDirective, AuthenticatedDirective, HiddenDirective, LinkDirective), schema_directives=( # extend schema directives SchemaDirective( target_directive=LinkDirective, arguments={"url": "https://spec.graphql.org/v1.0"}, ), SchemaDirective( target_directive=ComposeDirective, arguments={"directive_name": "lowercase"} ), SchemaDirective( target_directive=ComposeDirective, arguments={"directive_name": "uppercase"} ), SchemaDirective( target_directive=ComposeDirective, arguments={"directive_name": "lowercase"} ), ), ) def test_generate_schema() -> None: with open(f"{curr_dir}/schema_files/test_directive.graphql") as f: assert str(schema) == f.read() graphene-directives-0.4.6/tests/test_directive_arguments.py000066400000000000000000000060541455127165500243070ustar00rootroot00000000000000from pathlib import Path import graphene import pytest from graphql import GraphQLArgument, GraphQLInt, GraphQLNonNull, GraphQLString from graphene_directives import CustomDirective, DirectiveLocation, directive from graphene_directives.exceptions import DirectiveInvalidArgValueTypeError curr_dir = Path(__file__).parent CacheDirective = CustomDirective( name="cache", locations=[DirectiveLocation.OBJECT, DirectiveLocation.FIELD_DEFINITION], args={ "max_age": GraphQLArgument( GraphQLNonNull(GraphQLInt), description="Specifies the maximum age for cache in seconds.", ), "swr": GraphQLArgument( GraphQLInt, description="Stale-while-revalidate value in seconds. Optional." ), "scope": GraphQLArgument( GraphQLString, description="Scope of the cache. Optional." ), }, description="Caching directive to control cache behavior of fields or fragments.", is_repeatable=True, ) DbCacheDirective = CustomDirective( name="db_cache", locations=[DirectiveLocation.OBJECT, DirectiveLocation.FIELD_DEFINITION], args={ "max_age": GraphQLArgument( GraphQLNonNull(GraphQLInt), description="Specifies the maximum age for cache in seconds.", default_value=12, ), "swr": GraphQLArgument( GraphQLInt, description="Stale-while-revalidate value in seconds. Optional." ), "scope": GraphQLArgument( GraphQLString, description="Scope of the cache. Optional." ), }, description="Caching directive to control cache behavior of fields or fragments.", ) def test_input_argument_on_class() -> None: with pytest.raises(Exception) as e_info: @directive(target_directive=CacheDirective, required=True) class _TestClass(graphene.ObjectType): age = graphene.Int(required=True) assert e_info.type == DirectiveInvalidArgValueTypeError def test_input_argument_on_field() -> None: with pytest.raises(Exception) as e_info: class _TestClass(graphene.ObjectType): age = graphene.Int(required=True) kind = directive( target_directive=CacheDirective, field=graphene.Int( required=True, deprecation_reason="This field is deprecated and will be removed in future", ), ) assert e_info.type == DirectiveInvalidArgValueTypeError def test_input_default_argument_on_class() -> None: @directive(target_directive=DbCacheDirective, required=True) class _TestClass(graphene.ObjectType): age = graphene.Int(required=True) def test_input_default_argument_on_field() -> None: class _TestClass(graphene.ObjectType): age = graphene.Int(required=True) kind = directive( target_directive=DbCacheDirective, field=graphene.Int( required=True, deprecation_reason="This field is deprecated and will be removed in future", ), ) graphene-directives-0.4.6/tests/test_directive_duplicate_values.py000066400000000000000000000026351455127165500256340ustar00rootroot00000000000000from pathlib import Path import graphene import pytest from graphql import GraphQLArgument, GraphQLInt, GraphQLNonNull, GraphQLString from graphene_directives import ( CustomDirective, DirectiveLocation, DirectiveValidationError, directive, ) curr_dir = Path(__file__).parent CacheDirective = CustomDirective( name="cache", locations=[DirectiveLocation.FIELD_DEFINITION, DirectiveLocation.OBJECT], args={ "max_age": GraphQLArgument( GraphQLNonNull(GraphQLInt), description="Specifies the maximum age for cache in seconds.", ), "swr": GraphQLArgument( GraphQLInt, description="Stale-while-revalidate value in seconds. Optional." ), "scope": GraphQLArgument( GraphQLString, description="Scope of the cache. Optional." ), }, description="Caching directive to control cache behavior of fields or fragments.", is_repeatable=True, ) def test_duplicate_values() -> None: with pytest.raises(Exception) as e_info: @directive(CacheDirective, max_age=20) @directive(CacheDirective, max_age=20) class _Test(graphene.ObjectType): pass assert e_info.type == DirectiveValidationError def test_non_duplicate_values() -> None: @directive(CacheDirective, max_age=20) @directive(CacheDirective, max_age=22) class _Test(graphene.ObjectType): pass graphene-directives-0.4.6/tests/test_directive_include_graphql_spec_directives.py000066400000000000000000000175471455127165500307070ustar00rootroot00000000000000from pathlib import Path import graphene import pytest from graphql import GraphQLArgument, GraphQLInt, GraphQLNonNull, GraphQLString from graphene_directives import ( CustomDirective, DirectiveLocation, DirectiveValidationError, build_schema, directive, ) curr_dir = Path(__file__).parent def test_pass_reserved_directive() -> None: with pytest.raises(DirectiveValidationError) as e_info: SkipDirective = CustomDirective( name="skip", locations=[ DirectiveLocation.OBJECT, DirectiveLocation.INTERFACE, DirectiveLocation.ENUM, DirectiveLocation.UNION, DirectiveLocation.INPUT_OBJECT, DirectiveLocation.FIELD_DEFINITION, DirectiveLocation.INPUT_FIELD_DEFINITION, DirectiveLocation.SCALAR, ], args={ "max_age": GraphQLArgument( GraphQLNonNull(GraphQLInt), description="Specifies the maximum age for cache in seconds.", ), "swr": GraphQLArgument( GraphQLInt, description="Stale-while-revalidate value in seconds. Optional.", ), "scope": GraphQLArgument( GraphQLString, description="Scope of the cache. Optional." ), }, description="Caching directive to control cache behavior of fields or fragments.", ) @directive(target_directive=SkipDirective, max_age=500) class _TestClass(graphene.ObjectType): age = graphene.Int(required=True) kind = directive( target_directive=SkipDirective, field=graphene.Int( required=True, deprecation_reason="This field is deprecated and will be removed in future", ), max_age=500, ) _ = build_schema(types=(_TestClass,), directives=(SkipDirective,)) assert e_info.type == DirectiveValidationError def test_pass_reserved_directive_with_disable() -> None: with pytest.raises(DirectiveValidationError) as e_info: SkipDirective = CustomDirective( name="skip", locations=[ DirectiveLocation.OBJECT, DirectiveLocation.INTERFACE, DirectiveLocation.ENUM, DirectiveLocation.UNION, DirectiveLocation.INPUT_OBJECT, DirectiveLocation.FIELD_DEFINITION, DirectiveLocation.INPUT_FIELD_DEFINITION, DirectiveLocation.SCALAR, ], args={ "max_age": GraphQLArgument( GraphQLNonNull(GraphQLInt), description="Specifies the maximum age for cache in seconds.", ), "swr": GraphQLArgument( GraphQLInt, description="Stale-while-revalidate value in seconds. Optional.", ), "scope": GraphQLArgument( GraphQLString, description="Scope of the cache. Optional." ), }, description="Caching directive to control cache behavior of fields or fragments.", ) @directive(target_directive=SkipDirective, max_age=500) class _TestClass(graphene.ObjectType): age = graphene.Int(required=True) kind = directive( target_directive=SkipDirective, field=graphene.Int( required=True, deprecation_reason="This field is deprecated and will be removed in future", ), max_age=500, ) _ = build_schema( types=(_TestClass,), directives=(SkipDirective,), include_graphql_spec_directives=True, # default ) assert e_info.type == DirectiveValidationError def test_pass_reserved_directive_with_disable_schema() -> None: CacheDirective = CustomDirective( name="cache", locations=[ DirectiveLocation.OBJECT, DirectiveLocation.INTERFACE, DirectiveLocation.ENUM, DirectiveLocation.UNION, DirectiveLocation.INPUT_OBJECT, DirectiveLocation.FIELD_DEFINITION, DirectiveLocation.INPUT_FIELD_DEFINITION, DirectiveLocation.SCALAR, ], args={ "max_age": GraphQLArgument( GraphQLNonNull(GraphQLInt), description="Specifies the maximum age for cache in seconds.", ), "swr": GraphQLArgument( GraphQLInt, description="Stale-while-revalidate value in seconds. Optional.", ), "scope": GraphQLArgument( GraphQLString, description="Scope of the cache. Optional." ), }, description="Caching directive to control cache behavior of fields or fragments.", ) @directive(target_directive=CacheDirective, max_age=500) class _TestClass(graphene.ObjectType): age = graphene.Int(required=True) kind = directive( target_directive=CacheDirective, field=graphene.Int( required=True, deprecation_reason="This field is deprecated and will be removed in future", ), max_age=500, ) schema = build_schema( types=(_TestClass,), directives=(CacheDirective,), include_graphql_spec_directives=False, ) with open( f"{curr_dir}/schema_files/test_directive_include_graphql_spec_directives_disable.graphql", "w", ) as f: f.write(str(schema)) with open( f"{curr_dir}/schema_files/test_directive_include_graphql_spec_directives_disable.graphql" ) as f: assert str(schema) == f.read() def test_pass_reserved_directive_with_enable_schema() -> None: CacheDirective = CustomDirective( name="cache", locations=[ DirectiveLocation.OBJECT, DirectiveLocation.INTERFACE, DirectiveLocation.ENUM, DirectiveLocation.UNION, DirectiveLocation.INPUT_OBJECT, DirectiveLocation.FIELD_DEFINITION, DirectiveLocation.INPUT_FIELD_DEFINITION, DirectiveLocation.SCALAR, ], args={ "max_age": GraphQLArgument( GraphQLNonNull(GraphQLInt), description="Specifies the maximum age for cache in seconds.", ), "swr": GraphQLArgument( GraphQLInt, description="Stale-while-revalidate value in seconds. Optional.", ), "scope": GraphQLArgument( GraphQLString, description="Scope of the cache. Optional." ), }, description="Caching directive to control cache behavior of fields or fragments.", ) @directive(target_directive=CacheDirective, max_age=500) class _TestClass(graphene.ObjectType): age = graphene.Int(required=True) kind = directive( target_directive=CacheDirective, field=graphene.Int( required=True, deprecation_reason="This field is deprecated and will be removed in future", ), max_age=500, ) schema = build_schema( types=(_TestClass,), directives=(CacheDirective,), include_graphql_spec_directives=True, # default ) with open( f"{curr_dir}/schema_files/test_directive_include_graphql_spec_directives.graphql", "w", ) as f: f.write(str(schema)) with open( f"{curr_dir}/schema_files/test_directive_include_graphql_spec_directives.graphql" ) as f: assert str(schema) == f.read() graphene-directives-0.4.6/tests/test_directive_input_transform.py000066400000000000000000000045111455127165500255300ustar00rootroot00000000000000from pathlib import Path import graphene from graphql import GraphQLArgument, GraphQLInt, GraphQLNonNull, GraphQLString from graphene_directives import ( CustomDirective, DirectiveLocation, Schema, build_schema, directive_decorator, ) curr_dir = Path(__file__).parent def input_transform(inputs: dict, _schema: Schema) -> dict: """ def input_transform (inputs: Any, schema: Schema) -> dict, """ if inputs.get("max_age") > 200: inputs["swr"] = 30 return inputs CacheDirective = CustomDirective( name="cache", locations=[ DirectiveLocation.FIELD_DEFINITION, DirectiveLocation.OBJECT, DirectiveLocation.UNION, ], args={ "max_age": GraphQLArgument( GraphQLNonNull(GraphQLInt), description="Specifies the maximum age for cache in seconds.", ), "swr": GraphQLArgument( GraphQLInt, description="Stale-while-revalidate value in seconds. Optional." ), "scope": GraphQLArgument( GraphQLString, description="Scope of the cache. Optional." ), }, description="Caching directive to control cache behavior of fields or fragments.", input_transform=input_transform, ) cache = directive_decorator(target_directive=CacheDirective) @cache(max_age=500) class Position(graphene.ObjectType): x = graphene.Int(required=True) y = cache(field=graphene.Int(required=True), max_age=60) @cache(max_age=60) class Human(graphene.ObjectType): name = graphene.String() born_in = graphene.String() @cache(max_age=200) class Droid(graphene.ObjectType): name = cache(field=graphene.String(), max_age=300) primary_function = graphene.String() @cache(max_age=200) class Starship(graphene.ObjectType): name = graphene.String() length = cache(field=graphene.Int(deprecation_reason="Koo"), max_age=60) @cache(max_age=500) class SearchResult(graphene.Union): class Meta: types = (Human, Droid, Starship) class Query(graphene.ObjectType): position = graphene.Field(Position, deprecation_reason="Koo") schema = build_schema(query=Query, types=(SearchResult,), directives=[CacheDirective]) def test_generate_schema() -> None: with open(f"{curr_dir}/schema_files/test_directive_input_transform.graphql") as f: assert str(schema) == f.read() graphene-directives-0.4.6/tests/test_directive_using_helper.py000066400000000000000000000040661455127165500247670ustar00rootroot00000000000000from pathlib import Path import graphene from graphql import GraphQLArgument, GraphQLInt, GraphQLNonNull, GraphQLString from graphene_directives import ( CustomDirective, DirectiveLocation, build_schema, directive_decorator, ) curr_dir = Path(__file__).parent CacheDirective = CustomDirective( name="cache", locations=[ DirectiveLocation.FIELD_DEFINITION, DirectiveLocation.OBJECT, DirectiveLocation.UNION, ], args={ "max_age": GraphQLArgument( GraphQLNonNull(GraphQLInt), description="Specifies the maximum age for cache in seconds.", ), "swr": GraphQLArgument( GraphQLInt, description="Stale-while-revalidate value in seconds. Optional." ), "scope": GraphQLArgument( GraphQLString, description="Scope of the cache. Optional." ), }, description="Caching directive to control cache behavior of fields or fragments.", ) cache = directive_decorator(target_directive=CacheDirective) @cache(max_age=100) class Position(graphene.ObjectType): x = graphene.Int(required=True) y = cache(field=graphene.Int(required=True), max_age=60) @cache(max_age=60) class Human(graphene.ObjectType): name = graphene.String() born_in = graphene.String() @cache(max_age=200) class Droid(graphene.ObjectType): name = cache(field=graphene.String(), max_age=300) primary_function = graphene.String() @cache(max_age=200) class Starship(graphene.ObjectType): name = graphene.String() length = cache(field=graphene.Int(deprecation_reason="Koo"), max_age=60) @cache(max_age=500) class SearchResult(graphene.Union): class Meta: types = (Human, Droid, Starship) class Query(graphene.ObjectType): position = graphene.Field(Position, deprecation_reason="Koo") schema = build_schema(query=Query, types=(SearchResult,), directives=[CacheDirective]) def test_generate_schema() -> None: with open(f"{curr_dir}/schema_files/test_directive_using_helper.graphql") as f: assert str(schema) == f.read() graphene-directives-0.4.6/tests/test_directive_with_repeatable.py000066400000000000000000000105531455127165500254400ustar00rootroot00000000000000from pathlib import Path import graphene import pytest from graphql import ( GraphQLArgument, GraphQLBoolean, GraphQLInt, GraphQLNonNull, GraphQLString, ) from graphene_directives import ( CustomDirective, DirectiveLocation, DirectiveValidationError, build_schema, directive, ) curr_dir = Path(__file__).parent CacheDirective = CustomDirective( name="cache", locations=[ DirectiveLocation.OBJECT, DirectiveLocation.INTERFACE, DirectiveLocation.ENUM, DirectiveLocation.UNION, DirectiveLocation.INPUT_OBJECT, DirectiveLocation.FIELD_DEFINITION, DirectiveLocation.INPUT_FIELD_DEFINITION, DirectiveLocation.SCALAR, ], args={ "max_age": GraphQLArgument( GraphQLNonNull(GraphQLInt), description="Specifies the maximum age for cache in seconds.", ), "swr": GraphQLArgument( GraphQLInt, description="Stale-while-revalidate value in seconds. Optional." ), "scope": GraphQLArgument( GraphQLString, description="Scope of the cache. Optional." ), }, description="Caching directive to control cache behavior of fields or fragments.", is_repeatable=True, ) AuthenticatedDirective = CustomDirective( name="authenticated", locations=[ DirectiveLocation.OBJECT, DirectiveLocation.INTERFACE, DirectiveLocation.ENUM, DirectiveLocation.ENUM_VALUE, DirectiveLocation.UNION, DirectiveLocation.INPUT_OBJECT, DirectiveLocation.FIELD_DEFINITION, DirectiveLocation.ARGUMENT_DEFINITION, DirectiveLocation.SCALAR, ], args={ "required": GraphQLArgument( GraphQLNonNull(GraphQLBoolean), description="Auth required" ) }, description="Auth directive to control authorization behavior.", ) @directive(target_directive=CacheDirective, max_age=100) @directive(target_directive=CacheDirective, max_age=30) @directive(target_directive=AuthenticatedDirective, required=True) class Animal(graphene.Interface): age = graphene.Int(required=True) kind = directive( target_directive=CacheDirective, field=directive( target_directive=CacheDirective, field=graphene.Int( required=True, deprecation_reason="This field is deprecated and will be removed in future", ), max_age=20, ), max_age=60, ) class Query(graphene.ObjectType): animals = graphene.Field(Animal, deprecation_reason="Koo") schema = build_schema(query=Query, directives=(CacheDirective, AuthenticatedDirective)) def test_generate_schema() -> None: with open(f"{curr_dir}/schema_files/test_directive_with_repeatable.graphql") as f: assert str(schema) == f.read() def test_non_repeatable_on_non_field() -> None: with pytest.raises(Exception) as e_info: @directive(target_directive=AuthenticatedDirective, required=True) @directive(target_directive=AuthenticatedDirective, required=True) class _TestClass(graphene.Interface): age = graphene.Int(required=True) kind = directive( target_directive=CacheDirective, field=directive( target_directive=CacheDirective, field=graphene.Int( required=True, deprecation_reason="This field is deprecated and will be removed in future", ), max_age=20, ), max_age=60, ) assert e_info.type == DirectiveValidationError def test_non_repeatable_on_field() -> None: with pytest.raises(Exception) as e_info: class _TestClass(graphene.Interface): age = graphene.Int(required=True) kind = directive( target_directive=AuthenticatedDirective, field=directive( target_directive=AuthenticatedDirective, field=graphene.Int( required=True, deprecation_reason="This field is deprecated and will be removed in future", ), required=True, ), required=False, ) assert e_info.type == DirectiveValidationError