ckanext-actor-registry
Reusable publishers and contact points for CKAN metadata.
Status: 0.3.1 alpha. This version is intended for evaluation and feedback. It has an automated test suite that runs against CKAN 2.11 and CKAN 2.12. The user interface is in English by default, with a Swedish translation catalog following the installation's locale settings. Do not treat it as production-ready without your own review and tests.
Documentation
- Installation and supported versions: which CKAN and ckanext-scheming versions are tested, configuration, the database migration and the pitfalls to avoid;
- Run it with Docker Compose: a complete example (CKAN 2.11 or 2.12, PostgreSQL, Solr, Redis) with a smoke test, for trying the extension or as a starting point;
- Changelog and release notes;
- Testing: how to run the automated suite on each supported CKAN version;
- RDF and interoperability: the optional DCAT-AP profile;
- Translations: how the English and Swedish catalogues are maintained;
- Common setups: how organizations, publishers and contact points relate in different catalogues, and what we recommend for each;
- Choosing your fields: organization, publisher and contact points, and what happens to CKAN's own fields;
- Contributing, security policy and AI assistance disclosure.
Why this extension exists
CKAN datasets often repeat publisher and contact details. Repetition is easy to introduce but expensive to maintain: a changed email address or organization name must be corrected on every affected dataset.
This extension stores publishers and contact points once and lets datasets reference them by stable internal IDs. Registry changes are resolved when a dataset is displayed or exported, so one central update can be reflected wherever the record is used. This isn't just a convenience: DCAT-AP itself expects a dataset's publisher and contact point to be addressable entities in their own right, not text -- see "Data model and DCAT-AP mapping" below for how the registry implements that.
A publisher is an organization or a person that publishes datasets. A contact point is a reusable contact function; it can belong to an organization (a publisher) but doesn't have to. Neither concept replaces a CKAN organization: CKAN organizations continue to control dataset ownership and permissions.
Terminology. In the interface these records are called publishers. The word actor survives only in technical places: the URLs (
/actors), the dataset fieldpublisher_actor_id, the Python package and the action names (actor_registry_*).
Publishers vs. CKAN organizations
CKAN already has an "Organization" concept, so it's fair to ask why this extension adds new records instead of extending it.
A CKAN organization is primarily an access-control and ownership unit: every dataset belongs to exactly one organization, and membership in it determines who may create, edit or delete the dataset. That's a different question from who publishes a dataset or who to contact about it. The two often coincide in practice, but not always: a dataset can be maintained by editors who aren't themselves the responsible publisher, a single dataset commonly needs more than one contact point (technical, legal, general enquiries) where an organization has only the one dataset-to-organization relationship, a publisher can be a person rather than an organization at all, and the same publisher can publish datasets that live under different owner organizations on the same catalog -- none of which map cleanly onto extending the organization model.
Extending CKAN's organization type to carry this metadata would also mean changing a core concept that already governs permissions, for a purely descriptive need. Publishers and contact points are deliberately independent instead: additive tables with no CKAN user account, group membership or permission of their own, and no effect on how organizations already control dataset ownership.
Choosing your fields: organization, publisher and contact points
You do not have to use both registries. Which fields a dataset has is decided by the ckanext-scheming schema you configure. The extension does not add or remove a field in the schema; the optional snippets for CKAN's own contact fields (see "Migrate to Actor Registry") can hide Maintainer and Author on forms and pages, and only if your schema uses them.
Why choose deliberately. CKAN core stores contact details as free text on each dataset (Author, Author email, Maintainer, Maintainer email). When the same contact appears on many datasets, every copy has to be kept up to date separately, and copies drift apart. The organization, on the other hand, is mainly about ownership and who may edit, and is often also the right publisher, but not always (see above). So the two needs are different:
| Your catalogue | Use | Example schema |
|---|---|---|
| The owner organization is the publisher; contact details are repeated across datasets | Organization + contact points, no publisher field | contact_points_only_schema.yaml |
| The publisher can differ from the owner organization (another organization, a person) | Organization + publisher + contact points | actor_registry_schema.yaml |
Catalogues differ more than one expects: the organization can be a department while the city is the publisher, or the other way round. Common setups describes the setups we know of and how to handle each one.
Both ship in ckanext/actor_registry/examples/ and are exercised by the test
suite. Configure one of them (they both define the dataset type), or copy
one into your own schema.
How CKAN's own fields map:
| CKAN core | With the registry |
|---|---|
Organization (owner_org) |
unchanged: ownership and permissions |
| Author, Author email, Maintainer, Maintainer email | replaced by Contact points (contact_point_ids) if your schema uses that field instead |
| (no equivalent) | Publisher (publisher_actor_id), optional |
What happens to CKAN's own fields. Nothing, by the extension. Without Scheming the registry still works (its pages, lists and merging), but no dataset fields appear and CKAN's form is unchanged. With Scheming, the form and dataset page show exactly the fields in your schema, as with any Scheming schema: the example schemas leave out Author, Maintainer, licence, tags and version to stay short, so those do not appear. Add them back to your schema if you need them, for example during a transition. Values already stored on existing datasets stay in the database and in the API; they are only not shown when the schema lacks the field, and they are not copied into the registry automatically (see "Activating on an existing installation with datasets" below).
In the optional DCAT export (ckanext-dcat with this extension's profile):
a dataset without a publisher record is exported with its organization as
dct:publisher (ckanext-dcat's own fallback), and with its registry contact
points as dcat:contactPoint; without those, ckanext-dcat falls back to
Maintainer and then Author. A dataset with registry contact points
never exports Maintainer or Author as well, and the setting
ckanext.actor_registry.legacy_contact_fields = false stops exporting them
altogether (see Installation).
The stored values are never changed.
What visitors see is what is exported
A ground rule for this extension: the dataset page is designed first, and the DCAT export follows what the page shows, never the other way round. The page and the export use the same resolution of publishers and contact points:
- an inactive publisher or contact point is still what the dataset says until someone replaces it, so it is shown and exported (the forms no longer offer it; see Cleanup);
- a merged publisher is replaced by the one it was merged into, on both;
- a reference to a record that no longer exists is on neither;
- CKAN's own Maintainer and Author are exported only as far as the page
shows them: never when the dataset has registry contact points or the
migration is closed, and otherwise only the fields your schema shows
(without Scheming, CKAN's own page shows all of them). A shown Author is
exported as
dct:creator, which DCAT-AP-SE calls producent.
Deliberate exceptions, decided and documented:
- No publisher: the page shows no publisher (the organization is already
shown on the page), while ckanext-dcat exports the organization as
dct:publisher.
Features
- central registries for publishers and contact points;
- stable URIs, identifiers (unique per identifier scheme) and optional controlled publisher types;
- active/inactive records without breaking existing references; merging of duplicates and, for sysadmins, permanent deletion;
- alphabetical selectors with previews;
- inline creation and editing while editing a dataset, available to any logged-in editor;
- the current user's last publisher preselected on a new dataset;
- the current user's three most recently used contact points shown first;
- readable display on the dataset page (name and contact details, never internal IDs), and a page per publisher and contact point that lists its datasets;
- a worklist of datasets that are missing a publisher and/or a contact point;
- one shared validation layer (lengths, email, web addresses, URIs, phone numbers) and opt-in validators that keep datasets from pointing at records that no longer exist;
- Scheming helpers, form and display snippets, and a localised example schema;
- an English interface with Swedish (
sv,sv_SE) translations; - optional DCAT integration: publishers become
foaf:Agentvalues and contact points becomevcard:Kindvalues; - optional
vcard:hasTelephoneserialization for the European DCAT-AP 3 profile (avcard:Voicewith atel:URI, the usual vCard form); - validation that prevents a publisher and a contact point from sharing the same RDF URI.
Data model and DCAT-AP mapping
None of this matters if you don't use DCAT: the RDF profile is entirely
opt-in (enabled only by setting ckanext.dcat.rdf.profiles = actor_registry_euro_dcat_ap_3 after installing ckanext-dcat). Without it,
publishers and contact points work exactly like a plain metadata-management
feature -- centrally maintained records, picked from a dropdown, editable in
place -- and you never need to think about foaf:Agent or vcard:Kind. The
rest of this section only matters once RDF/DCAT-AP export enters the
picture.
DCAT-AP does not treat a dataset's publisher or contact point as free text:
dct:publisher is expected to be a foaf:Agent with its own identity, and
dcat:contactPoint a vcard:Kind -- both addressable, reusable entities, not
strings duplicated on every dataset. This extension's registry gives CKAN
real, database-backed records with stable URIs for exactly that purpose;
publisher_actor_id / contact_point_ids on the dataset are resolved into
the foaf:Agent / vcard:Kind RDF shape only when a dataset is serialized
for DCAT export -- nothing is duplicated onto the dataset itself.
A publisher's Kind (Organization or Person) determines the specific agent
type used on export. A contact point's phone number is additionally serialized
as a vcard:hasTelephone / vcard:Voice node carrying a tel: URI, the
usual vCard form. ckanext-dcat itself does not export phone numbers. The extension builds on ckanext-dcat's
EuropeanDCATAP3Profile, so a DCAT-AP 3 consumer gets correctly typed,
dereferenceable publisher and contact point data without extra configuration.
Screenshots
Everything below follows from one idea: publishers and contact points are maintained once, centrally, instead of being retyped on every dataset -- which is manageable for a handful of datasets and increasingly unmanageable as a catalog grows.
The screenshots are being retaken for 0.2.0. Each image link below is marked; the images will be added in a later commit.
Publishers: organizations and people
A publisher can represent either an organization or a person -- the Kind field
is the only difference, and everything else in the registry (the dataset
pickers, the quick-create widget, merging) treats both the same way. An
identifier and its scheme (for example SE:ORGNR) are optional but must be
given together, and the pair is unique among active publishers: a clash names
the existing publisher instead of failing with a generic error.
(Screenshot to be added later.)
(Screenshot to be added later.)
Contact points
Contact points are a separate, parallel register. A contact point can optionally belong to an organization ("Belongs to organization"), but doesn't have to -- a shared mailbox or a role-based contact works just as well as a named person.
(Screenshot to be added later.)
Linking a dataset to the registry
When editing a dataset, the publisher (a single publisher) and one or more contact points are chosen from the same registry used everywhere else in CKAN:
(Screenshot to be added later.)
If the publisher or contact point you need doesn't exist yet, either can be created without leaving the dataset form. And if one already exists but needs a correction, it can be edited from right here too -- the change applies everywhere it is used, not just on this dataset:
(Screenshot to be added later.)
What visitors see
On the public dataset page the publisher and each contact point are shown with their readable details -- name, web address, email and so on, one per line -- never internal IDs. A publisher that has been merged into another shows as the surviving publisher; inactive or missing records are simply left out.
(Screenshot to be added later.)
Publisher and contact point pages list the datasets they are linked to: the first ten, with the total and a "Show all" link to a paginated list. The lists respect dataset visibility, so private datasets are only shown to people who may see them.
(Screenshot to be added later.)
Merging two publishers
Merging two duplicate publishers -- select exactly two, choose which value wins per field, confirm, and the publisher that isn't kept is soft-retired and points to the remaining record. Datasets that pointed to the retired publisher are re-pointed automatically, so the surviving publisher's dataset count grows to reflect the merge:
(Screenshot to be added later.)
Soft-retiring rather than deleting exists for one reason: a merged publisher's
web address (its URI) may already be linked to, or cached, outside this
catalogue -- in a harvested copy, an external DCAT graph, a bookmark. A retired
publisher's own page keeps responding and points at the record it was merged
into, instead of going dead. Retired publishers are hidden from /actors by
default so the list stays focused on what's still in use, with a checkbox to
show them again. From there, a sysadmin can also permanently delete one --
useful for cleaning up test data or duplicates that were never referenced
outside this catalogue -- but doing so gives up that permanent-address
guarantee for good, which is why the extension asks for that decision
explicitly rather than doing it automatically after a merge.
A dataset with no publisher set
A newly created dataset with neither a publisher nor a contact point shows an inline warning on its own page, with pickers for both fields. This is also exactly what an editor sees, dataset by dataset, right after the extension is activated on a catalog that already has data -- see "Activating on an existing installation with datasets" below for the full picture, including the separate worklist view:
(Screenshot to be added later.)
The same dataset also shows up on the registry's own worklist, so it can be found and fixed from either place:
(Screenshot to be added later.)
Saving resolves the warning immediately, without leaving the page:
(Screenshot to be added later.)
Requirements and compatibility
- CKAN 2.11.1 or later 2.11 releases, or CKAN 2.12; the full automated suite (including the official DCAT-AP 3 SHACL validation) runs on all of them;
- PostgreSQL, as required by CKAN;
ckanext-schemingfor the included dataset form widgets; 3.1.0 is recommended (see docs/INSTALLATION.md for the versions that are tested and a known deprecation warning on CKAN 2.12);ckanext-dcatand RDFLib only when the optional RDF profile is enabled.
CKAN 2.11.0 and 2.10 and older are not supported (2.11.0 could not be tested: its official image has no working pytest plugin).
Installation
The quickest way to try the extension is the Docker Compose example. For an existing CKAN, follow docs/INSTALLATION.md; in short, install the package from PyPI in the same Python environment as CKAN:
pip install ckanext-actor-registry
To work on the extension, install a source checkout instead: pip install -e . in the
checkout.
Add actor_registry to ckan.plugins:
ckan.plugins = ... actor_registry
Create or upgrade the extension's database tables:
ckan -c /etc/ckan/default/ckan.ini db upgrade -p actor_registry
Restart CKAN. Logged-in users can then manage publishers at /actors and contact
points at /contactpoints.
Scheming integration
The extension supplies choice helpers, two form snippets, two display snippets and two optional validators. A minimal schema configuration is:
- field_name: publisher_actor_id
label: Publisher
preset: select
required: true
form_snippet: actors_multiple_select.html
display_snippet: actor.html
choices_helper: actors_choices
sorted_choices: true
validators: scheming_required actor_registry_actor_exists
- field_name: contact_point_ids
label: Contact points
preset: multiple_select
form_snippet: contactpoints_multiple_select.html
display_snippet: contactpoints.html
choices_helper: contactpoints_choices
sorted_choices: true
validators: ignore_missing scheming_multiple_choice actor_registry_contactpoints_exist
A complete, localised (en, sv, sv_SE) example schema ships with the extension
and is used by the tests: ckanext.actor_registry.examples:actor_registry_schema.yaml.
It is an integration example, not a universal metadata schema.
The field names shown above are part of the 0.x data contract. Datasets store
registry UUIDs in these fields (publisher_actor_id as a string,
contact_point_ids as a list). The display snippets resolve them to readable
details on the dataset page. For RDF export the profile resolves them into the
publisher and contact shapes that the Scheming profiles in ckanext-dcat
expect, at serialization time only; nothing extra is written onto the dataset.
The two actor_registry_*_exist validators are opt-in: they make a dataset
refuse a reference to a record that does not exist or is no longer active.
Optional DCAT profile
The core registry is independent of DCAT, country and organization type. For
RDF export with telephone support, enable the profile entry point after
installing ckanext-dcat:
ckanext.dcat.rdf.profiles = actor_registry_euro_dcat_ap_3
The profile subclasses EuropeanDCATAP3Profile. It delegates normal publisher
and contact serialization to ckanext-dcat and adds telephone values as
vcard:hasTelephone / vcard:Voice / vcard:hasValue with a tel: URI.
Theme and publisher-type URIs are also typed as skos:Concept so standalone
DCAT-AP 3 validation can identify their intended range. Store publisher types
as controlled-vocabulary URIs rather than local display labels.
This is an interoperability aid, not a claim that installing this extension
alone makes a catalog conformant with DCAT-AP or a national profile based on
it. Conformance also
depends on the complete dataset schema, validation and profile configuration.
Access control and data handling
- registry list and edit pages, and inline publisher/contact creation, are available to any logged-in CKAN user, not just sysadmins -- the registry holds shared, catalog-wide data meant for editors generally;
- merging two publishers is the one exception and stays restricted to CKAN sysadmins, since it is destructive and re-points datasets across the whole catalog rather than just the acting user's own organization;
- deployments must set
ckan.csrf_protection.ignore_extensions = falseso CKAN validates the CSRF tokens included by the management and inline forms; - individual publisher and contact point display pages are readable wherever the CKAN site itself permits access;
- input is validated on the server by one shared layer, for the forms, the inline
dialogs and the merge action alike: length limits that match the database
columns, email addresses (checked with CKAN's own validator), web addresses,
URIs and phone numbers. Stored web addresses and URIs are only rendered as
links when CKAN's
h.is_urlaccepts them; anything else is shown as plain text; - per-user convenience data stores only CKAN user IDs, the most recent publisher ID and recently used contact point IDs;
- the dataset lists on a publisher's or contact point's page, on their "Show all"
pages and in the
/actorsworklist only include private datasets that the visitor may see: sysadmins, and members of the dataset's organization. The totals shown follow the same rule, so a private dataset is never revealed by a count. FYI: a user who was given access to a single private dataset through CKAN's dataset collaborators feature (ckan.auth.allow_dataset_collaborators, off by default) but is not a member of its organization does not see that dataset in these lists; the dataset itself can still be opened directly; - a publisher can also be permanently deleted (
/actors/<id>/delete), sysadmin- only like merging. Datasets that had it as publisher have that field cleared; contact points it owned become standalone rather than being deleted themselves. This is a real delete, not a soft-retire -- see "Merging two publishers" above for why that distinction matters; - a contact point can likewise be permanently deleted
(
/contactpoints/<id>/delete), sysadmin-only. It is removed from thecontact_point_idsof any dataset that listed it (other contact points on the same dataset are left alone); an owning publisher, if any, is unaffected.
Activating on an existing installation with datasets
Enabling this extension on a CKAN instance that already has datasets does not retroactively link anything: every existing dataset starts out with no publisher and no contact point in the registry, because those are new, separate fields the extension adds via Scheming (see "Scheming integration" above). Three features exist specifically to help you find and fix these datasets afterwards; none runs automatically, so plan to use them as a one-time cleanup pass after activation.
Migrate to Actor Registry (/actor-registry/migrate). If your datasets
already hold contact details in CKAN's own free-text fields, this page turns
them into shared contact points. It reads Maintainer, and Author only when a
dataset has no maintainer, on the datasets that have no contact point yet,
and groups them by email address (or by name when there is no valid
address), showing the spellings used and any existing contact point with the
same address. For each group you create a contact point, link to an existing
one, or skip; clear the datasets that belong elsewhere and they stay for a
later round. One email address may back several contact points. The page
shows how many datasets are linked, skipped and remaining.
While the old fields are still in use, editors may keep changing them. When Maintainer or Author changes on a dataset after it was linked, the page lists it under Changed since linking, with the old and new values, and you choose: update the contact point (for every dataset that uses it), link this dataset to another contact point, create a new one, or keep it as it is.
Closing the migration. When you are done, a sysadmin closes the migration
on the same page. Maintainer and Author are then hidden in forms and on
dataset pages, for everyone including sysadmins, and are no longer exported;
their values stay stored, and saving a dataset keeps them. It takes effect at
once, without a restart, and the migration can be reopened. If datasets are
still without a contact point, or have unhandled changes, the page says how
many and asks you to confirm. Closing sets
ckanext.actor_registry.legacy_contact_fields to false at runtime (see
Installation).
Hiding needs one thing in your schema: give the old fields the extension's
legacy_contact.html snippets, which show them while the migration is open:
- field_name: maintainer
label: Maintainer
form_snippet: legacy_contact.html
display_snippet: legacy_contact.html
# ... and the same for maintainer_email, author and author_email
A schema without these fields has nothing to hide. A theme that overrides
Scheming's additional_info.html block itself must leave out rows with
display_snippet: legacy_contact.html while the migration is closed, as the
extension's own version does.
The original fields are never changed: linking only sets the contact points,
through a partial update that checks package_update for each dataset. A
dataset that fails your schema's validation is reported and the others are
still linked. Editors see and migrate only the datasets they may edit;
sysadmins all of them. Every decision is logged in the
actor_registry_migration_decision table (run ckan db upgrade -p actor_registry).
A worklist on /actors. The registry management page (Publishers) lists
the active datasets that are missing something, with a direct link to each one.
By default it shows the datasets missing both a publisher and any contact
point ("Datasets missing both a publisher and a contact point (N)"), which
surfaces the datasets nobody has touched yet. The Missing: buttons above the
list widen or change that: Publisher (every dataset without a publisher),
Contact point (every dataset without a contact point) or Either (missing at
least one of the two). The page shows the first ten with the total and a "Show
all" link to a paginated list, and private datasets only appear for people who
may see them.
An inline fix on the dataset's own page. Opening any dataset that is
missing a publisher and/or a contact point shows a warning box near the top
of the page with the same selector widgets (including "create new...") used
in the full dataset edit form, so editors recognize the control instead of
having to find the right field inside the advanced edit view. Saving it
performs a partial update -- only the field(s) you actually filled in are
touched, the rest of the dataset is left alone -- and requires the normal
package_update permission for that dataset, not sysadmin. The prompt
follows your schema: it asks for a contact point when the schema has that
field, and for a publisher only when the schema requires one.
Deactivating and cleaning up. Deactivating a publisher or contact point
stops new use of it: it is no longer offered in the forms, and choosing it
anyway gives a message that names it. Datasets that already use it keep it and
can still be saved; the form shows it marked inactive instead of dropping
it. The Cleanup page (/actor-registry/cleanup, a tab, also hinted on the
registry pages) lists the inactive records still in use, and datasets that
point to records that no longer exist, and replaces or removes such a
reference on the datasets you may edit (editors their own, sysadmins all; a
merged publisher suggests the one it was merged into).
All three rely on ckanext-scheming, so they only cover dataset types managed
by Scheming (the migration only those whose schema has the contact points
field).
Upgrade note for the prototype
The migration history retains the table name
contactpoints_alembic_version. This is intentional so installations upgraded
from the earlier, unpublished ckanext-contactpoints prototype preserve their
existing data. New installations should use only the actor_registry plugin
and migration command.
Upgrading from the 0.1.0 source snapshot? See the "Upgrading" section of the 0.2.0 release notes: run the migration, and note that the translation keys changed with the UI wording.
Known limitations
- no Action API is provided for registry administration;
- there is no import/export command for registry data;
- browser-based (end-to-end) tests and a formal security review are still pending;
- selection and validation of identifier schemes and publisher-type vocabularies is left to the deploying catalog;
- the dataset lists on publisher and contact point pages do not include private datasets that a user can only see as a dataset collaborator (see "Access control and data handling").
See CHANGELOG.md for release contents and CONTRIBUTING.md for how to help. Please report security issues according to SECURITY.md.
The automated and manual test strategy is documented in TESTING.md.
Development transparency
The implementation and documentation were developed by Björn Hagström with assistance from OpenAI's ChatGPT and Codex and, from version 0.2.0, from Anthropic's Claude Code. The human maintainer directs, reviews and accepts responsibility for the released work, including its correctness, security, licensing and provenance. See AI_ASSISTANCE.md.
License
MIT. See LICENSE.
Metadata
Release files for ckanext-actor-registry 0.3.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| ckanext_actor_registry-0.3.1.tar.gz | 229.9 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| ckanext_actor_registry-0.3.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 468.9 kB
Release files / ckanext_actor_registry-0.3.1.tar.gz
| Download URL | ckanext_actor_registry-0.3.1.tar.gz |
|---|---|
| Size | 229.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
24da12914fb4d6cc2cdbce1c3bdbbee9f41ee469861a99553f28766398888c8f
|
|
BLAKE2b-256 checksum How to use checksums |
042c2bef8187cbb540dea9dbf44dd08e9cd912cec701307ad4c09c9d5fef5162
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.9.6
|
Release files / ckanext_actor_registry-0.3.1-py3-none-any.whl
| Download URL | ckanext_actor_registry-0.3.1-py3-none-any.whl |
|---|---|
| Size | 239.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
20b6637cdc66d1d51d55641b2ed9671ee71822f237cdc99d940583c1a11d281a
|
|
BLAKE2b-256 checksum How to use checksums |
51a13963e159c5fa97ef54e7f9ad3bcaa494edfaf71d13370d6cf04568741f9f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.9.6
|