A python library that provides complexity calculation helpers for GraphQL
Project description
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.
✨ Why GraphQL Complexity?
Protect your GraphQL API from expensive queries and potential DoS attacks by calculating query complexity before execution.
5,500+ downloads • Used in production • Actively 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
- Getting Started
- Estimators
- Advanced Examples
- Framework Integration
- Real-World Use Cases
- Credits
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
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
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a5dc7d9e09d87cf2bdbb3eb986051a185c8f6fdd1e97c3601119510b0f600093
|
|
| MD5 |
f94ae48185f73dde7cb77fdac1565c2b
|
|
| BLAKE2b-256 |
82b57c59e03e6205a090f08c939e24803f0a8ab56a5b42968815cfa56a45b55a
|
Provenance
The following attestation bundles were made for graphql_complexity-0.4.2.tar.gz:
Publisher:
python-publish.yml on Checho3388/graphql-complexity
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
graphql_complexity-0.4.2.tar.gz -
Subject digest:
a5dc7d9e09d87cf2bdbb3eb986051a185c8f6fdd1e97c3601119510b0f600093 - Sigstore transparency entry: 937928680
- Sigstore integration time:
-
Permalink:
Checho3388/graphql-complexity@725c5c3772d79d4abdd17a15fd56a6aa5852d35a -
Branch / Tag:
refs/heads/main - Owner: https://github.com/Checho3388
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
python-publish.yml@725c5c3772d79d4abdd17a15fd56a6aa5852d35a -
Trigger Event:
workflow_dispatch
-
Statement type:
File details
Details for the file graphql_complexity-0.4.2-py3-none-any.whl.
File metadata
- Download URL: graphql_complexity-0.4.2-py3-none-any.whl
- Upload date:
- Size: 15.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
271c8fc79cda21049a5db3ca438fd3c7250b0ed44458d2abcbd3259abed3349c
|
|
| MD5 |
c05cea52d7b7b457a04a1b4bb8833fc0
|
|
| BLAKE2b-256 |
92ce0aa0cc42376235246e59c83758879d0d924c74ef64b69d9736f2956aeb43
|
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
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
graphql_complexity-0.4.2-py3-none-any.whl -
Subject digest:
271c8fc79cda21049a5db3ca438fd3c7250b0ed44458d2abcbd3259abed3349c - Sigstore transparency entry: 937928727
- Sigstore integration time:
-
Permalink:
Checho3388/graphql-complexity@725c5c3772d79d4abdd17a15fd56a6aa5852d35a -
Branch / Tag:
refs/heads/main - Owner: https://github.com/Checho3388
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
python-publish.yml@725c5c3772d79d4abdd17a15fd56a6aa5852d35a -
Trigger Event:
workflow_dispatch
-
Statement type: