Skip to main content

feincms3-formbuilder

feincms3-formbuilder provides the abstract models, views, processing helpers, renderer factory, admin utilities, and templates needed to build a form-builder app on top of feincms3-forms. Its relationship to feincms3-forms mirrors the relationship of feincms3 to django-content-editor: the lower-level library defines the protocol; feincms3-formbuilder wires everything together so that projects only need to write the thin, project-specific layer.

For how the package is organized internally, see docs/ARCHITECTURE.md.

Contents


Core setup

Installation

pip install feincms3-formbuilder

Add the app to INSTALLED_APPS:

INSTALLED_APPS = [
    ...
    "content_editor",
    "feincms3_forms",
    "feincms3_formbuilder",
    ...
]

Models

Create four concrete models in your app.

ConfiguredForm

Subclass AbstractConfiguredForm, add any project fields (e.g. a slug), and override FORMS to point validate and process at your own functions:

# myapp/models.py
from content_editor.models import Region, create_plugin_base
from django.db import models
from feincms3_forms import models as forms_models
from feincms3_formbuilder.models import (
    AbstractConfiguredForm,
    AbstractFormStep,
    AbstractFormSubmission,
)


class ConfiguredForm(AbstractConfiguredForm):
    slug = models.SlugField(unique=True, blank=True)

    FORMS = [
        forms_models.FormType(
            key="simple",
            label="simple form",
            regions=[
                Region(key="form", title="Form fields"),
                Region(key="success", title="Success message"),
            ],
            form_class="django.forms.Form",
            validate="myapp.validation.validate_configured_form",
            process="myapp.processing.process_simple_form",
        ),
        forms_models.FormType(
            key="multistep",
            label="multi-step form",
            regions=lambda configured_form: (
                (
                    [
                        Region(key=step.region_key, title=step.title)
                        for step in configured_form.steps.all()
                    ]
                    if configured_form.pk
                    else []
                )
                + [Region(key="success", title="Success message")]
            ),
            form_class="django.forms.Form",
            validate="myapp.validation.validate_configured_form",
            process="myapp.processing.process_multistep_form",
        ),
    ]

FormStep

Subclass AbstractFormStep and add a FK to ConfiguredForm. The AbstractFormStep provides title, an auto-generated identifier (used as the region key), and ordering:

class FormStep(AbstractFormStep):
    configured_form = models.ForeignKey(
        ConfiguredForm,
        on_delete=models.CASCADE,
        related_name="steps",
    )

    class Meta(AbstractFormStep.Meta):
        unique_together = [
            ("configured_form", "ordering"),
            ("configured_form", "identifier"),
        ]

FormSubmission

Subclass AbstractFormSubmission, add a FK to ConfiguredForm, and override get_formatted_data to pass your field model:

class FormSubmission(AbstractFormSubmission):
    configured_form = models.ForeignKey(
        ConfiguredForm,
        on_delete=models.CASCADE,
        related_name="submissions",
    )

    def get_formatted_data(self):
        return super().get_formatted_data(field_model=SimpleField)

AbstractFormSubmission stores submitted_at, data (JSON), ip_address, user_agent, and optional related_content_type / related_object_id generic FK fields (used for the submission-ref feature described below).

AbstractConfiguredForm and AbstractFormStep both ship with created_at (auto_now_add=True) and updated_at (auto_now=True). The default ordering on AbstractConfiguredForm is ["-created_at"].

SimpleField and proxy models

Create the plugin base, a SimpleField model, and proxy models for each field type you want to support:

ConfiguredFormPlugin = create_plugin_base(ConfiguredForm)


class SimpleField(forms_models.SimpleFieldBase, ConfiguredFormPlugin):
    class Meta:
        verbose_name = "form field"
        verbose_name_plural = "form fields"


Text = SimpleField.proxy(SimpleField.Type.TEXT)
Email = SimpleField.proxy(SimpleField.Type.EMAIL)
URL = SimpleField.proxy(SimpleField.Type.URL)
Date = SimpleField.proxy(SimpleField.Type.DATE)
Integer = SimpleField.proxy(SimpleField.Type.INTEGER)
Textarea = SimpleField.proxy(SimpleField.Type.TEXTAREA)
Checkbox = SimpleField.proxy(SimpleField.Type.CHECKBOX)
Select = SimpleField.proxy(SimpleField.Type.SELECT)
Radio = SimpleField.proxy(SimpleField.Type.RADIO)
SelectMultiple = SimpleField.proxy(SimpleField.Type.SELECT_MULTIPLE)
CheckboxSelectMultiple = SimpleField.proxy(SimpleField.Type.CHECKBOX_SELECT_MULTIPLE)

You can add further non-field plugins (e.g. a RichText) the same way any django-content-editor plugin is added.


Renderer

Call create_form_renderer() with your field-producing plugin models as positional arguments and any non-field plugins via extra_plugins:

# myapp/renderer.py
from feincms3.renderer import template_renderer
from feincms3_formbuilder.renderer import create_form_renderer
from myapp.models import NewsletterField, RichText, SimpleField

renderer = create_form_renderer(
    SimpleField,
    NewsletterField,
    extra_plugins={
        RichText: template_renderer("myapp/richtext.html"),
    },
)

create_form_renderer(*field_models, extra_plugins=None) returns a RegionRenderer where:

  • Every model in field_models is wired to the built-in render_form_field handler, which renders each field using feincms3_formbuilder/form_field.html. Pass any number of plugin models here — they all share that same wrapper template.
  • Every model in extra_plugins is registered with the renderer callable you provide. Use this for plugins that are not form fields (e.g. a RichText block) or for field plugins that need different outer markup than form_field.html — in that case write a custom renderer that calls form.get_form_fields(plugin) itself.

If you want every field to render through your own template, override feincms3_formbuilder/form_field.html in your project's templates directory rather than registering each model individually.


Validation

Implement a validate function that returns a list of error strings. Use the validate_with_renderer helper so that field-name uniqueness is checked across all plugins registered with your renderer:

# myapp/validation.py
from feincms3_formbuilder.models import validate_with_renderer
from myapp.renderer import renderer


def validate_configured_form(configured_form):
    return validate_with_renderer(configured_form, renderer)

Processing

A process function receives the request and validated data and must return an HttpResponse. Use the create_submission and render_success_region helpers to keep the implementation minimal.

Simple form — receives a bound, valid form:

# myapp/processing.py
from feincms3_formbuilder.processing import create_submission, render_success_region
from myapp.models import FormSubmission
from myapp.renderer import renderer


def process_simple_form(request, form, *, configured_form):
    data = dict(form.cleaned_data)
    create_submission(request, configured_form, data, submission_model=FormSubmission)
    return render_success_region(request, configured_form, renderer=renderer)

Multi-step form — receives accumulated_data collected across all steps:

def process_multistep_form(request, configured_form, accumulated_data):
    data = dict(accumulated_data)
    create_submission(request, configured_form, data, submission_model=FormSubmission)
    return render_success_region(request, configured_form, renderer=renderer)

create_submission automatically extracts the _ref token (see Templatetags) from data, verifies it, and stores the resolved generic FK on the submission.

FORMBUILDER_CLIENT_IP_RESOLVER setting

create_submission stores the client IP on each submission. By default it uses REMOTE_ADDR (the TCP peer address), which cannot be spoofed by clients. Deployments behind a proxy — where REMOTE_ADDR is the proxy — should set FORMBUILDER_CLIENT_IP_RESOLVER to a dotted path to a callable (request) -> str | None that consults the appropriate forwarded header:

# settings.py
FORMBUILDER_CLIENT_IP_RESOLVER = "myproject.utils.client_ip"

# myproject/utils.py
def client_ip(request):
    xff = request.META.get("HTTP_X_FORWARDED_FOR", "")
    return xff.split(",")[0].strip() or request.META.get("REMOTE_ADDR")

No forwarded header is honored by default because trusting one without a proxy in front would let clients spoof their own IP. The resolver lives in your project so the trust model (which header, how many hops) is explicit.


Admin

Use ConfiguredFormAdmin together with the simple_field_inlines() helper and FormStepInline:

# myapp/admin.py
from django.contrib import admin
from feincms3_formbuilder.admin import FormStepInline, simple_field_inlines
from myapp.models import ConfiguredForm, FormStep, SimpleField


@admin.register(ConfiguredForm)
class ConfiguredFormAdmin(admin.ModelAdmin):
    inlines = [
        FormStepInline.for_model(FormStep),
        *simple_field_inlines(SimpleField),
    ]

simple_field_inlines(model) returns one SimpleFieldInline per field type, each pre-configured with a Material Icons button and a deny_regions({"success"}) constraint so that field plugins cannot be placed in the success region.

FormStepInline is an OrderableAdmin TabularInline. Bind it to your concrete FormStep model with FormStepInline.for_model(FormStep), or by subclassing and setting model explicitly.

For the submission admin, subclass BaseFormSubmissionAdmin:

from feincms3_formbuilder.admin import BaseFormSubmissionAdmin
from myapp.models import FormSubmission


@admin.register(FormSubmission)
class FormSubmissionAdmin(BaseFormSubmissionAdmin):
    pass  # add project-specific actions, list_filter etc. here

BaseFormSubmissionAdmin ships:

  • list_display, list_filter, date_hierarchy, and a two-section fieldsets (main data + related object) covering every field on AbstractFormSubmission plus the consumer's required configured_form FK.
  • formatted_data_display — calls obj.get_formatted_data().
  • related_object_link — resolves the generic FK (related_content_type / related_object_id) to an admin change-page link, or - when unset.
  • has_add_permission() returning False (submissions are user-generated).

Views and URLs

The package ships ready-made views for two common cases: simple_form_view for single-page forms and multistep_form_view for forms split into steps. They return HTML fragments (the <form> element, and the bare success region after a valid submission), so the page showing the form has to load them, for example with htmx. If they don't fit your needs, write your own views from the same parts; docs/ARCHITECTURE.md describes them.

To use the ready-made views, write a thin wrapper that looks up the ConfiguredForm and dispatches to one of them:

# myapp/views.py
from django.shortcuts import get_object_or_404
from feincms3_formbuilder.views import multistep_form_view, simple_form_view
from myapp.models import ConfiguredForm
from myapp.renderer import renderer


def form_view(request, slug):
    configured_form = get_object_or_404(ConfiguredForm, slug=slug)
    if configured_form.form_type == "multistep":
        return multistep_form_view(request, configured_form, renderer=renderer)
    return simple_form_view(request, configured_form, renderer=renderer)
# myapp/urls.py
from django.urls import path
from myapp import views

app_name = "forms"

urlpatterns = [
    path("<slug:slug>/", views.form_view, name="form"),
]

The "multistep" string above must match the key= you set on the corresponding FormType in FORMS.

multistep_form_view walks all regions whose key starts with STEP_REGION_PREFIX ("step_") — this matches AbstractFormStep.region_key. Pass get_step_regions= (a callable (configured_form) -> list[Region]) to override the selection, e.g. to mix step regions with project-specific content regions.


Templates

The package ships three minimal templates under feincms3_formbuilder/:

Template Used by
form.html simple_form_view — wraps the form in a <form> tag with a Submit button
multistep_form.html multistep_form_view — adds step navigation, Back / Next / Submit buttons
form_field.html render_form_field — renders label, widget, help text, and errors for each field

Override any of them by creating a file at the same path inside your project's template directories. For example, to style the step navigation, copy feincms3_formbuilder/multistep_form.html into your app's templates/feincms3_formbuilder/ directory and modify it as needed.


Templatetags

Load feincms3_formbuilder_tags to access the make_submission_ref filter. It signs a content-type / object-id pair so that a form submission can be linked back to a related object (e.g. an event registration linked to an event):

{% load feincms3_formbuilder_tags %}

<form method="post">
  {% csrf_token %}
  <input type="hidden" name="_ref" value="{{ event|make_submission_ref }}">
  ...
</form>

When create_submission processes the form data it pops _ref, verifies the signature, and stores the resolved generic FK on the submission. You can then query submissions for a specific object:

FormSubmission.objects.for_related_object(event)

The view layer also reads ?ref= from the GET query string and pre-fills it into the form's initial data under the key _ref. This lets you link to a form with ?ref={{ obj|make_submission_ref }} and have the token survive through the form submission, provided your form class declares a hidden _ref field:

from django import forms

class BaseForm(forms.Form):
    _ref = forms.CharField(required=False, widget=forms.HiddenInput)

If your form class has no _ref field the initial value is silently ignored.


Optional features

Notifications

feincms3-formbuilder ships an optional notification module that lets a project send confirmation/staff emails after a form submission. The package provides the abstract model, validator, and helper; the project owns the concrete model, admin integration, and editor widget.

Concrete FormNotification model

from feincms3_formbuilder.notifications import AbstractFormNotification


class FormNotification(AbstractFormNotification):
    configured_form = models.ForeignKey(
        ConfiguredForm,
        on_delete=models.CASCADE,
        related_name="notifications",
    )

AbstractFormNotification provides three fields:

Field Purpose
recipients Comma-separated emails and {{ form_data.<field_name> }} variables (e.g. staff@example.com, {{ form_data.email }}); no filters, tags or other variables
subject Plain-text subject; supports template variables
body HTML body; supports template variables; rendered with autoescape on

The recipients field is validated at save time (via validators=[validate_recipients] on the field):

  • Empty values are rejected.
  • Each comma-separated recipient must be an email address or exactly {{ form_data.<field_name> }}. Filters, tags and other variables are rejected.

To check that each {{ form_data.<name> }} in recipients refers to an email field of the form, call validate_notification_recipients from your form type's validate function (see Validation). The admin shows an error after saving:

# myapp/validation.py
from feincms3_formbuilder.models import validate_with_renderer
from feincms3_formbuilder.notifications import validate_notification_recipients
from myapp.renderer import renderer


def validate_configured_form(configured_form):
    return [
        *validate_with_renderer(configured_form, renderer),
        *validate_notification_recipients(
            configured_form, renderer, configured_form.notifications.all(),
        ),
    ]

Only SimpleFieldBase.Type.EMAIL counts as an email field unless you pass email_field_types. The list replaces the default, so include SimpleFieldBase.Type.EMAIL alongside your custom types. A custom field plugin's type is its lowercased class name.

Sending notifications from process()

# myapp/processing.py
from feincms3_formbuilder.processing import create_submission, render_success_region
from feincms3_formbuilder.notifications import send_form_notifications


def process_simple_form(request, form, *, configured_form):
    data = dict(form.cleaned_data)
    submission = create_submission(
        request, configured_form, data, submission_model=FormSubmission,
    )
    send_form_notifications(
        configured_form.notifications.all(),
        context={"form_data": data, "submission": submission},
    )
    return render_success_region(request, configured_form, renderer=renderer)

context is a plain dict; whatever keys you place there are available to the editor as Django template variables in subject and body. The form_data key is the documented standard (used by the notification body's help text and the only one recipients supports); other keys are project-specific.

Variables for editors

Documented out of the box:

  • {{ form_data.<field_name> }} — any cleaned value from the form

Anything else (a submission link, a related-object link, a project- specific identifier) is whatever the project decides to put in context.

Failure handling

Recipients that don't resolve to a valid email address are skipped and logged at ERROR; the notification is still sent to the remaining recipients.

send_form_notifications defaults to fail_silently=True: per-notification failures (template syntax errors, no valid rendered recipient, SMTP errors) are logged via the feincms3_formbuilder.notifications logger at ERROR and the remaining notifications continue to send. Pass fail_silently=False to re-raise instead — useful in tests.

FORMBUILDER_FROM_EMAIL setting

The From address used for every notification is, in order:

  1. settings.FORMBUILDER_FROM_EMAIL if set and non-empty
  2. settings.DEFAULT_FROM_EMAIL

Admin integration

The package ships no admin classes for notifications. Wire your inline in your project admin:

class FormNotificationInline(admin.TabularInline):
    model = FormNotification
    extra = 0


@admin.register(ConfiguredForm)
class ConfiguredFormAdmin(admin.ModelAdmin):
    inlines = [
        FormStepInline.for_model(FormStep),
        FormNotificationInline,
        *simple_field_inlines(SimpleField),
    ]

For a rich-text editor on body, use formfield_overrides or a custom ModelForm:

from django_prose_editor.fields import ProseEditorFormField

class FormNotificationInlineForm(forms.ModelForm):
    body = ProseEditorFormField()
    class Meta:
        model = FormNotification
        fields = "__all__"

class FormNotificationInline(admin.TabularInline):
    model = FormNotification
    form = FormNotificationInlineForm

Extending with extra fields

Projects that want from_email / reply_to / bcc / cc add fields to their concrete subclass and pass a custom send_one to the helper:

from feincms3_formbuilder.notifications import send_form_notifications

def my_send_one(notification, context):
    # Build EmailMultiAlternatives including notification.reply_to etc.
    ...

send_form_notifications(
    configured_form.notifications.all(),
    context={"form_data": data, "submission": submission},
    send_one=my_send_one,
)

Conditional fields

A field can be made conditional on a dropdown or radio field, the controlling field: it is shown, and required if configured as required, only when the controlling field is answered with one of the configured values. Otherwise its value is not stored.

Setup

Add ConditionalFieldMixin to your concrete SimpleField and generate the migration:

from feincms3_formbuilder.models import ConditionalFieldMixin


class SimpleField(ConditionalFieldMixin, forms_models.SimpleFieldBase, ConfiguredFormPlugin):
    class Meta:
        verbose_name = "form field"
        verbose_name_plural = "form fields"
python manage.py makemigrations myapp

simple_field_inlines() adds show_when_field and show_when_values to the "Advanced" fieldset of every inline on its own. If you build your inlines another way, append both names to advanced_fields yourself.

Include the script on every page showing the form:

{% load static %}
<script src="{% static 'feincms3_formbuilder/conditionals.js' %}" defer></script>

The ready-made views support conditional fields without further setup. A view of your own must build its forms with create_form_with_conditionals from feincms3_formbuilder.conditionals instead of feincms3-forms' create_form; otherwise conditional fields are always shown and required. docs/ARCHITECTURE.md describes what a custom multi-step view needs in addition.

Configuring a condition

Editors fill in two fields in the "Advanced" fieldset of the conditional field:

  • show_when_field is the name of the controlling field.
  • show_when_values takes one choice key of the controlling field per line. The key is the value the browser submits:
    • for a choices line without |, the slugified label (Phone → phone)
    • for a key | Label line, the key

Limits:

  • Only dropdown and radio fields can control other fields. A condition naming any other field type in the same form never matches, and the conditional field is never shown.
  • A controlling field must not itself be conditional.
  • A field has at most one condition.
  • In a multi-step form, the controlling field must be on the same step or an earlier one. A field controlled from a later step is never shown; the save-time configuration check reports it.
  • A step on which every field is inactive still appears in the progress indicator, with only a Next button. Put conditional fields on a step that has other fields too.

Without JavaScript

The conditional field stays hidden until the user submits the form. The server then sees the answer to the controlling field. For a required conditional field, it returns the form with the field visible and a "required" error on it, so the missing answer is asked for rather than silently dropped. An optional conditional field is accepted empty, as if the user had left it blank.

Custom templates and renderers

render_form_field passes the condition to form_field.html as the condition template variable. It is empty for an unconditional field and otherwise holds:

  • field: the name of the controlling field.
  • values: the satisfying values, already encoded as a JSON string for the data-show-when-values attribute.
  • active: whether the server considers the condition met.

If your project overrides feincms3_formbuilder/form_field.html, it must emit both data attributes and hidden from condition as the shipped template does (see the markup below). Otherwise a conditional field whose condition is not met is shown with disabled inputs.

A custom renderer gets the same condition from condition_context(form, plugin), importable from feincms3_formbuilder.conditionals.

Custom scripts

The server renders each condition as data attributes on the field's wrapper element, with hidden set while the condition is not met:

<div data-show-when-field="contact_pref" data-show-when-values='["phone","sms"]' hidden>
  …field…
</div>

The inputs of a conditional field carry data-required-if-active where Django would render required: it marks the inputs that are required while the field is active. While the condition is not met, the inputs also carry disabled and no required.

To replace conditionals.js, your script must do the following whenever the controlling field changes:

  • Toggle hidden on the wrapper.
  • Toggle disabled on the inputs inside it, so the browser does not send values for fields the user cannot see.
  • Set required on an input exactly when the field is active and the input carries data-required-if-active. A hidden input that is still required blocks the browser's submit.

Save-time configuration check

Projects whose editors configure conditions need this check. At runtime a misconfigured condition, such as a mistyped value, only means that the field is silently never shown, with nothing to tell the editor why. Add validate_conditionals to the list your form type's validate function returns (see Validation) to report these problems:

# myapp/validation.py
from feincms3_formbuilder.conditionals import validate_conditionals
from feincms3_formbuilder.models import validate_with_renderer
from feincms3_formbuilder.notifications import validate_notification_recipients
from myapp.renderer import renderer


def validate_configured_form(configured_form):
    return [
        *validate_with_renderer(configured_form, renderer),
        *validate_notification_recipients(
            configured_form, renderer, configured_form.notifications.all(),
        ),
        *validate_conditionals(configured_form, renderer),
    ]

It reports a condition that:

  • names a controlling field that does not exist,
  • names the field itself,
  • names a controlling field that is not a dropdown or radio field,
  • names a controlling field that is itself conditional,
  • names a controlling field on a later step,
  • has a controlling field but no values,
  • lists a value the controlling field does not offer. The message lists the values the controlling field does offer.

The admin shows these as errors after saving. Saving is not blocked, so the editor has to fix the condition and save again.

Notifications

A recipient written as {{ form_data.<name> }} that names a field which is not active is skipped, exactly like an optional email field left empty.


Submission export (XLSX)

An optional Excel export of form submissions, exposed as an admin action. Install the extra, which pulls in xlsxdocument (and openpyxl):

pip install feincms3-formbuilder[xlsx]

make_export_action(renderer) builds an admin action; add it to your submission admin's actions:

from feincms3_formbuilder.admin import BaseFormSubmissionAdmin, make_export_action
from myapp.models import FormSubmission
from myapp.renderer import renderer


@admin.register(FormSubmission)
class FormSubmissionAdmin(BaseFormSubmissionAdmin):
    actions = [make_export_action(renderer)]

The produced workbook:

  • One sheet per configured form, named after the form.
  • Columns: ID, submitted at, IP address, user agent, then one column per form field.

To build the document outside the admin (e.g. from a management command), call the helper directly — it returns an xlsxdocument.XLSXDocument:

from feincms3_formbuilder.reporting import build_submissions_xlsx

xlsx = build_submissions_xlsx(FormSubmission.objects.all(), renderer=renderer)
response = xlsx.to_response("form-submissions.xlsx")

build_submissions_xlsx raises ImproperlyConfigured if the xlsx extra is not installed.

Metadata

Release files for feincms3-formbuilder 0.5.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 feincms3-formbuilder 0.5.0
File Size Uploaded
feincms3_formbuilder-0.5.0.tar.gz 41.4 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for feincms3-formbuilder 0.5.0
File Interpreter ABI Platform
feincms3_formbuilder-0.5.0-py3-none-any.whl Python 3 none any Details

Total release size: 86.2 kB

Release files / feincms3_formbuilder-0.5.0.tar.gz

Download URL feincms3_formbuilder-0.5.0.tar.gz
Size 41.4 kB
Tags Source
SHA-256 checksum
How to use checksums
e8d209e5d799bfff83a34453d4c0c0574327e1691d99985f4cf0d115e98c7b60
BLAKE2b-256 checksum
How to use checksums
ca7669b819c04bc2160624c125625b5627213798cae988fc3c6f64780a8cde7e
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 Oct 7, 2026.

Transparency log

Release files / feincms3_formbuilder-0.5.0-py3-none-any.whl

Download URL feincms3_formbuilder-0.5.0-py3-none-any.whl
Size 44.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
7626958e3b3204e2244a64e3a0206f255410878b5c5bf55cdd6a3b2a638e6d97
BLAKE2b-256 checksum
How to use checksums
269c28bdb772fd5f0775c8045fc6bd174ada24f47928c7cedd85f0e07bf2c16d
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 Oct 7, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.5.0 This release

2 release files

0.4.0

2 release files

0.3.5

2 release files

0.3.4

2 release files

0.3.3

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