drupal-api-client
A Python client for Drupal APIs — a port of @drupal-api-client/api-client (JavaScript).
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 -efrom a different directory will shadow this one —import drupal_api_clientthen loads the other copy even thoughpytest(which usespythonpath = ["src"]) still runs against this tree. Re-runpip install -e .from the directory you intend to work in. Check withpython -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()direction —DefaultSerializerdeserializes 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
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 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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
071a578842ca22a8e2c6b3ca3ec730abe0094eb1782f1a4a7a9dfb2c50695bf7
|
|
| MD5 |
128cbd15865e32e2e4f388491dd015f4
|
|
| BLAKE2b-256 |
94d7418c60be9340a3f7fbfff70559794f361e1fe98ba427f408b93e1d12f8be
|
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
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
drupal_api_client-0.3.0.tar.gz -
Subject digest:
071a578842ca22a8e2c6b3ca3ec730abe0094eb1782f1a4a7a9dfb2c50695bf7 - Sigstore transparency entry: 2341026851
- Sigstore integration time:
-
Permalink:
VincenzoGambino/drupal-api-client-python@bc00373134452337d6bcd4e4b6f72d7173be2d62 -
Branch / Tag:
refs/tags/v0.3.0 - Owner: https://github.com/VincenzoGambino
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@bc00373134452337d6bcd4e4b6f72d7173be2d62 -
Trigger Event:
release
-
Statement type:
File details
Details for the file drupal_api_client-0.3.0-py3-none-any.whl.
File metadata
- Download URL: drupal_api_client-0.3.0-py3-none-any.whl
- Upload date:
- Size: 29.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0cf60cb4a13280377e1763d69914d539a0b126d4b0bdb606712f5137da1e469a
|
|
| MD5 |
bca7056c55b84b10c41243722e8da2d8
|
|
| BLAKE2b-256 |
f83c019662745c367bd9d093fa194d245d86e80432b150b0c794d02156b8d866
|
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
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
drupal_api_client-0.3.0-py3-none-any.whl -
Subject digest:
0cf60cb4a13280377e1763d69914d539a0b126d4b0bdb606712f5137da1e469a - Sigstore transparency entry: 2341026859
- Sigstore integration time:
-
Permalink:
VincenzoGambino/drupal-api-client-python@bc00373134452337d6bcd4e4b6f72d7173be2d62 -
Branch / Tag:
refs/tags/v0.3.0 - Owner: https://github.com/VincenzoGambino
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@bc00373134452337d6bcd4e4b6f72d7173be2d62 -
Trigger Event:
release
-
Statement type: