Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for DirectivesEstimator, include and skip directives and added shortcuts for complexity calculation #1

Merged
merged 10 commits into from
Feb 22, 2024
Binary file removed .mutmut-cache
Binary file not shown.
164 changes: 164 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,166 @@
# graphql-complexity
Python library to compute the complexity of a GraphQL operation

![Unit Tests](https://github.com/Checho3388/graphql-complexity/actions/workflows/python-package.yml/badge.svg)


## Usage
The library uses the query complexity algorithm to compute the complexity of a GraphQL operation. The algorithm is
based on the number of fields requested in the operation and the depth of the query.

```python
from graphql_complexity import (get_complexity, SimpleEstimator)

query = """
query SomeQuery {
user {
id
name
}
}
"""

complexity = get_complexity(query, estimator=SimpleEstimator(complexity=1, multiplier=1))
if complexity > 10:
raise Exception("Query is too complex")
```

## Estimators
In order to get the complexity of a query, an estimator needs to be defined. The main responsibility of
the estimator is to give each node an integer value representing its complexity and (optionally) a
multiplier that reflects the complexity in relation to the depth of the query.

There are two built-in estimators, plus the capability to create any new estimator by
implementing the `ComplexityEstimator` interface.

### Estimate fields complexity based on constants for complexity and multiplier

This estimator assigns a **constant** complexity value to each field and multiplies
it by another **constant** which is propagated along the depth of the query.

```python
from graphql_complexity import SimpleEstimator


estimator = SimpleEstimator(complexity=2, multiplier=1)
```

Given the following query:
```qgl
query {
user {
name
email
}
}
```
As the complexity and multiplier are constant, the complexity of the fields will be:

| Field | Complexity |
|-------|---------------|
| user | `1` |
| name | `2 * (2 * 1)` |
| email | `2 * (2 * 1)` |

And the total complexity will be `6`.

### Define fields complexity using schema directives

Assigns a complexity value to each field and multiplies it by the depth of the query.
It also supports the `@complexity` directive to assign a custom complexity value to a field.

This approach requires to provide the schema to the estimator.

```python
from graphql_complexity import DirectivesEstimator


schema = """
directive @complexity(
value: Int!
) on FIELD_DEFINITION

type Query {
oneField: String @complexity(value: 5)
otherField: String @complexity(value: 1)
withoutDirective: String
}
"""

estimator = DirectivesEstimator(schema)
```

Given the schema from above and the following query:
```qgl
query {
oneField
otherField
withoutDirective
}
```

The complexity of the fields will be:

| Field | Complexity | Comment |
|------------------|------------|---------------------------------------------------------------------------------------------------|
| oneField | `5` | |
| otherField | `1` | |
| withoutDirective | `1` | The default complexity for fields without directive is `1`, this can be modified by parameters. |

And the total complexity will be `7`.

### Create a custom estimator
This option allows to define a custom estimator to compute the complexity of a field using the `ComplexityEstimator` interface. For example:

```python
from graphql_complexity import ComplexityEstimator


class CustomEstimator(ComplexityEstimator):
def get_field_complexity(self, node, key, parent, path, ancestors) -> int:
if node.name.value == "specificField":
return 100
return 1

def get_field_multiplier(self, node, key, parent, path, ancestors) -> int:
return 1
```


## Supported libraries (based on GraphQL-core)
The library is compatible with the following GraphQL libraries:

### Strawberry


The library is compatible with [strawberry-graphql](https://pypi.org/project/strawberry-graphql/).
To use the library with strawberry-graphql, you need to install the library with the `strawberry-graphql` extra.
```shell
poetry install --extras strawberry-graphql
```

To use the library with [strawberry-graphql](https://pypi.org/project/strawberry-graphql/), you need to use the `build_complexity_extension` method to build
the complexity extension and add it to the schema. This method receives an estimator and returns a complexity
extension that can be added to the schema.

```python
import strawberry
from graphql_complexity.extensions.strawberry_graphql import build_complexity_extension

@strawberry.type
class Query:
@strawberry.field()
def hello_world(self) -> str:
return "Hello world!"

extension = build_complexity_extension()
schema = strawberry.Schema(query=Query, extensions=[extension])

schema.execute_sync("query { helloWorld }")
```
The `build_complexity_extension` method accepts an estimator as optional argument giving the possibility to use one
of the built-in estimators or a custom estimator.

## Credits

Estimators idea was heavily inspired by [graphql-query-complexity](https://github.com/slicknode/graphql-query-complexity).
5 changes: 4 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
[tool.poetry]
name = "graphql-complexity"
name = "graphql_complexity"
version = "0.1.0"
description = "A python library that provides complexity calculation helpers for GraphQL"
authors = ["Checho3388 <[email protected]>"]
packages = [
{ include = "graphql_complexity", from = "src" },
]
license = "MIT"
readme = "README.md"

Expand Down
112 changes: 0 additions & 112 deletions src/estimators.py

This file was deleted.

26 changes: 0 additions & 26 deletions src/extensions.py

This file was deleted.

14 changes: 14 additions & 0 deletions src/graphql_complexity/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from graphql_complexity.evaluator.complexity import get_complexity

from .estimators import (
ComplexityEstimator,
DirectivesEstimator,
SimpleEstimator
)

__all__ = [
"get_complexity",
"SimpleEstimator",
"ComplexityEstimator",
"DirectivesEstimator",
]
5 changes: 5 additions & 0 deletions src/graphql_complexity/estimators/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from .base import ComplexityEstimator
from .directive import DirectivesEstimator
from .simple import SimpleEstimator

__all__ = ["ComplexityEstimator", "SimpleEstimator", "DirectivesEstimator"]
11 changes: 11 additions & 0 deletions src/graphql_complexity/estimators/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import abc


class ComplexityEstimator(abc.ABC):
@abc.abstractmethod
def get_field_complexity(self, node, key, parent, path, ancestors) -> int:
"""Return the complexity of the field."""

@abc.abstractmethod
def get_field_multiplier(self, node, key, parent, path, ancestors) -> int:
"""Return the multiplier that will be applied to the children of the given node."""
Loading
Loading