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_modelsis wired to the built-inrender_form_fieldhandler, which renders each field usingfeincms3_formbuilder/form_field.html. Pass any number of plugin models here — they all share that same wrapper template. - Every model in
extra_pluginsis registered with the renderer callable you provide. Use this for plugins that are not form fields (e.g. aRichTextblock) or for field plugins that need different outer markup thanform_field.html— in that case write a custom renderer that callsform.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-sectionfieldsets(main data + related object) covering every field onAbstractFormSubmissionplus the consumer's requiredconfigured_formFK.formatted_data_display— callsobj.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()returningFalse(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:
settings.FORMBUILDER_FROM_EMAILif set and non-emptysettings.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_fieldis the name of the controlling field.show_when_valuestakes one choice key of the controlling field per line. The key is the value the browser submits:- for a
choicesline without|, the slugified label (Phone→phone) - for a
key | Labelline, the key
- for a
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 thedata-show-when-valuesattribute.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
hiddenon the wrapper. - Toggle
disabledon the inputs inside it, so the browser does not send values for fields the user cannot see. - Set
requiredon an input exactly when the field is active and the input carriesdata-required-if-active. A hidden input that is stillrequiredblocks 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)
| File | Size | Uploaded | |
|---|---|---|---|
| feincms3_formbuilder-0.5.0.tar.gz | 41.4 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| 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 logRelease 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