Skip to main content

Django-EasyJWT

PyPI version Python Django License: MIT

A Django package for implementing remote JWT authentication in microservice architectures. It provides a centralized authentication service with multiple client services authenticating against it.

Table of Contents


Why Django-EasyJWT?

When managing multiple services with the same users, a centralized authentication service eliminates password confusion and provides a single source of truth. Django-EasyJWT was built for scenarios where:

  • Multiple services share the same user base
  • Different services require different access levels
  • You need custom user data and permissions passed through authentication
  • You want to keep your auth service lean and behind a private network

Features

  • JWT Authentication: Access/refresh token pairs and sliding tokens
  • Remote Auth: Client services authenticate against a central auth service
  • Stateless Auth: Optional token-only authentication with no DB lookup (JWTStatelessUserAuthentication)
  • Session Support: Authenticated users can access Django admin
  • Custom User Model: Optional email-based user model included
  • Token Blacklisting: Optional token revocation via outstanding/blacklist tracking
  • Password Security: Django password validators enforced on create-user and password-change
  • User Registration: Built-in create-user/ endpoint with email normalization
  • Password Change: Built-in password-change/ endpoint with credential verification
  • Sliding Tokens: Optional single-token JWT with embedded refresh window
  • Extensible: Custom serializers for additional user data
  • Configurable: HTTP timeouts, SSL verification, and configurable endpoint paths
  • Production Warnings: Automatic logging when SSL or insecure HTTP is configured
  • Refresh-Token Cookies: Optional HttpOnly cookie storage with CSRF protection and logout blacklisting for browser-based SPAs

Installation

uv add django-easyjwt

Or with pip:

pip install django-easyjwt

Architecture Overview

┌─────────────────┐         ┌─────────────────┐
│  Client-Service │ ──────► │   Auth-Service  │
│   (Port 8001)   │         │   (Port 8000)   │
├─────────────────┤         ├─────────────────┤
│ easyjwt_client  │         │  easyjwt_auth   │
│ easyjwt_user    │         │  easyjwt_user   │
└─────────────────┘         └─────────────────┘
        │                           │
        ▼                           ▼
   Local DB                     Auth DB
 (User copies)              (User source)

The package contains three sub-packages:

Package Used In Purpose
easyjwt_auth Auth-Service JWT token generation and validation
easyjwt_client Client-Service Remote authentication against auth-service
easyjwt_user Both (optional) Custom user model with email as username

Quick Start

Create an Auth-Service

uv venv
source .venv/bin/activate
uv pip install django djangorestframework django_easyjwt
uv run django-admin startproject config
mv config auth-service
cd auth-service

Add to config/settings.py:

from datetime import timedelta

INSTALLED_APPS = [
    # ...
    'rest_framework',
    'easyjwt_auth',
    'easyjwt_user',
]

AUTH_USER_MODEL = "easyjwt_user.User"

REST_FRAMEWORK = {
    "DEFAULT_PERMISSION_CLASSES": ("rest_framework.permissions.IsAuthenticated",),
    "DEFAULT_AUTHENTICATION_CLASSES": (
        "rest_framework.authentication.SessionAuthentication",
        "easyjwt_auth.authentication.JWTAuthentication",
    ),
}

EASY_JWT = {
    "ACCESS_TOKEN_LIFETIME": timedelta(minutes=5),
    "REFRESH_TOKEN_LIFETIME": timedelta(days=1),
    "ROTATE_REFRESH_TOKENS": False,
    "BLACKLIST_AFTER_ROTATION": False,
    "UPDATE_LAST_LOGIN": False,
    "ALGORITHM": "HS256",
    "SIGNING_KEY": "d577273ff885c3f84dadb8578bb40000",  # Set properly for production!
    "VERIFYING_KEY": None,
    "AUDIENCE": None,
    "ISSUER": None,
    "JWK_URL": None,
    "LEEWAY": 0,
    "USER_ID_FIELD": "id",
    "USER_ID_CLAIM": "user_id",
    "USER_AUTHENTICATION_RULE": "easyjwt_auth.authentication.default_user_authentication_rule",
    "AUTH_TOKEN_CLASSES": ("easyjwt_auth.tokens.AccessToken",),
    "TOKEN_TYPE_CLAIM": "token_type",
    "TOKEN_USER_CLASS": "easyjwt_auth.models.TokenUser",
    "JTI_CLAIM": "jti",
    "CHECK_REVOKE_TOKEN": False,
    "SLIDING_TOKEN_REFRESH_EXP_CLAIM": "refresh_exp",
    "SLIDING_TOKEN_LIFETIME": timedelta(minutes=5),
    "SLIDING_TOKEN_REFRESH_LIFETIME": timedelta(days=1),
}

Add to config/urls.py:

from django.urls import path, include

urlpatterns = [
    # ...
    path('auth/', include('easyjwt_auth.urls')),
    path('auth/', include('easyjwt_user.urls')),
]

Run migrations and create a superuser:

uv run python manage.py makemigrations
uv run python manage.py migrate
uv run python manage.py createsuperuser
uv run python manage.py runserver 0.0.0.0:8000

Create a Client-Service

cd ..
uv venv
source .venv/bin/activate
uv pip install django django_rest_framework django_easyjwt
uv run django-admin startproject config
mv config client-service
cd client-service

Add to config/settings.py:

INSTALLED_APPS = [
    # ...
    'rest_framework',
    'easyjwt_client',
    'easyjwt_user',
]

AUTH_USER_MODEL = "easyjwt_user.User"

REST_FRAMEWORK = {
    "DEFAULT_PERMISSION_CLASSES": ("rest_framework.permissions.IsAuthenticated",),
    "DEFAULT_AUTHENTICATION_CLASSES": (
        "easyjwt_client.authentication.EasyJWTAuthentication",
        "rest_framework.authentication.SessionAuthentication",
    ),
}

AUTHENTICATION_BACKENDS = [
    'django.contrib.auth.backends.ModelBackend',
    'easyjwt_client.authentication.RemoteAuthBackend',
]

EASY_JWT = {
    "AUTH_HEADER_TYPES": ("Bearer",),
    "AUTH_HEADER_NAME": "Authorization",
    "REMOTE_AUTH_SERVICE_URL": "http://127.0.0.1:8000",
    "REMOTE_AUTH_SERVICE_TOKEN_PATH": "/auth/token/",
    "REMOTE_AUTH_SERVICE_REFRESH_PATH": "/auth/token/refresh/",
    "REMOTE_AUTH_SERVICE_VERIFY_PATH": "/auth/token/verify/",
    "REMOTE_AUTH_SERVICE_USER_PATH": "/auth/user/",
    "REMOTE_AUTH_SERVICE_PASSWORD_CHANGE_PATH": "/auth/password-change/",
    "REMOTE_AUTH_REQUEST_TIMEOUT": 30,
    "REMOTE_AUTH_SSL_VERIFY": True,
    "USER_ID_FIELD": "id",
    "USER_ID_CLAIM": "user_id",
}

Add to config/urls.py:

from django.urls import path, include
from test_app.views import TestView

urlpatterns = [
    path('admin/', admin.site.urls),
    path('auth/', include("easyjwt_client.urls")),
    path('api/test/', TestView.as_view()),
]

Create test_app/views.py:

from rest_framework import generics
from rest_framework.response import Response

class TestView(generics.GenericAPIView):
    def get(self, request):
        return Response("success", status=200)

Run migrations:

uv run python manage.py makemigrations
uv run python manage.py migrate
uv run python manage.py runserver 0.0.0.0:8001

Standing up the Services

Run both services in separate terminals:

# Terminal 1 - Auth-Service
cd auth-service
source .venv/bin/activate
uv run python manage.py runserver 0.0.0.0:8000

# Terminal 2 - Client-Service
cd client-service
source .venv/bin/activate
uv run python manage.py runserver 0.0.0.0:8001

Configuration Reference

Auth-Service Settings

Setting Type Default Description
ACCESS_TOKEN_LIFETIME timedelta timedelta(minutes=5) Access token validity duration
REFRESH_TOKEN_LIFETIME timedelta timedelta(days=1) Refresh token validity duration
ROTATE_REFRESH_TOKENS bool False Issue a new refresh token on each refresh
BLACKLIST_AFTER_ROTATION bool False Blacklist old refresh token after rotation
UPDATE_LAST_LOGIN bool False Update user's last_login on token issuance
ALGORITHM str "HS256" JWT signing algorithm
SIGNING_KEY str settings.SECRET_KEY Secret key for signing tokens
VERIFYING_KEY str "" Public key for asymmetric algorithms
AUDIENCE str None Expected JWT audience claim
ISSUER str None Expected JWT issuer claim
LEEWAY int/float 0 Leeway in seconds for token expiration checks
AUTH_HEADER_TYPES tuple ("Bearer",) Valid Authorization header types
AUTH_HEADER_NAME str "HTTP_AUTHORIZATION" Header name for extracting JWT
USER_ID_FIELD str "id" User model field for ID
USER_ID_CLAIM str "user_id" JWT claim name for user ID
TOKEN_USER_CLASS str "easyjwt_auth.models.TokenUser" Stateless user class for token-only auth
CHECK_REVOKE_TOKEN bool False Invalidate tokens on password change
REVOKE_TOKEN_CLAIM str "hash_password" Claim name for password hash revocation check
SLIDING_TOKEN_LIFETIME timedelta timedelta(minutes=5) Sliding token validity
SLIDING_TOKEN_REFRESH_LIFETIME timedelta timedelta(days=1) Sliding token refresh window

Client-Service Settings

Setting Type Default Description
REMOTE_AUTH_SERVICE_URL str "http://127.0.0.1:8000" Base URL of auth-service
REMOTE_AUTH_SERVICE_TOKEN_PATH str "/auth/token/" Token endpoint path
REMOTE_AUTH_SERVICE_REFRESH_PATH str "/auth/token/refresh/" Refresh endpoint path
REMOTE_AUTH_SERVICE_VERIFY_PATH str "/auth/token/verify/" Verify endpoint path
REMOTE_AUTH_SERVICE_USER_PATH str "/auth/user/" User endpoint path
REMOTE_AUTH_SERVICE_PASSWORD_CHANGE_PATH str "/auth/password-change/" Password change endpoint path
REMOTE_AUTH_SERVICE_BLACKLIST_PATH str "/auth/token/blacklist/" Token blacklist endpoint path
REMOTE_AUTH_REQUEST_TIMEOUT int 30 HTTP request timeout in seconds
REMOTE_AUTH_SSL_VERIFY bool True Verify SSL certificates
AUTH_HEADER_TYPES tuple ("Bearer",) Valid auth header types
USER_MODEL_SERIALIZER str "easyjwt_user.serializers.TokenUserSerializer" Serializer for deserializing remote user data
REFRESH_TOKEN_IN_COOKIE bool False Opt-in: store refresh in HttpOnly cookie
AUTH_COOKIE_NAME str "refresh_token" Refresh cookie name
AUTH_COOKIE_HTTP_ONLY bool True Prevent JS access to refresh cookie
AUTH_COOKIE_SECURE bool False HTTPS-only (set True in prod)
AUTH_COOKIE_SAMESITE str "Lax" SameSite cookie attribute
AUTH_COOKIE_PATH str "/" Safe default; narrow to public token-route prefix for tighter scoping
AUTH_COOKIE_DOMAIN str None Cookie domain (None = current host)
AUTH_COOKIE_MAX_AGE int None Cookie max-age in seconds
ALLOWED_AUTH_ORIGINS list None Defense-in-depth origin allowlist
BLACKLIST_ON_LOGOUT bool True Best-effort blacklist on logout

API Endpoints

Auth-Service (easyjwt_auth.urls)

Endpoint Method Auth Description
/auth/token/ POST None Obtain access and refresh tokens
/auth/token/refresh/ POST None Refresh an expired access token
/auth/token/verify/ POST None Verify if a token is valid
/auth/token/blacklist/ POST None Blacklist a refresh token (revocation)
/auth/create-user/ POST None Register a new user account
/auth/password-change/ POST None Change password (verifies credentials)
/auth/login/ GET None Django LoginView for session auth

Client-Service (easyjwt_client.urls)

Endpoint Method Auth Description
/auth/token/ POST None Obtain tokens via remote auth-service
/auth/token/refresh/ POST None Refresh token via remote
/auth/token/verify/ POST None Verify token via remote
/auth/token/logout/ POST None Clear refresh cookie + blacklist (cookie mode)
/auth/csrf/ GET None Bootstrap CSRF cookie for SPAs (cookie mode)
/auth/password-change/ POST Session Change password (validates match)
/auth/password-change/done/ GET Session Password change confirmation page
/auth/password-reset/ GET None Redirects to auth-service reset page
/auth/login/ GET None Django LoginView for session auth

User (easyjwt_user.urls)

Endpoint Method Auth Description
/auth/user/ GET JWT Get current user's profile data

Testing the API

Obtain Token Pair

curl -X POST \
  -H "Content-Type: application/json" \
  -d '{"email": "user@test.com", "password": "user-pass"}' \
  http://127.0.0.1:8001/auth/token/

Response:

{
  "refresh": "...",
  "access": "..."
}

Make Authenticated Request

export ACCESS_TOKEN=<your_access_token>

curl -H "Authorization: Bearer ${ACCESS_TOKEN}" \
  http://127.0.0.1:8001/api/test/

Response: "success"

Refresh Token

curl -X POST \
  -H "Content-Type: application/json" \
  -d '{"refresh": "${REFRESH_TOKEN}"}' \
  http://127.0.0.1:8001/auth/token/refresh/

Verify Token

curl -X POST \
  -H "Content-Type: application/json" \
  -d '{"token": "${ACCESS_TOKEN}"}' \
  http://127.0.0.1:8001/auth/token/verify/

Create User (Auth-Service)

curl -X POST \
  -H "Content-Type: application/json" \
  -d '{"email": "newuser@test.com", "password": "S3cure!P@ssw0rd"}' \
  http://127.0.0.1:8000/auth/create-user/

Response:

{
  "id": 1,
  "email": "newuser@test.com",
  "first_name": "",
  "last_name": "",
  "phone": null
}

Change Password (Auth-Service)

curl -X POST \
  -H "Content-Type: application/json" \
  -d '{"email": "user@test.com", "password": "old-pass", "new_password": "N3wP@ss!2026"}' \
  http://127.0.0.1:8000/auth/password-change/

Get User Profile

curl -H "Authorization: Bearer ${ACCESS_TOKEN}" \
  http://127.0.0.1:8000/auth/user/

Response:

{
  "id": 1,
  "email": "user@test.com",
  "first_name": "Test",
  "last_name": "User"
}

Advanced Usage

Extra Data

You can pass additional user data from the auth-service to client-services using custom serializers.

Auth-Service Configuration

models.py:

class AccessGroup(models.Model):
    user = models.OneToOneField(User, related_name="accessgroup", on_delete=models.CASCADE)
    user_type = models.TextField()

serializers.py:

class AccessGroupSerializer(serializers.ModelSerializer):
    class Meta:
        model = AccessGroup
        fields = ("user_type",)

class TokenUserSerializer(serializers.ModelSerializer):
    accessgroup = AccessGroupSerializer()

    class Meta:
        model = User
        fields = ("id", "email", "first_name", "last_name", "accessgroup")

Add to EASY_JWT settings:

"USER_MODEL_SERIALIZER": "userdata.serializers.TokenUserSerializer",

Client-Service Configuration

Use the same model and serializer, but override create() and update() methods:

class TokenUserSerializer(serializers.ModelSerializer):
    accessgroup = AccessGroupSerializer()

    class Meta:
        model = User
        fields = ("id", "email", "first_name", "last_name", "accessgroup")

    def create(self, validated_data):
        accessgroup = validated_data.pop("accessgroup")
        user, _ = User.objects.get_or_create(
            email=validated_data.pop("email"),
            defaults=validated_data
        )
        AccessGroup.objects.update_or_create(user=user, defaults=accessgroup)
        return user

    def update(self, instance, validated_data):
        accessgroup = validated_data.pop("accessgroup")
        for attr, value in validated_data.items():
            setattr(instance, attr, value)
        instance.save()
        AccessGroup.objects.update_or_create(user=instance, defaults=accessgroup)
        return instance

Permissions

Use custom permission classes to control access based on user data:

from rest_framework import permissions

class AccessGroupPermission(permissions.BasePermission):
    message = "You do not have permission to this service"

    def has_permission(self, request, view):
        return (
            not request.user.is_anonymous
            and hasattr(request.user, 'accessgroup')
            and view.access_level.startswith(request.user.accessgroup.user_type)
        )

Refresh-Token Cookies & CSRF

By default, refresh tokens are returned in the JSON response body — suitable for mobile apps and server-to-server clients. For browser-based SPAs, enable cookie mode so the refresh token is stored in an HttpOnly cookie and never exposed to JavaScript:

EASY_JWT = {
    ...,
    "REFRESH_TOKEN_IN_COOKIE": True,
}

When enabled:

  • Login (token/) sets the refresh token as an HttpOnly cookie and removes it from the JSON body (XSS protection — JavaScript cannot read it).
  • Refresh (token/refresh/) reads the refresh token from the cookie (not the body) and rotates the cookie if ROTATE_REFRESH_TOKENS is enabled on the auth-service.
  • Logout (token/logout/) clears the cookie and best-effort blacklists the token server-side.
  • CSRF protection is automatically applied to all three endpoints.

Non-browser clients should leave REFRESH_TOKEN_IN_COOKIE = False and continue reading refresh tokens from the JSON body.

Setting Default Description
REFRESH_TOKEN_IN_COOKIE False Master switch (opt-in).
AUTH_COOKIE_NAME "refresh_token" Cookie key.
AUTH_COOKIE_HTTP_ONLY True JS cannot read the cookie.
AUTH_COOKIE_SECURE False Set True in production over HTTPS.
AUTH_COOKIE_SAMESITE "Lax" None, "Lax", or "Strict".
AUTH_COOKIE_PATH "/" Safe generic default — works regardless of mount prefix. Narrow to the public token-route prefix (e.g. "/auth/token/") for tighter scoping once you know your mount point.
AUTH_COOKIE_DOMAIN None None = current host.
AUTH_COOKIE_MAX_AGE None None = session cookie. Cap at the refresh-token lifetime for persistence.
ALLOWED_AUTH_ORIGINS None Optional exact-origin allowlist for defense-in-depth.
BLACKLIST_ON_LOGOUT True Best-effort server-side refresh-token revocation on logout.

CSRF Requirements

Cookie mode requires django.middleware.csrf.CsrfViewMiddleware in MIDDLEWARE (enforced by the E002 system check). The library ships a bootstrap endpoint:

  1. SPA calls GET <client>/csrf/ — Django emits the csrftoken cookie.
  2. SPA reads the csrftoken cookie from document.cookie.
  3. SPA sends it in the X-CSRFToken header on POST requests to token/, token/refresh/, and token/logout/.

W002 warning: CSRF_COOKIE_HTTPONLY=True is incompatible with this bootstrap pattern — JavaScript cannot read the cookie. If you need HttpOnly CSRF cookies, deliver the token via a rendered {% csrf_token %} tag in HTML instead.

Deployment Tiers

Tier Setup Support
A. Same-origin SPA + API GET /csrf/ → read csrftoken → send X-CSRFToken. Fully supported (default).
B. Same parent domain (app.example.com + api.example.com) Set CSRF_COOKIE_DOMAIN=".example.com", CSRF_TRUSTED_ORIGINS=["https://app.example.com"], and configure CORS for credentialed requests. Documented recipe.
C. Truly cross-site Use a same-site BFF or reverse proxy. SameSite=None;Secure cookie auth is fragile due to third-party cookie restrictions. Recommended.

ALLOWED_AUTH_ORIGINS performs defense-in-depth Origin validation; it does not by itself make cross-origin cookie authentication work — CORS and CSRF_TRUSTED_ORIGINS configuration is required for tier B.

Auth-Service Prerequisites

The logout blacklist call requires:

  1. easyjwt_auth.token_blacklist in INSTALLED_APPS on the auth-service.
  2. The token/blacklist/ route (wired by default in easyjwt_auth/urls.py).
  3. ROTATE_REFRESH_TOKENS=True on the auth-service for refresh-token rotation to rewrite the cookie. BLACKLIST_AFTER_ROTATION=True is recommended to revoke the previous refresh token but is not required for the cookie rewrite.

Logout Caveats

  • Blacklisting a refresh token does not invalidate an already-issued access JWT — keep access tokens short-lived.
  • The frontend must also discard its in-memory access token on logout.
  • The blacklist call is best-effort: if the auth-service is unreachable, the cookie is still cleared (logged out on the client), but a copied refresh token remains valid until its natural expiry.

Security

  • Password Validation: Django's AUTH_PASSWORD_VALIDATORS are enforced on both the create-user/ and password-change/ endpoints
  • Email Normalization: User creation uses UserManager.create_user() which normalizes and lowercases email addresses
  • Credential Verification: Password change requires the current password — old credentials are verified before accepting a new password
  • Error Sanitization: Remote auth error responses do not leak upstream server details (raw response bodies, tracebacks) to clients
  • SSL/HTTP Warnings: The client app logs a warning at startup when DEBUG=False and either SSL_VERIFY=False or REMOTE_AUTH_SERVICE_URL uses http://
  • Token Revocation: Optional CHECK_REVOKE_TOKEN invalidates all outstanding JWTs when a user changes their password
  • Token Blacklisting: Optional easyjwt_auth.token_blacklist app provides server-side token revocation with outstanding token tracking

Running Tests

uv pip install -e ".[dev]"
uv run pytest

Or with coverage:

uv run pytest --cov=easyjwt_auth --cov=easyjwt_client --cov=easyjwt_user

Changelog

See CHANGELOG.md for full version history.

v1.0.6+

  • Fixed TOKEN_USER_CLASS default pointing to wrong module path
  • CreateUserSerializer now uses create_user() for email normalization
  • Added REMOTE_AUTH_SERVICE_PASSWORD_CHANGE_PATH setting (was hardcoded)
  • TokenViewBase serializer classes resolved at runtime, not import time
  • Added catch-all requests.RequestException handler for unhandled HTTP errors
  • Merged CI publish workflow into version workflow (fixes [skip ci] blocking PyPI)

v1.0.5+

  • Security hardening — 21 review issues fixed across security, bugs, design, and test coverage
  • Removed dead jwt_secret field (migration 0002)
  • Unified settings access via api_settings across all modules
  • RemoteAuthBackend inherits BaseBackend (correct Django signature)
  • Added comprehensive test coverage (61% → 88%, 155 tests)
  • Password confirmation check, response sanitization, admin password validation
  • SSL/HTTP production warning in client apps.py

v1.0.0

  • Replaced MD5 with SHA-256 for password hashing
  • Renamed ModelBackend to RemoteAuthBackend
  • Added configurable HTTP request timeouts
  • Made SSL verification configurable
  • Consolidated HTTP error handling
  • Added __all__ exports to public modules

Acknowledgements

This package is heavily based on djangorestframework-simplejwt and influenced by SimpleJWT.

An example implementation is available at django-easyjwt-example.


License

MIT License - see LICENCE file.

Release files for django-easyjwt 1.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for django-easyjwt 1.1.0
File Size Uploaded
django_easyjwt-1.1.0.tar.gz 49.5 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for django-easyjwt 1.1.0
File Interpreter ABI Platform
django_easyjwt-1.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 103.5 kB

Release files / django_easyjwt-1.1.0.tar.gz

Download URL django_easyjwt-1.1.0.tar.gz
Size 49.5 kB
Tags Source
SHA-256 checksum
How to use checksums
306681f9c93f77e8fd1441b634d27c86e1a735cea30c65547942bfb8708c5e12
BLAKE2b-256 checksum
How to use checksums
8f3da7bc78e293eb6bdcf673d494e07a01627ae4adf3f1da75ede92aa4dc8879
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 11, 2026.

Transparency log

Release files / django_easyjwt-1.1.0-py3-none-any.whl

Download URL django_easyjwt-1.1.0-py3-none-any.whl
Size 53.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
bfb51579b8d9ee4ff9c3e790b1960f3227810d83f6ee082fc31c040cfedecef0
BLAKE2b-256 checksum
How to use checksums
26847c510851cda8165203a36871247381206ce8fbc93bc47ad8932495c535fe
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 11, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

1.1.0 This release

2 release files

1.0.13

2 release files

1.0.12

2 release files

1.0.11

2 release files

1.0.10

2 release files

1.0.9

2 release files

1.0.8

2 release files

1.0.7

2 release files

1.0.6

2 release files

1.0.5

2 release files

1.0.4

2 release files

1.0.3

2 release files

1.0.2

2 release files

1.0.1

2 release files

0.7.9

2 release files

0.7.8

2 release files

0.7.7

2 release files

0.7.6

2 release files

0.7.5

2 release files

0.7.4

2 release files

0.7.3

2 release files

0.7.2

2 release files

0.7.1

2 release files

0.7.0

2 release files

0.6.2

2 release files

0.6.1

2 release files

0.6.0

2 release files

0.5.2

2 release files

0.5.1

2 release files

0.5.0

2 release files

0.4.7

2 release files

0.4.6

2 release files

0.4.5

2 release files

0.4.4

2 release files

0.4.3

2 release files

0.4.2

2 release files

0.4.1

2 release files

0.4.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page