API Reference¶
Generated in part from source docstrings via mkdocstrings.
Integration points¶
IdempotencyMiddleware¶
Applies idempotency-key handling to every matching request.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
get_response
|
Callable[[HttpRequest], HttpResponse]
|
The next middleware/view in the chain, provided by Django's middleware machinery. |
required |
Source code in src/drf_idempotency/middleware.py
19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | |
__call__(request)
¶
Process one request.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
request
|
HttpRequest
|
The incoming request. |
required |
Returns:
| Type | Description |
|---|---|
HttpResponse
|
The response — either freshly produced or replayed from a |
HttpResponse
|
prior identical request. Requests whose method isn't in the |
HttpResponse
|
configured |
HttpResponse
|
Idempotency-specific errors (invalid/missing key, key reuse, |
HttpResponse
|
a concurrent request in progress) are returned as a JSON |
HttpResponse
|
error response with the appropriate status code, rather than |
HttpResponse
|
propagating as raw exceptions — plain Django middleware has |
HttpResponse
|
no equivalent of DRF's own exception handling to do this |
HttpResponse
|
automatically. |
Source code in src/drf_idempotency/middleware.py
30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | |
idempotent¶
Apply idempotency-key handling to a single view function or method.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
methods
|
Sequence[str] | None
|
HTTP methods this decorator applies to. Requests using
any other method pass through untouched. Defaults to the
|
None
|
Returns:
| Type | Description |
|---|---|
Callable[[_F], _F]
|
A decorator that wraps the view, applying the same |
Callable[[_F], _F]
|
acquire-or-replay logic as |
Callable[[_F], _F]
|
class: |
Example
.. code-block:: python
from rest_framework.decorators import api_view
from drf_idempotency import idempotent
@api_view(["POST"])
@idempotent()
def create_payment(request):
...
class PaymentViewSet(viewsets.ViewSet):
@idempotent()
def create(self, request):
...
Source code in src/drf_idempotency/decorators.py
23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 | |
Status tracking¶
get_idempotency_status¶
Return the current status of an idempotency key.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
key
|
str
|
The idempotency key to look up. |
required |
Returns:
| Type | Description |
|---|---|
str | None
|
|
str | None
|
unknown (never used, or its record has expired). Note that a |
str | None
|
request that failed (raised an exception, or produced an |
str | None
|
uncached error response) releases its key entirely — see |
str | None
|
|
str | None
|
from "never attempted" via this function; that is by design, since |
str | None
|
it means the key remains free for a fresh, successful attempt. |
Source code in src/drf_idempotency/status.py
12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | |
Fingerprinting¶
compute_fingerprint¶
Compute a stable fingerprint of a request's method, path, and body.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
request
|
HttpRequest
|
The incoming request. |
required |
Returns:
| Type | Description |
|---|---|
str
|
A 64-character SHA-256 hex digest. |
Source code in src/drf_idempotency/fingerprint.py
21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | |
Backends¶
BaseBackend¶
Bases: ABC
Abstract base class for idempotency storage backends.
Subclass this and implement every abstract method to add a new
storage backend. See :class:drf_idempotency.backends.redis.RedisBackend
and :class:drf_idempotency.backends.database.DatabaseBackend for
reference implementations.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
**options
|
Any
|
Backend-specific keyword arguments, passed through from
the |
{}
|
Source code in src/drf_idempotency/backends/base.py
70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 | |
acquire_or_get(key, fingerprint, *, lock_ttl)
abstractmethod
¶
Atomically claim key, or return its current state.
This is the operation race-safety depends on: if two requests
call this concurrently with the same key, exactly one must
receive AcquireResult(acquired=True, existing=None) and the
other(s) must receive AcquireResult(acquired=False, existing=...).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
key
|
str
|
The idempotency key to claim. |
required |
fingerprint
|
str
|
The requesting request's fingerprint, stored alongside the key if this call acquires it. |
required |
lock_ttl
|
timedelta
|
How long the claim is held before it's considered abandoned and eligible to be reclaimed by a later call. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
An |
AcquireResult
|
class: |
AcquireResult
|
claimed the key. |
Source code in src/drf_idempotency/backends/base.py
86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 | |
cleanup_expired()
abstractmethod
¶
Remove expired records.
For backends with native TTL support (e.g. Redis), this is
typically a no-op returning 0, since expiry is handled by the
store itself. For backends without native expiry (e.g. the
database backend), this performs the actual deletion and should
be called periodically — see the cleanup_expired_idempotency_keys
management command.
Returns:
| Type | Description |
|---|---|
int
|
The number of records removed. |
Source code in src/drf_idempotency/backends/base.py
153 154 155 156 157 158 159 160 161 162 163 164 165 166 | |
complete(key, *, status_code, headers, body, ttl)
abstractmethod
¶
Store the final response for a successfully-processed request.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
key
|
str
|
The idempotency key (must currently be held by the
caller, i.e. previously returned |
required |
status_code
|
int
|
The HTTP status code to replay on future lookups. |
required |
headers
|
Mapping[str, str]
|
The HTTP headers to replay. |
required |
body
|
bytes
|
The raw response body bytes to replay. |
required |
ttl
|
timedelta
|
How long the completed record remains eligible for replay before it expires. |
required |
Source code in src/drf_idempotency/backends/base.py
107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 | |
fail(key)
abstractmethod
¶
Release a claimed key without storing a response.
Called when request processing raised an unhandled exception or produced a server error (5xx) that should not be cached — the key becomes available for a fresh attempt.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
key
|
str
|
The idempotency key to release. |
required |
Source code in src/drf_idempotency/backends/base.py
129 130 131 132 133 134 135 136 137 138 139 | |
get(key)
abstractmethod
¶
Look up a key's current record without claiming it.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
key
|
str
|
The idempotency key to look up. |
required |
Returns:
| Type | Description |
|---|---|
StoredRecord | None
|
The current :class: |
StoredRecord | None
|
unknown (or has expired). |
Source code in src/drf_idempotency/backends/base.py
141 142 143 144 145 146 147 148 149 150 151 | |
StoredRecord¶
The stored state of one idempotency key.
Attributes:
| Name | Type | Description |
|---|---|---|
key |
str
|
The idempotency key this record belongs to. |
fingerprint |
str
|
The fingerprint of the request that first used this
key (see :func: |
status |
str
|
One of |
response_status_code |
int | None
|
The stored response's HTTP status code, or
|
response_headers |
Mapping[str, str] | None
|
The stored response's headers, or |
response_body |
bytes | None
|
The stored response's raw body bytes, or |
created_at |
datetime
|
When the key was first claimed. |
Source code in src/drf_idempotency/backends/base.py
24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | |
AcquireResult¶
The outcome of calling :meth:BaseBackend.acquire_or_get.
Attributes:
| Name | Type | Description |
|---|---|---|
acquired |
bool
|
|
existing |
StoredRecord | None
|
The current stored record, populated whenever
|
Source code in src/drf_idempotency/backends/base.py
51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 | |
DatabaseBackend¶
Bases: BaseBackend
Stores idempotency records in the database via
:class:~drf_idempotency.models.IdempotencyRecord.
Requires "drf_idempotency" to be listed in INSTALLED_APPS and
migrated (python manage.py migrate).
Source code in src/drf_idempotency/backends/database.py
25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 | |
acquire_or_get(key, fingerprint, *, lock_ttl)
¶
See :meth:drf_idempotency.backends.base.BaseBackend.acquire_or_get.
Source code in src/drf_idempotency/backends/database.py
33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 | |
cleanup_expired()
¶
See :meth:drf_idempotency.backends.base.BaseBackend.cleanup_expired.
Source code in src/drf_idempotency/backends/database.py
117 118 119 120 | |
complete(key, *, status_code, headers, body, ttl)
¶
See :meth:drf_idempotency.backends.base.BaseBackend.complete.
Source code in src/drf_idempotency/backends/database.py
79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 | |
fail(key)
¶
See :meth:drf_idempotency.backends.base.BaseBackend.fail.
Deletes the record entirely, so the next request using this key starts fresh rather than being permanently stuck.
Source code in src/drf_idempotency/backends/database.py
99 100 101 102 103 104 105 | |
get(key)
¶
See :meth:drf_idempotency.backends.base.BaseBackend.get.
Source code in src/drf_idempotency/backends/database.py
107 108 109 110 111 112 113 114 115 | |
RedisBackend¶
Bases: BaseBackend
Stores idempotency records in Redis.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
url
|
str | None
|
A |
None
|
client
|
Any | None
|
An existing |
None
|
key_prefix
|
str
|
Prefix applied to every Redis key this backend writes, to avoid collisions with unrelated keys in a shared Redis instance. |
_DEFAULT_KEY_PREFIX
|
**options
|
Any
|
Accepted for interface compatibility; unused. |
{}
|
Source code in src/drf_idempotency/backends/redis.py
33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 | |
acquire_or_get(key, fingerprint, *, lock_ttl)
¶
See :meth:drf_idempotency.backends.base.BaseBackend.acquire_or_get.
Source code in src/drf_idempotency/backends/redis.py
66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 | |
cleanup_expired()
¶
See :meth:drf_idempotency.backends.base.BaseBackend.cleanup_expired.
Always returns 0 — Redis expires keys natively via EX, so
there is nothing for this method to do.
Source code in src/drf_idempotency/backends/redis.py
119 120 121 122 123 124 125 | |
complete(key, *, status_code, headers, body, ttl)
¶
See :meth:drf_idempotency.backends.base.BaseBackend.complete.
Source code in src/drf_idempotency/backends/redis.py
86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 | |
fail(key)
¶
See :meth:drf_idempotency.backends.base.BaseBackend.fail.
Source code in src/drf_idempotency/backends/redis.py
108 109 110 | |
get(key)
¶
See :meth:drf_idempotency.backends.base.BaseBackend.get.
Source code in src/drf_idempotency/backends/redis.py
112 113 114 115 116 117 | |
Models¶
IdempotencyRecord¶
Bases: Model
A single idempotency key's stored state and (eventually) response.
Attributes:
| Name | Type | Description |
|---|---|---|
key |
The client-supplied idempotency key. Unique — this uniqueness
constraint is exactly what makes concurrent lock acquisition
race-safe (see |
|
fingerprint |
A SHA-256 hex digest of the request method, path, and
body, computed by :func: |
|
status |
One of |
|
response_status_code |
The HTTP status code of the stored response, once completed. |
|
response_headers |
The stored response headers, as a JSON object. |
|
response_body |
The stored response body, base64-encoded (so binary bodies round-trip safely through a text column). |
|
created_at |
When this record was first created (lock acquired). |
|
completed_at |
When the response was stored, if it has been. |
|
expires_at |
When this record should be treated as gone. Rows past
this timestamp are ignored by reads and removed by the
|
Source code in src/drf_idempotency/models.py
14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 | |
Exceptions¶
IdempotencyError¶
Bases: APIException
Base class for all errors raised by this package.
Source code in src/drf_idempotency/exceptions.py
15 16 17 18 | |
MissingIdempotencyKeyError¶
Bases: IdempotencyError
Raised when REQUIRE_KEY is enabled and a request using a
configured method omits the idempotency key header.
Source code in src/drf_idempotency/exceptions.py
21 22 23 24 25 26 27 | |
InvalidIdempotencyKeyError¶
Bases: IdempotencyError
Raised when a supplied idempotency key fails validation (empty, too long, or containing disallowed characters).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
reason
|
str
|
A human-readable description of why the key is invalid. |
required |
Source code in src/drf_idempotency/exceptions.py
30 31 32 33 34 35 36 37 38 39 40 41 42 | |
IdempotencyKeyReuseError¶
Bases: IdempotencyError
Raised when an idempotency key is reused with a request whose fingerprint (method, path, and body) does not match the original request that key was first used for.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
key
|
str
|
The reused idempotency key. |
required |
Source code in src/drf_idempotency/exceptions.py
45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 | |
ConcurrentRequestError¶
Bases: IdempotencyError
Raised when another request using the same idempotency key is already being processed.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
key
|
str
|
The idempotency key currently locked by another request. |
required |
Source code in src/drf_idempotency/exceptions.py
67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 | |
Settings¶
See Settings for the full list of recognized
IDEMPOTENCY keys.
Management command¶
cleanup_expired_idempotency_keys¶
python manage.py cleanup_expired_idempotency_keys [--quiet]
See Deployment.