Skip to main content

drupal-api-client

A Python client for Drupal APIs — a port of @drupal-api-client/api-client (JavaScript).

CI PyPI

What's included

  • ApiClient — base HTTP client with auth, caching, logging, and serializer hooks.
  • DecoupledRouterClient — resolves Drupal path aliases via the Decoupled Router module.
  • JsonApiClient — full CRUD over Drupal's JSON:API module.

Installation

pip install drupal-api-client

For building query strings, install the companion package:

pip install drupal-jsonapi-params

Local development

Install this checkout in editable mode from the repository root so that import drupal_api_client resolves to the code you're editing:

pip install -e .

Heads-up: if you have more than one checkout of this project, an earlier pip install -e from a different directory will shadow this one — import drupal_api_client then loads the other copy even though pytest (which uses pythonpath = ["src"]) still runs against this tree. Re-run pip install -e . from the directory you intend to work in. Check with python -c "import drupal_api_client, os; print(os.path.dirname(drupal_api_client.__file__))".

Quick start

Reading a collection

from drupal_api_client import JsonApiClient

with JsonApiClient("https://example.com") as client:
    articles = client.get_collection("node--article")
    for article in articles["data"]:
        print(article["attributes"]["title"])

Reading with filters (using drupal-jsonapi-params)

from drupal_api_client import JsonApiClient
from drupal_jsonapi_params import DrupalJsonApiParams, FilterOperator

params = (
    DrupalJsonApiParams()
    .add_filter("status", "1")
    .add_filter("title", "Hello", FilterOperator.CONTAINS)
    .add_include(["field_image"])
    .add_page_limit(10)
)

with JsonApiClient("https://example.com") as client:
    articles = client.get_collection("node--article", query_string=params)

Resolving a path alias

from drupal_api_client import JsonApiClient

with JsonApiClient("https://example.com") as client:
    article = client.get_resource_by_path("/about-us")
    print(article["data"]["attributes"]["title"])

Authenticated writes

from drupal_api_client import JsonApiClient, BasicAuth

auth = BasicAuth(username="admin", password="secret")
with JsonApiClient("https://example.com", authentication=auth) as client:
    new_article = client.create_resource(
        "node--article",
        {
            "data": {
                "type": "node--article",
                "attributes": {"title": "New article", "body": {"value": "..."}},
            }
        },
    )

    client.update_resource(
        "node--article",
        new_article["data"]["id"],
        {"data": {"type": "node--article", "id": new_article["data"]["id"], "attributes": {"title": "Updated"}}},
    )

    client.delete_resource("node--article", new_article["data"]["id"])

Authentication

Three auth types are supported:

from drupal_api_client import BasicAuth, OAuthAuth, CustomAuth

# HTTP Basic
BasicAuth(username="admin", password="secret")

# OAuth2 (client_credentials or password grant)
OAuthAuth(client_id="...", client_secret="...")
OAuthAuth(client_id="...", client_secret="...", grant_type="password",
          username="...", password="...")

# Custom (passed verbatim into the Authorization header)
CustomAuth(value="Bearer my-token-here")

Caching

Pass any object implementing the Cache protocol (get, set, delete):

from drupal_api_client import JsonApiClient, InMemoryCache

with JsonApiClient("https://example.com", cache=InMemoryCache()) as client:
    client.get_resource("node--article", "abc-123")  # HTTP call
    client.get_resource("node--article", "abc-123")  # cache hit, no HTTP

Write methods invalidate the canonical cached entries for the affected resource. Cache entries with locales or query strings are not auto-invalidated — pass disable_cache=True to bypass them, or implement a custom cache with prefix-based invalidation.

Discriminated unions

get_resource_by_path raises ResourceNotFoundError when the path can't be resolved. For lower-level access, DecoupledRouterClient.translate_path returns a discriminated union:

from drupal_api_client import DecoupledRouterClient, ResolvedPath, UnresolvedPath

with DecoupledRouterClient("https://example.com") as router:
    result = router.translate_path("/about-us")
    match result:
        case ResolvedPath(entity=entity, label=label):
            print(f"Found {label}: {entity['uuid']}")
        case UnresolvedPath(message=msg):
            print(f"Not found: {msg}")

Testing against a live Drupal site

The default test suite (pytest) mocks HTTP via respx and needs no running Drupal instance. A separate, opt-in module, tests/test_live_integration.py, runs the same kinds of operations against a real site instead — useful for catching cases where a mocked fixture has drifted from what the real API actually returns:

DRUPAL_API_CLIENT_LIVE_BASE_URL=https://your-site.ddev.site pytest -m live

It's skipped automatically when the env var isn't set. It was developed against a ddev-hosted Drupal 11 site running the Umami demo profile's content, with the jsonapi core module and the decoupled_router contrib module enabled.

The live suite covers reads, path resolution, per-locale index lookup, the DefaultSerializer (including relationship inlining), and the async clients. Write tests (create/update/delete) additionally need credentials for a user with article CRUD + editorial-transition permissions:

DRUPAL_API_CLIENT_LIVE_BASE_URL=https://your-site.ddev.site \
DRUPAL_API_CLIENT_LIVE_USERNAME=apitest \
DRUPAL_API_CLIENT_LIVE_PASSWORD=your-password \
    pytest -m live

They're skipped if the credential vars are absent. See the module docstring in tests/test_live_integration.py for the exact drush commands to provision such a user on a ddev Umami site.

GraphQL

from drupal_api_client import GraphqlClient

with GraphqlClient("https://drupal.example.com") as client:
    result = client.query("query { nodeArticles(first: 10) { nodes { title } } }")

Async

Every client has an async counterpart on httpx.AsyncClient (AsyncApiClient, AsyncJsonApiClient, AsyncDecoupledRouterClient, AsyncGraphqlClient), used via async with:

from drupal_api_client import AsyncJsonApiClient

async with AsyncJsonApiClient("https://drupal.example.com") as client:
    recipes = await client.get_collection("node--recipe")
    recipe = await client.get_resource_by_path("/recipes/my-recipe")

Deserializing responses

By default responses are returned as parsed JSON:API dicts. Pass DefaultSerializer to flatten resources (hoist attributes, inline relationships from included) and expose get_meta()/get_links():

from drupal_api_client import DefaultSerializer, JsonApiClient

with JsonApiClient("https://drupal.example.com", serializer=DefaultSerializer()) as client:
    article = client.get_resource("node--article", "<uuid>")
    print(article["title"])          # attributes hoisted, no `attributes` wrapper
    print(article.get_meta())        # document-level meta

Not yet included

  • serialize() directionDefaultSerializer deserializes only; build JSON:API request bodies directly.
  • A structured, per-instance injectable logger object (Python uses stdlib logging; see below).

Logging

Configure standard Python logging:

import logging
logging.basicConfig(level=logging.DEBUG)
logging.getLogger("drupal_api_client").setLevel(logging.DEBUG)

Compatibility

  • Python 3.10+
  • Drupal 9.x, 10.x, 11.x with the JSON:API module enabled
  • Optional Drupal modules: Decoupled Router (for path resolution), JSON:API Views (for get_view)

License

ISC. See LICENSE. Original JavaScript implementation is MIT-licensed by the Drupal API Client contributors; see NOTICE.

Contributing

Issues and pull requests welcome at github.com/VincenzoGambino/drupal-api-client-python.

Download files

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

Source Distribution

drupal_api_client-0.3.0.tar.gz (59.9 kB view details)

Uploaded Source

Built Distribution

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

drupal_api_client-0.3.0-py3-none-any.whl (29.9 kB view details)

Uploaded Python 3

File details

Details for the file drupal_api_client-0.3.0.tar.gz.

File metadata

  • Download URL: drupal_api_client-0.3.0.tar.gz
  • Upload date:
  • Size: 59.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for drupal_api_client-0.3.0.tar.gz
Algorithm Hash digest
SHA256 071a578842ca22a8e2c6b3ca3ec730abe0094eb1782f1a4a7a9dfb2c50695bf7
MD5 128cbd15865e32e2e4f388491dd015f4
BLAKE2b-256 94d7418c60be9340a3f7fbfff70559794f361e1fe98ba427f408b93e1d12f8be

See more details on using hashes here.

Provenance

The following attestation bundles were made for drupal_api_client-0.3.0.tar.gz:

Publisher: publish.yml on VincenzoGambino/drupal-api-client-python

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

File details

Details for the file drupal_api_client-0.3.0-py3-none-any.whl.

File metadata

File hashes

Hashes for drupal_api_client-0.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 0cf60cb4a13280377e1763d69914d539a0b126d4b0bdb606712f5137da1e469a
MD5 bca7056c55b84b10c41243722e8da2d8
BLAKE2b-256 f83c019662745c367bd9d093fa194d245d86e80432b150b0c794d02156b8d866

See more details on using hashes here.

Provenance

The following attestation bundles were made for drupal_api_client-0.3.0-py3-none-any.whl:

Publisher: publish.yml on VincenzoGambino/drupal-api-client-python

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

Release history Release notifications | RSS feed

This release

0.3.0 This release

2 files

0.2.0

2 files

Supported by

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