Skip to main content

A python library that provides complexity calculation helpers for GraphQL

Project description

GraphQL Complexity Logo

GraphQL Complexity

Welcome to GraphQL-Complexity! This Python library provides functionality to compute the complexity of a GraphQL operation, contributing to better understanding and optimization of your GraphQL APIs. This library is designed to be stable, robust, and highly useful for developers working with GraphQL.

Build PyPI codecov Downloads Python Version License: MIT

✨ Why GraphQL Complexity?

Protect your GraphQL API from expensive queries and potential DoS attacks by calculating query complexity before execution.

5,500+ downloadsUsed in productionActively maintained

Features

  • Compute complexity of GraphQL queries
  • Multiple built-in estimators for complexity computation
  • Customizable estimators for specific use cases
  • Support for Strawberry GraphQL library

Table of Contents

Installation (Quick Start)

You can install the library via pip:

pip install graphql-complexity

For Strawberry GraphQL integration, use the following command:

pip install graphql-complexity[strawberry-graphql]

Getting Started

Create a file named complexity.py with the following content:

from graphql_complexity import get_complexity, SimpleEstimator
from graphql import build_schema


schema = build_schema("""
    type User {
        id: ID!
        name: String!
    }
    type Query {
        user: User
    }
""")

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

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

The library exposes the method get_complexity with the algorithm to compute the complexity of a GraphQL operation. The algorithm visits each node of the query and computes the complexity of each field using an estimator.

Estimators

GraphQL-Complexity provides various built-in estimators for computing query complexity:

SimpleEstimator

Estimate fields complexity based on constants for complexity and multiplier. This assigns a constant complexity value to each field and multiplies it by another constant, which is propagated along the depth of the query.

from graphql_complexity import SimpleEstimator


estimator = SimpleEstimator(complexity=2)

DirectivesEstimator

Define fields complexity using schema directives. This 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.

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)

Custom estimator

Custom estimators can be defined to compute the complexity of a field using the ComplexityEstimator interface.

from graphql_complexity import ComplexityEstimator


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

Advanced Examples

Handling Fragments

GraphQL fragments allow you to reuse query parts. Here's how complexity is calculated for queries with fragments:

from graphql_complexity import get_complexity, SimpleEstimator
from graphql import build_schema

schema = build_schema("""
    type User {
        id: ID!
        name: String!
        email: String!
        posts: [Post!]!
    }
    
    type Post {
        id: ID!
        title: String!
        content: String!
        author: User!
    }
    
    type Query {
        user(id: ID!): User
        users: [User!]!
    }
""")

# Query with named fragment
query = """
    query GetUsers {
        users {
            ...UserFields
            posts {
                ...PostFields
            }
        }
    }
    
    fragment UserFields on User {
        id
        name
        email
    }
    
    fragment PostFields on Post {
        id
        title
        content
    }
"""

complexity = get_complexity(
    query=query,
    schema=schema,
    estimator=SimpleEstimator(complexity=1)
)
print(f"Query complexity: {complexity}")

Complex Nested Queries

When dealing with nested relationships, complexity can grow exponentially. Here's how to handle it:

from graphql_complexity import get_complexity, DirectivesEstimator
from graphql import build_schema

schema = build_schema("""
    directive @complexity(
        value: Int!
    ) on FIELD_DEFINITION
    
    type Organization {
        id: ID!
        name: String!
        teams: [Team!]! @complexity(value: 5)
    }
    
    type Team {
        id: ID!
        name: String!
        members: [User!]! @complexity(value: 3)
    }
    
    type User {
        id: ID!
        name: String!
        email: String!
        tasks: [Task!]! @complexity(value: 2)
    }
    
    type Task {
        id: ID!
        title: String!
        description: String!
    }
    
    type Query {
        organization(id: ID!): Organization
    }
""")

# Deeply nested query
query = """
    query GetOrgStructure {
        organization(id: "1") {
            id
            name
            teams {
                id
                name
                members {
                    id
                    name
                    email
                    tasks {
                        id
                        title
                        description
                    }
                }
            }
        }
    }
"""

estimator = DirectivesEstimator(schema)
complexity = get_complexity(query=query, schema=schema, estimator=estimator)

# Set a reasonable limit
MAX_COMPLEXITY = 100
if complexity > MAX_COMPLEXITY:
    raise Exception(f"Query too complex: {complexity} > {MAX_COMPLEXITY}")

Working with Arguments

Field arguments can significantly impact query complexity, especially with pagination:

from graphql_complexity import ComplexityEstimator, get_complexity
from graphql import build_schema, FieldNode
from graphql.language import ast

schema = build_schema("""
    type Product {
        id: ID!
        name: String!
        price: Float!
    }
    
    type Query {
        products(first: Int, offset: Int): [Product!]!
        product(id: ID!): Product
    }
""")

class ArgumentAwareEstimator(ComplexityEstimator):
    """Custom estimator that considers pagination arguments"""
    
    def get_field_complexity(self, node: FieldNode, type_info, path) -> int:
        field_name = node.name.value
        
        # Base complexity
        base_complexity = 1
        
        # Check for list/pagination arguments
        if node.arguments:
            for arg in node.arguments:
                if arg.name.value == "first":
                    # Extract the limit value
                    if isinstance(arg.value, ast.IntValue):
                        limit = int(arg.value.value)
                        # Multiply complexity by the number of items requested
                        base_complexity *= min(limit, 100)  # Cap at 100
        
        # Apply higher base cost for lists
        if field_name == "products":
            base_complexity *= 5
        
        return base_complexity

query = """
    query GetProducts {
        products(first: 50) {
            id
            name
            price
        }
    }
"""

complexity = get_complexity(
    query=query,
    schema=schema,
    estimator=ArgumentAwareEstimator()
)
print(f"Query complexity with pagination: {complexity}")

Combining Multiple Estimators

You can create sophisticated complexity analysis by combining estimators:

from graphql_complexity import (
    ComplexityEstimator,
    SimpleEstimator,
    DirectivesEstimator,
    get_complexity
)
from graphql import build_schema

schema = build_schema("""
    directive @complexity(value: Int!) on FIELD_DEFINITION
    
    type User {
        id: ID!
        name: String!
        expensiveComputation: String! @complexity(value: 50)
        posts: [Post!]!
    }
    
    type Post {
        id: ID!
        title: String!
    }
    
    type Query {
        user(id: ID!): User
        users: [User!]!
    }
""")

class CompositeEstimator(ComplexityEstimator):
    """Combines multiple estimation strategies"""
    
    def __init__(self):
        self.directive_estimator = DirectivesEstimator(schema)
        self.simple_estimator = SimpleEstimator(complexity=1)
    
    def get_field_complexity(self, node, type_info, path) -> int:
        # First, try directive-based estimation
        directive_complexity = self.directive_estimator.get_field_complexity(
            node, type_info, path
        )
        
        # If directive provides a value, use it
        if directive_complexity > 1:
            return directive_complexity
        
        # Otherwise, fall back to simple estimation
        return self.simple_estimator.get_field_complexity(node, type_info, path)

query = """
    query GetUserData {
        user(id: "1") {
            id
            name
            expensiveComputation
            posts {
                id
                title
            }
        }
    }
"""

complexity = get_complexity(
    query=query,
    schema=schema,
    estimator=CompositeEstimator()
)
print(f"Combined estimator complexity: {complexity}")

Framework Integration

Strawberry GraphQL

The library is compatible with strawberry-graphql. Use the following command to install the library with Strawberry support:

poetry install --extras strawberry-graphql

To use the library with Strawberry GraphQL, 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.

import strawberry
from graphql_complexity import SimpleEstimator
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(estimator=SimpleEstimator())
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.

Django Integration

Integrate complexity analysis with Django and Graphene:

# myapp/complexity.py
from graphql_complexity import get_complexity, SimpleEstimator
from graphql import GraphQLError


class ComplexityMiddleware:
    def __init__(self, max_complexity=1000):
        self.max_complexity = max_complexity
        self.estimator = SimpleEstimator(complexity=1)
    
    def resolve(self, next, root, info, **args):
        # Calculate complexity on first field resolution
        if not hasattr(info.context, '_complexity_checked'):
            try:
                complexity = get_complexity(
                    query=info.operation.loc.source.body,
                    schema=info.schema,
                    estimator=self.estimator
                )
                
                if complexity > self.max_complexity:
                    raise GraphQLError(
                        f"Query is too complex: {complexity}. "
                        f"Maximum allowed complexity: {self.max_complexity}"
                    )
                
                info.context._complexity_checked = True
                info.context._query_complexity = complexity
                
            except Exception as e:
                raise GraphQLError(f"Complexity analysis failed: {str(e)}")
        
        return next(root, info, **args)


# settings.py
GRAPHENE = {
    'MIDDLEWARE': [
        'myapp.complexity.ComplexityMiddleware',
    ],
}

FastAPI Integration

Use complexity analysis with FastAPI and Strawberry:

from fastapi import FastAPI
from strawberry.fastapi import GraphQLRouter
import strawberry
from graphql_complexity import SimpleEstimator
from graphql_complexity.extensions.strawberry_graphql import build_complexity_extension


@strawberry.type
class User:
    id: strawberry.ID
    name: str
    email: str


@strawberry.type
class Query:
    @strawberry.field
    def user(self, id: strawberry.ID) -> User:
        return User(id=id, name="John Doe", email="john@example.com")
    
    @strawberry.field
    def users(self) -> list[User]:
        return [
            User(id="1", name="John Doe", email="john@example.com"),
            User(id="2", name="Jane Smith", email="jane@example.com"),
        ]


# Configure complexity limit
MAX_COMPLEXITY = 1000
extension = build_complexity_extension(
    estimator=SimpleEstimator(complexity=1),
    max_complexity=MAX_COMPLEXITY
)

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

app = FastAPI()
app.include_router(graphql_app, prefix="/graphql")

Flask Integration

Integrate with Flask-GraphQL:

from flask import Flask, request, jsonify
from flask_graphql import GraphQLView
from graphql import build_schema, GraphQLError
from graphql_complexity import get_complexity, SimpleEstimator


app = Flask(__name__)

schema = build_schema("""
    type User {
        id: ID!
        name: String!
        email: String!
    }
    
    type Query {
        user(id: ID!): User
        users: [User!]!
    }
""")

MAX_COMPLEXITY = 1000
estimator = SimpleEstimator(complexity=1)


def validate_complexity(query_string):
    """Validate query complexity before execution"""
    try:
        complexity = get_complexity(
            query=query_string,
            schema=schema,
            estimator=estimator
        )
        
        if complexity > MAX_COMPLEXITY:
            raise GraphQLError(
                f"Query exceeds complexity limit. "
                f"Query complexity: {complexity}, Max allowed: {MAX_COMPLEXITY}"
            )
        
        return complexity
    except Exception as e:
        raise GraphQLError(f"Complexity validation error: {str(e)}")


class ComplexityGraphQLView(GraphQLView):
    def dispatch_request(self):
        # Get the query from request
        data = request.get_json()
        query = data.get('query', '')
        
        # Validate complexity
        try:
            complexity = validate_complexity(query)
            # Store complexity in request context for logging
            request.query_complexity = complexity
        except GraphQLError as e:
            return jsonify({'errors': [{'message': str(e)}]}), 400
        
        # Proceed with normal GraphQL execution
        return super().dispatch_request()


app.add_url_rule(
    '/graphql',
    view_func=ComplexityGraphQLView.as_view(
        'graphql',
        schema=schema,
        graphiql=True
    )
)


if __name__ == '__main__':
    app.run(debug=True)

Real-World Use Cases

E-commerce Platform

Protect your e-commerce API from expensive queries:

from graphql_complexity import ComplexityEstimator, get_complexity
from graphql import build_schema

schema = build_schema("""
    directive @complexity(value: Int!) on FIELD_DEFINITION
    
    type Product {
        id: ID!
        name: String!
        description: String!
        price: Float!
        reviews: [Review!]! @complexity(value: 10)
        relatedProducts: [Product!]! @complexity(value: 15)
    }
    
    type Review {
        id: ID!
        rating: Int!
        comment: String!
        author: User!
    }
    
    type User {
        id: ID!
        name: String!
        orders: [Order!]! @complexity(value: 20)
    }
    
    type Order {
        id: ID!
        items: [Product!]!
        total: Float!
    }
    
    type Query {
        product(id: ID!): Product
        products(limit: Int, offset: Int): [Product!]!
    }
""")

# This query would be too expensive
expensive_query = """
    query ExpensiveQuery {
        products(limit: 100) {
            id
            name
            reviews {
                author {
                    orders {
                        items {
                            relatedProducts {
                                reviews {
                                    comment
                                }
                            }
                        }
                    }
                }
            }
        }
    }
"""

# Reasonable query
reasonable_query = """
    query ReasonableQuery {
        product(id: "123") {
            id
            name
            price
            description
        }
    }
"""

Social Media API

Rate limit complex social graph queries:

from graphql_complexity import ComplexityEstimator, get_complexity
from graphql import build_schema

schema = build_schema("""
    directive @complexity(value: Int!) on FIELD_DEFINITION
    
    type User {
        id: ID!
        username: String!
        followers: [User!]! @complexity(value: 10)
        following: [User!]! @complexity(value: 10)
        posts: [Post!]! @complexity(value: 5)
    }
    
    type Post {
        id: ID!
        content: String!
        author: User!
        likes: [User!]! @complexity(value: 5)
        comments: [Comment!]! @complexity(value: 3)
    }
    
    type Comment {
        id: ID!
        text: String!
        author: User!
    }
    
    type Query {
        user(id: ID!): User
        feed(limit: Int): [Post!]!
    }
""")


class SocialMediaEstimator(ComplexityEstimator):
    """Custom estimator for social media queries"""
    
    def get_field_complexity(self, node, type_info, path) -> int:
        field_name = node.name.value
        
        # Heavy penalty for deeply nested social graphs
        depth = len(path)
        depth_penalty = 2 ** depth if depth > 3 else 1
        
        # Base costs
        costs = {
            'followers': 10,
            'following': 10,
            'posts': 5,
            'likes': 5,
            'comments': 3,
            'feed': 8,
        }
        
        base_cost = costs.get(field_name, 1)
        return base_cost * depth_penalty


# Example: Preventing graph explosion
problematic_query = """
    query DeepSocialGraph {
        user(id: "1") {
            followers {
                following {
                    posts {
                        likes {
                            followers {
                                # This gets exponentially expensive!
                                username
                            }
                        }
                    }
                }
            }
        }
    }
"""

Analytics Dashboard

Manage complexity for data-heavy analytics queries:

from graphql_complexity import ComplexityEstimator, get_complexity
from graphql import build_schema
from datetime import datetime

schema = build_schema("""
    directive @complexity(value: Int!) on FIELD_DEFINITION
    
    type Analytics {
        pageViews: Int! @complexity(value: 5)
        uniqueVisitors: Int! @complexity(value: 5)
        averageSessionDuration: Float! @complexity(value: 10)
        conversionRate: Float! @complexity(value: 15)
        revenueByDay: [DailyRevenue!]! @complexity(value: 20)
    }
    
    type DailyRevenue {
        date: String!
        amount: Float!
        transactions: Int!
    }
    
    type Query {
        analytics(
            startDate: String!
            endDate: String!
            granularity: String
        ): Analytics @complexity(value: 10)
    }
""")


def calculate_analytics_complexity(query: str, date_range_days: int) -> int:
    """Calculate complexity based on date range"""
    base_complexity = get_complexity(
        query=query,
        schema=schema,
        estimator=SimpleEstimator(complexity=1)
    )
    
    # Multiply by date range (more days = more expensive)
    range_multiplier = max(1, date_range_days / 7)  # Weekly baseline
    
    return int(base_complexity * range_multiplier)

Credits

Estimators idea was heavily inspired by graphql-query-complexity.

About

Python library to compute the complexity of a GraphQL operation

Topics

python graphql strawberry graphql-core

License

MIT license

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

graphql_complexity-0.4.2.tar.gz (16.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

graphql_complexity-0.4.2-py3-none-any.whl (15.4 kB view details)

Uploaded Python 3

File details

Details for the file graphql_complexity-0.4.2.tar.gz.

File metadata

  • Download URL: graphql_complexity-0.4.2.tar.gz
  • Upload date:
  • Size: 16.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for graphql_complexity-0.4.2.tar.gz
Algorithm Hash digest
SHA256 a5dc7d9e09d87cf2bdbb3eb986051a185c8f6fdd1e97c3601119510b0f600093
MD5 f94ae48185f73dde7cb77fdac1565c2b
BLAKE2b-256 82b57c59e03e6205a090f08c939e24803f0a8ab56a5b42968815cfa56a45b55a

See more details on using hashes here.

Provenance

The following attestation bundles were made for graphql_complexity-0.4.2.tar.gz:

Publisher: python-publish.yml on Checho3388/graphql-complexity

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file graphql_complexity-0.4.2-py3-none-any.whl.

File metadata

File hashes

Hashes for graphql_complexity-0.4.2-py3-none-any.whl
Algorithm Hash digest
SHA256 271c8fc79cda21049a5db3ca438fd3c7250b0ed44458d2abcbd3259abed3349c
MD5 c05cea52d7b7b457a04a1b4bb8833fc0
BLAKE2b-256 92ce0aa0cc42376235246e59c83758879d0d924c74ef64b69d9736f2956aeb43

See more details on using hashes here.

Provenance

The following attestation bundles were made for graphql_complexity-0.4.2-py3-none-any.whl:

Publisher: python-publish.yml on Checho3388/graphql-complexity

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page