Metadata-Version: 2.5
Name: django-graphex
Version: 4.1.0
Summary: GraphQL for Django, powered by graphql-core and Pydantic: define types/mutations/subscriptions from your models, with nested filtering (and/or/not), pagination, permissions and directives.
Project-URL: Homepage, https://github.com/eamigo86/django-graphex
Project-URL: Repository, https://github.com/eamigo86/django-graphex
Project-URL: Documentation, https://eamigo86.github.io/django-graphex/
Author-email: Ernesto Perez Amigo <eamigop86@gmail.com>
License-Expression: MIT
License-File: LICENSE
Keywords: api,django,graphql,protocol
Classifier: Development Status :: 5 - Production/Stable
Classifier: Framework :: Django
Classifier: Framework :: Django :: 5.2
Classifier: Framework :: Django :: 6.0
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Software Development :: Libraries
Requires-Python: <4.0,>=3.12
Requires-Dist: django<7.0,>=5.2
Requires-Dist: graphql-core<3.4,>=3.3.0
Requires-Dist: pydantic<3,>=2
Requires-Dist: python-dateutil<3.0,>=2.8.2
Requires-Dist: text-unidecode<2.0,>=1.3
Provides-Extra: subscriptions
Requires-Dist: channels-redis<5.0,>=4.2; extra == 'subscriptions'
Requires-Dist: channels<5.0,>=4.0; extra == 'subscriptions'
Description-Content-Type: text/markdown

# django-graphex

![Codecov](https://img.shields.io/codecov/c/github/eamigo86/django-graphex)
![PyPI - Python Version](https://img.shields.io/pypi/pyversions/django-graphex)
![Django Versions](https://img.shields.io/pypi/frameworkversions/django/django-graphex?label=django&color=0C4B33)
![PyPI](https://img.shields.io/pypi/v/django-graphex?color=blue)
![PyPI - License](https://img.shields.io/pypi/l/django-graphex)
![Downloads](https://img.shields.io/pepy/dt/django-graphex)
![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)

**GraphQL for Django, powered by [graphql-core](https://github.com/graphql-python/graphql-core)
and [Pydantic](https://docs.pydantic.dev/).** Define your GraphQL API straight from
your Django models — no DRF, no graphene, no `django-filter`.

- **Model-first types & mutations** — `DjangoModelType` / `DjangoModelMutation`
  give you query, list and create/update/delete from a single `Meta.model`,
  validated and persisted with **Pydantic v2** + the Django ORM (FK existence,
  uniqueness, `unique_together`, partial updates, `choices` → Enum).
- **Logical filtering** — one nested `filter:` argument with `and` / `or` / `not`,
  per-field lookups, relation descent and plain-pk/UUID filtering (no `django-filter`).
- **Pagination** — limit/offset, page and keyset **cursor** paginators with a
  uniform `results` / `totalCount` shape (and an automatic N+1 query optimizer).
- **Custom validation** — DRF-style inline `validate_<field>()` / `validate()` or a
  `Meta.pydantic_model`.
- **Permissions, security & directives** — permission classes, depth & cost limits,
  introspection control, and string/number/date/list directives. `Meta.only_fields`
  / `Meta.exclude_fields` are a **security boundary**: a column a type hides is
  unreadable, unorderable **and** unfilterable through it.
- **Subscriptions** — real-time GraphQL over Django Channels 4 (optional extra).

> **Coming from `graphene-django` or `graphene-django-extras`?** See the
> [Migration Guide](https://eamigo86.github.io/django-graphex/migration/) for a
> step-by-step upgrade with before/after examples.
>
> **Upgrading from `django-graphex` 1.x?** 2.0 removed the graphene backend
> entirely — see the [Upgrade Guide](https://eamigo86.github.io/django-graphex/UPGRADE-2.0/)
> and the `scripts/migrate_2_0.py` codemod.

## Requirements

- **Python:** 3.12+ (3.13, 3.14 supported)
- **Django:** 5.2+ (5.2 LTS, 6.0 supported) — each Django version tested on the Python versions it officially supports
- **graphql-core:** >=3.3.0,<3.4
- **pydantic:** >=2,<3

**django-graphex 4.1.0 requires `graphql-core>=3.3.0,<3.4`.** The historical
3.1.1 release requires `graphql-core>=3.2.13,<3.3`. Follow the
[4.0 upgrade guide](docs/UPGRADE-4.0.md) when migrating custom executors,
AST builders or subscription transports, and the
[4.1 upgrade guide](docs/UPGRADE-4.1.md) when moving from 4.0.

## Installation

```bash
# uv (recommended)
uv add "django-graphex==4.1.0"
# real-time subscriptions (adds Django Channels 4):
uv add "django-graphex[subscriptions]==4.1.0"
```

```bash
# pip
pip install "django-graphex==4.1.0"
pip install "django-graphex[subscriptions]==4.1.0"
```

The base install never imports `channels`; only the `subscriptions` extra does.

## Quick start

```python
from django.contrib.auth import get_user_model
from django.urls import path
from django_graphex.core import BooleanField, CharField, Field, Mutation, ObjectType
from django_graphex.fields import DjangoListObjectField, DjangoObjectField
from django_graphex.paginations import LimitOffsetGraphqlPagination
from django_graphex.schema import DjangoGraphQLSchema
from django_graphex.types import DjangoListObjectType, DjangoObjectType
from django_graphex.views import AuthenticatedGraphQLView

User = get_user_model()


class UserType(DjangoObjectType):
    class Meta:
        model = User
        only_fields = ("id", "username", "first_name", "last_name")
        filter_fields = {"username": ("icontains", "exact")}


class UserListType(DjangoListObjectType):
    class Meta:
        model = User
        pagination = LimitOffsetGraphqlPagination()


class RegisterUser(Mutation):
    ok = BooleanField()
    user = Field(UserType)

    class Arguments:
        username = CharField(required=True)
        password = CharField(required=True)

    @classmethod
    def mutate(cls, root, info, username, password):
        user = User.objects.create_user(username=username, password=password)
        return cls(ok=True, user=user)


class Query(ObjectType):
    user = DjangoObjectField(UserType)
    users = DjangoListObjectField(UserListType)


class Mutation(ObjectType):
    register_user = RegisterUser.Field()


schema = DjangoGraphQLSchema(query=Query, mutation=Mutation)

urlpatterns = [
    path("graphql/", AuthenticatedGraphQLView.as_view(schema=schema, graphiql=True)),
]
```

`User` is deliberately **not** mounted through a generic model mutation:
account creation must call `create_user()` so the password is hashed, and the
client must never choose staff/superuser flags. Use generated mutations for
ordinary application models; see the mutation guide.

Query it with the nested `filter:` argument (`and` / `or` / `not`):

```graphql
{
  users(filter: { isActive: { exact: true }, username: { icontains: "jo" } }) {
    results(limit: 10, ordering: "-date_joined") { id username }
    totalCount
  }
}
```

## Configuration

All settings live under a single `DJANGO_GRAPHEX` dict (every key is optional):

```python
# settings.py
DJANGO_GRAPHEX = {
    "DEFAULT_PAGINATION_CLASS": "django_graphex.paginations.LimitOffsetGraphqlPagination",
    "DEFAULT_PAGE_SIZE": 20,
    "MAX_PAGE_SIZE": 50,
    # Response caching. Default is False (disabled).
    # WARNING: cache keys are identity-salted per user (v1.2.1+), but shared
    # caches can still leak data if misconfigured. Review the caching guide
    # before enabling in production: docs/usage/caching.md
    "CACHE_ACTIVE": False,
}
```

When response caching is enabled, only HTTP 200 GraphQL data responses without
errors are stored or reused; a failed request is executed again rather than
replaying an old cached error. See the [caching guide](docs/usage/caching.md).

The built-in HTTP views are synchronous. Async resolvers or middleware are not
executed there: an awaitable execution returns HTTP 500 with a controlled
GraphQL JSON error asking for synchronous resolvers and middleware. See the
[view contract](docs/usage/views.md#custom-execution-backend).

The library default is `DEFAULT_PAGE_SIZE: 100` and `MAX_PAGE_SIZE: None`.
Omit `DEFAULT_PAGE_SIZE` to inherit 100 for LimitOffset, Page, and Cursor
paginators; the explicit `20` above is an application override. A default is
not a maximum: configure `MAX_PAGE_SIZE` to cap client-requested sizes. Clients
that previously relied on an unbounded omitted-size list should paginate past
the first 100 rows or explicitly opt out; `None` has paginator-specific legacy
behavior and does not mean “all rows” for Page pagination.

With `MAX_QUERY_COST` configured, `GraphQLView` checks the selected operation
against GraphQL-coerced request variables before running resolvers. A variable
limit above the budget now receives HTTP 400 / `QUERY_TOO_COMPLEX`, even if its
static fallback estimate passed; `MAX_PAGE_SIZE` remains the recommended hard
ceiling for lists with omitted sizes.

`CLEAN_RESPONSE` is off by default. When enabled for `GraphQLView`, it removes
nulls, empty strings, and empty containers recursively, but retains numeric
zero and `false`. Selected introspection operations bypass cleaning. See
[response cleaning](https://eamigo86.github.io/django-graphex/usage/security/#ast-based-introspection-detection-clean_response).

For bounded model-backed limit/offset and page-number results, ties in the effective
ordering use the primary key ascending unless it is already an ordering term.
This preserves an explicit descending key, but does not prevent offset shifts
when rows change between requests. See [pagination ordering](https://eamigo86.github.io/django-graphex/usage/pagination/#stable-ordering-for-offset-and-page-results).

Two keys are **not** in that dict on purpose — both ship enabled, and pinning a
key to its own default only hides that you depend on it:

- **`REQUIRE_CSRF_HEADER`** (`True`) demands an `X-Requested-With` header on
  form-encoded and `multipart/form-data` POSTs, which a browser can send
  cross-site with no CORS preflight. `application/json` clients are unaffected;
  form-encoded and multipart clients get **HTTP 403** without it.
- **`MAX_SUBSCRIPTIONS_PER_CONNECTION`** (`50`) caps the concurrent operations
  one `graphql-transport-ws` socket may hold.

Both are documented in
[Settings](https://eamigo86.github.io/django-graphex/usage/settings/), and both
are breaking for clients written against 2.2.0.

To use directives, add the middleware and pass `all_directives` to the schema:

```python
DJANGO_GRAPHEX = {"MIDDLEWARE": ["django_graphex.middleware.GraphQLDirectiveMiddleware"]}

from django_graphex.directives import all_directives
from django_graphex.schema import DjangoGraphQLSchema
schema = DjangoGraphQLSchema(query=Query, mutation=Mutation, directives=all_directives)
```

## Playground

A fully wired example project lives in [`examples/playground/`](examples/playground/). It exercises every major feature end-to-end — types, paginators, filtering, mutations, permissions, subscriptions, file uploads on both paths, the projection boundary on all three axes, and the query optimizer — and installs the library from this repo checkout (editable, no PyPI release needed). `make test` runs the end-to-end suite; several tests assert the verbatim answer strings its README quotes.

## Documentation

📚 **[Full documentation](https://eamigo86.github.io/django-graphex/)** — including the
[Quick Start](https://eamigo86.github.io/django-graphex/quickstart/),
[Model backend](https://eamigo86.github.io/django-graphex/usage/backends/),
[Filtering](https://eamigo86.github.io/django-graphex/usage/filtering/),
[Pagination](https://eamigo86.github.io/django-graphex/usage/pagination/),
[Subscriptions](https://eamigo86.github.io/django-graphex/usage/subscriptions/),
[Settings](https://eamigo86.github.io/django-graphex/usage/settings/) and the
[Migration Guide](https://eamigo86.github.io/django-graphex/migration/).

The [current core33 comparison](docs/why.md#current-core33-comparison) uses
eight committed portable results in
core33-4.1.0-f9f4eae81954959ab71e4a9037d5ba06486d6a8e, measured from
4.1.0 source f9f4eae81954959ab71e4a9037d5ba06486d6a8e, not a published
wheel. In the local SQLite
create-comment operation, GraphEx/Ariadne means were 0.5159 vs 0.9268 ms at
50,000 comments (44.34% less mean time for GraphEx) and 0.5549 vs 0.9382 ms
at 100,000 (40.85% less mean time). The separate p50 ratios were 0.54×
(0.4758/0.8754 ms) and 0.51× (0.4592/0.8978 ms), respectively.
These are median-of-three-run statistics, not a paired historical comparison:
the four whole stacks differ in graphql-core version and the host load changed.
The result is not a PostgreSQL or universal performance claim. The preceding
core33-4.1.0-460efb9b7c29b097f2939c05e79514f2fda3242c and older
series remain historical.

## License

MIT License — see the [LICENSE](LICENSE) file.
