pytest-Jira-xray-plus
Run your tests with pytest. Keep each input case in a local report. When you are ready, send a summary and readable case details to Jira/Xray.
This tutorial takes you from two passing cases to an Xray-ready report using a single file. You can complete steps 1–7 without Jira or credentials.
Start the tutorial · Send to Jira · Options · Troubleshooting
What this package produces: a local record for every observed pytest case, and optionally one Xray result per Jira Test key. Parameter details can appear in that result's comment. Native Xray iteration rows and datasets are not supported.
This guide accompanies the 0.1.1 documentation patch. Runtime behavior is unchanged from 0.1.0. The complete tutorial below works with the installed package.
1. Create a project
You need Python 3.12 or newer and a terminal. Start in a folder where you keep small Python projects. These commands use Bash on Linux or macOS:
mkdir xray-demo
cd xray-demo
python3.12 -m venv .venv
. .venv/bin/activate
python -m pip install pytest-jira-xray-plus==0.1.1
Here is what each command does:
mkdircreates the project folder;cdenters it.python3.12 -m venv .venvcreates an isolated Python environment in.venv.- Activating it makes
pythonuse that environment. pip installinstalls this package and its required dependencies, including pytest.
On Windows, create the environment with py -3.12 -m venv .venv. Activate it
with .venv\Scripts\Activate.ps1 in PowerShell or
.venv\Scripts\activate.bat in Command Prompt. Use the same python -m ...
commands for the rest of the local tutorial.
Use a fresh environment. Do not also install pytest-jira-xray: that upstream
package and this fork share the pytest_xray module and xray plugin name.
Installing both can overwrite files. If that happened, recreate the environment.
No repository clone or configuration file is needed. Package installation needs internet access; the local test commands below do not contact Jira.
2. Run two input cases
Create test_example.py inside xray-demo, next to .venv.
Paste this complete file:
import pytest
@pytest.mark.parametrize(
"value, expected",
[(1, 2), (2, 3)],
ids=["one-plus-one", "two-plus-one"],
)
def test_add_one(value, expected):
assert value + 1 == expected
Run it from xray-demo:
python -m pytest -q test_example.py
Expected summary (timing and progress characters may vary):
2 passed
Here is how the file works:
import pytestlets us use pytest's test decorators.- The function name starts with
test_, so pytest discovers it. parametrizeruns the function once for each input pair: first1, 2, then2, 3. These two runs are called cases.idsgives those cases readable names. Use names without secrets.assertchecks that adding one gives the expected result.
python -m pytest uses pytest from your active environment. -q keeps output
short. You have not added a Jira marker or contacted any server.
Your folder contains:
xray-demo/
.venv/
test_example.py
Pytest may also create cache folders. You do not need to edit them.
3. Save that result locally
Keep the test file as it is. Add one option to the command:
python -m pytest -q test_example.py --xray-case-report=cases.json
You still get 2 passed. You also get cases.json in xray-demo.
The new option tells this plugin where to write its local report. It does not
enable Jira uploading.
Open the file in your editor, or pretty-print it:
python -m json.tool cases.json
Here is an excerpt from the report. It shows the actual case names and summaries;
we have left out each case's attempts list to keep this first view short:
{
"schema_version": 1,
"pytest_exit_status": 0,
"cases": [
{
"nodeid": "test_example.py::test_add_one[one-plus-one]",
"status": "passed"
},
{
"nodeid": "test_example.py::test_add_one[two-plus-one]",
"status": "passed"
}
]
}
pytest_exit_status: 0 means the test session succeeded. A nodeid identifies the
file, function and input case. Each full case record also has an attempts list,
with observed phases:
| Phase | Meaning |
|---|---|
setup |
Prepare the test and any fixtures. |
call |
Run the test function. |
teardown |
Clean up after the test. |
A fixture is pytest code that prepares or cleans up a test resource. Our example has no custom fixtures, but pytest still reports these phases.
cases.json is replaced on each run. Use a different filename to keep an older
run. Its parent folder must already exist. This file is the plugin's own format;
it is not an Xray import file.
4. Choose what the local report contains
By default, the report includes readable case IDs but excludes input values and failure text. You can stop here and keep that default. The next two commands show alternative choices, not options to combine.
To include scalar input values:
python -m pytest -q test_example.py --xray-case-report=cases.json --xray-case-values
Expected: 2 passed. Each attempt now has parameter_values, such as
{"value": 1, "expected": 2} for the first case. This is useful when the values are
safe to store; leave the option off otherwise.
To hide the function and parameter identities instead:
python -m pytest -q test_example.py --xray-case-report=cases.json --xray-case-redact-identities
Expected: 2 passed. The case names become aliases such as case-1, and input
values remain absent. Aliases are assigned in arrival order and cannot be matched
across separate runs. This affects only the local JSON, not console output or
future Xray comments. It cannot be combined with --xray-case-values.
The following steps return to the default reporting command, so readable case IDs will be present and values will be absent again.
5. See what happens when one case fails
Replace the file with this version. Only the second expected value has changed,
from 3 to 99:
import pytest
@pytest.mark.parametrize(
"value, expected",
[(1, 2), (2, 99)],
ids=["one-plus-one", "two-plus-one"],
)
def test_add_one(value, expected):
assert value + 1 == expected
python -m pytest -q test_example.py --xray-case-report=cases.json
Expected summary:
1 failed, 1 passed
This is an intentional failure. Pytest exits with code 1, rather than 0
for success. The report is still written. one-plus-one remains passed;
two-plus-one is failed. Neither case disappears because another case has a
different result.
Checkpoint: you now have useful local reporting. You can apply
--xray-case-report=cases.json to your own pytest suite without adding Jira
markers or credentials. The remaining steps are optional.
6. Prepare an Xray result without uploading
Restore the passing inputs and add a Jira Test marker. Replace the whole file:
import pytest
@pytest.mark.xray("DEMO-1")
@pytest.mark.parametrize(
"value, expected",
[(1, 2), (2, 3)],
ids=["one-plus-one", "two-plus-one"],
)
def test_add_one(value, expected):
assert value + 1 == expected
The new line, @pytest.mark.xray("DEMO-1"), associates both cases with one Jira
Test issue. DEMO-1 is just a placeholder here; offline export does not look
it up. A Test Execution is the Jira issue that holds a set of test results.
A Test Run is the result of one Test inside that execution.
Keep the local report and add three options:
python -m pytest -q test_example.py --xray-case-report=cases.json --jira-xray --xraypath=xray.json --allow-duplicate-ids
| Addition | What it does |
|---|---|
--jira-xray |
Enables export of tests carrying the Xray marker. |
--xraypath=xray.json |
Writes the export to this file instead of sending it. |
--allow-duplicate-ids |
Allows the two cases to share DEMO-1; their Xray results are combined. |
Expected: 2 passed, plus a message showing the generated Xray report file.
You now have two reports with different purposes:
| File | What you will find |
|---|---|
cases.json |
Two separate pytest cases, each passed. |
xray.json |
One Xray result for DEMO-1, with status: "PASS". |
Why one Xray result? Both cases refer to the same Jira Test. This exporter combines them under that key. It does not create native Xray iteration rows.
To keep readable input details in that combined result, add one more option:
python -m pytest -q test_example.py --xray-case-report=cases.json --jira-xray --xraypath=xray.json --allow-duplicate-ids --xray-parameter-details
Open xray.json. Its comment now contains both case names, scalar input
values, the reported phase and its outcome. Open cases.json: it still has two
separate records and still excludes input values by default.
--xray-parameter-details is an explicit opt-in because its comments can contain
sensitive values. It adds details only for parametrized tests. Without it,
passing cases usually have no comment text; failure details can still appear.
Keep --xraypath in every offline export command. Without it, --jira-xray
selects uploading. This also applies when options are inherited from pytest.ini
or PYTEST_ADDOPTS.
7. Keep full comments when an export is shortened
Large parametrized suites can produce long comments. The default client policy limits each exported comment to 32,000 UTF-8 bytes. This is a configurable client choice, not a universal Jira/Xray server limit.
To see the behavior with our small example, deliberately choose a tiny limit:
python -m pytest -q test_example.py --xray-case-report=cases.json --jira-xray --xraypath=xray.json --allow-duplicate-ids --xray-parameter-details --xray-comment-limit=256 --xray-full-report=full-xray.json
Two new options work together:
--xray-comment-limit=256bounds each final exported comment to 256 UTF-8 bytes, including the truncation notice. It does not limit the entire request or attached evidence. The minimum is 256.--xray-full-report=full-xray.jsonsaves the complete original Xray payload before any comment is shortened.
Expected: 2 passed and a message containing truncated 1 comment(s).
| File | Result |
|---|---|
cases.json |
Both structured cases, unaffected by the comment limit. |
full-xray.json |
Full original comment containing both case identities and values. |
xray.json |
Shortened comment with an explicit TRUNCATED notice. |
If an oversized comment has no full-report path, export stops instead of silently losing text. If the archive cannot be written, export also stops. An otherwise passing pytest session then exits with code 3. Statuses and evidence are not truncated. Use the default limit for ordinary evaluation, then validate the appropriate size with your server administrator.
The full archive can contain values, failure text and evidence. Protect it like test data. All three report paths must differ. Files are replaced on each run; choose unique paths to retain history.
8. Send to Jira (optional)
Everything above was local. This step sends a request to a server. Start with an authorized disposable test project and existing credentials; no credentials or private test data need to be shared with the maintainer.
Prepare the two issue keys
- Choose an existing sandbox Jira Test issue. Replace
DEMO-1in the marker with its key. - Choose an existing sandbox Test Execution, for example
DEMO-100. You will pass that key as--execution. - Preview the intended upload with the command below. Replace
DEMO-100with your chosen execution key. Keep--xraypath=xray.json: this preview is local.
python -m pytest -q test_example.py --jira-xray --execution=DEMO-100 --allow-duplicate-ids --xray-parameter-details --xray-case-report=cases.json --xray-full-report=full-xray.json --xraypath=xray.json
python -m json.tool xray.json
Expect 2 passed. Check testExecutionKey matches the intended execution,
tests[0].testKey matches your Test marker, and comment contains the case details
you intend to send. This uses the normal comment limit, not step 7's deliberately
small demonstration limit. No credentials are required for this file export.
The two keys are not interchangeable: one identifies the Test; the other selects where its result goes.
Configure Server or Data Center authentication
This example uses an existing personal access token and Bash. It reads the token without placing it in shell history:
export XRAY_API_BASE_URL='https://jira.example.invalid'
read -r -s -p 'Existing sandbox Jira token: ' XRAY_API_KEY
printf '\n'
export XRAY_API_KEY
Replace the .invalid URL with the authorized sandbox base URL. Include Jira's
context path, if it has one, but do not append a REST endpoint. The plugin adds
/rest/raven/2.0/import/execution itself.
XRAY_API_BASE_URL tells the plugin where Jira is. XRAY_API_KEY holds the token;
--api-key-auth below selects that authentication method. In CI, inject these
through protected environment variables rather than committing them to files.
Upload the result
Use the same passing test file. This time remove --xraypath:
python -m pytest -q test_example.py --jira-xray --api-key-auth --execution=DEMO-100 --allow-duplicate-ids --xray-parameter-details --xray-case-report=cases.json --xray-full-report=full-xray.json
Replace DEMO-100 with your chosen execution key. The new options select token
authentication and an existing Test Execution. Keep --cloud off for
Server/Data Center.
On successful import, pytest prints the returned Test Execution ID. In Jira, open that Test Execution and the Test Run for your Test. Expect one aggregate result and comment text with case details, not native per-input iteration rows. The two local report files remain available for inspection.
When finished with this Bash session:
unset XRAY_API_KEY
Omitting --execution asks Xray to create a new Test Execution, subject to its
required fields and permissions. Start with an explicit disposable execution so
you know what is being updated. Reimports can replace existing Test Run details,
including dataset/iteration information; inspect a run before retrying a timeout.
Imports are not retried automatically because the server might already have
accepted the first request. This replacement behavior is documented in the
official Server/DC import API.
This command uses the aggregate comment fallback. Confirm the effective Test type with your administrator if your project uses a custom label such as Automatic. A label alone does not establish Generic behavior or native iteration support. See the compatibility section below for the tested versions and limits.
Optional recipes
Scalar values are strings, integers, finite floats, booleans or None. Other
values get a placeholder instead of invoking arbitrary object serialization.
Indirect fixtures record their input parameters, not the fixture's return value.
Use step 4 to choose values or identity aliases for the local report.
Parallel tests and retries
Install the optional plugins separately:
python -m pip install pytest-xdist==3.8.0 pytest-rerunfailures==16.7
python -m pytest -q test_example.py -n 2 --xray-case-report=cases.json
python -m pytest -q test_example.py --reruns=1 --xray-case-report=cases.json
-n 2 uses two worker processes. --reruns=1 allows one retry after a failure.
Our restored passing example has nothing to retry. With a failing-then-passing
test, pytest may exit 0 while this reporter still records an earlier failure;
see the status table below. The main process writes one report. Results can arrive
in a different order. With --dist=each, the same node observed on two workers
is retained as two worker attempts.
Associate one function with multiple Tests
Replace the marker in step 6 with @pytest.mark.xray("DEMO-1", "DEMO-2").
Each marked Test receives the combined result. Use this only when those Test
issues are genuinely intended to share the result.
Authentication and execution settings
Choose one authentication method:
| Method | CLI option | Required environment variables |
|---|---|---|
| Jira personal access token | --api-key-auth |
XRAY_API_BASE_URL, XRAY_API_KEY |
| Basic authentication, if enabled by your administrator | Neither auth flag | XRAY_API_BASE_URL, XRAY_API_USER, XRAY_API_PASSWORD |
| Xray Cloud client credentials | --cloud --client-secret-auth |
XRAY_API_BASE_URL, XRAY_CLIENT_ID, XRAY_CLIENT_SECRET |
Cloud is a different service. It uses the Xray API base URL, /api/v2/authenticate
and /api/v2/import/execution, with PASSED/FAILED status spelling. Server/DC
uses PASS/FAIL. --cloud does not enable native iterations or change Test types.
Cloud has synthetic test coverage, not live target validation.
TLS certificate verification is enabled by default. Set XRAY_API_VERIFY_SSL
to True or to a trusted CA bundle path for an internal CA. False disables
verification and should not be used with real credentials.
Optional execution metadata:
| Environment variable | Meaning |
|---|---|
XRAY_EXECUTION_SUMMARY |
Execution title. A new execution defaults to Execution of automated tests. |
XRAY_EXECUTION_DESC |
Execution description. |
XRAY_EXECUTION_REVISION |
Revision, such as a commit ID. |
XRAY_EXECUTION_TEST_ENVIRONMENTS |
Environment names separated by whitespace. |
XRAY_EXECUTION_FIX_VERSION |
Fix version; the first nonempty parsed value is used. |
These variables configure the legacy exporter; local case reporting does not need them. Secrets belong in your shell environment or protected CI settings, not test files or command-line arguments.
All package options
All on/off switches below default to off. Paths and issue keys are text values
and default to unset; --xray-comment-limit is an integer defaulting to 32000.
The table describes the combinations required to make each option useful.
| Option | Effect |
|---|---|
--xray-case-report=PATH |
Write observed cases locally. Parent folder must exist. No Jira required. |
--xray-case-values |
Include scalar input values in the case report; requires its path. |
--xray-case-redact-identities |
Replace local names/IDs with per-run aliases; requires the case report and excludes case values. |
--jira-xray |
Enable export of marked tests. Uploads unless --xraypath is set. |
--xraypath=PATH |
With --jira-xray, write Xray JSON instead of uploading. Creates parent folders. |
--allow-duplicate-ids |
Combine cases that share a Jira Test key. |
--xray-parameter-details |
Add parameter identities and scalar values to legacy comments. |
--xray-comment-limit=BYTES |
Maximum UTF-8 bytes per final comment; default 32000, minimum 256. |
--xray-full-report=PATH |
Archive the complete original legacy payload before shortening comments; requires --jira-xray. |
--execution=KEY |
Update an existing Test Execution. |
--testplan=KEY |
Set a Test Plan key in execution metadata. |
--api-key-auth |
Authenticate using XRAY_API_KEY as a Bearer token. |
--client-secret-auth |
Authenticate with Xray Cloud client credentials. |
--cloud |
Use the Cloud import endpoint and status spelling. |
--add-captures |
Add captured stdout, stderr and logs to comments; may expose secrets. |
Use python -m pytest --help for pytest's own options. --collect-only does not
write reports. No option enables native iteration rows.
Result rules and compatibility
| Event | Local case summary | Legacy Xray summary |
|---|---|---|
| Passing call | passed | PASS |
| Failed call or fixture setup/teardown | failed | FAIL |
| Skip | skipped | ABORTED |
Expected failure (xfail) |
skipped; xfail recorded | FAIL |
Unexpected pass (xpass) |
passed; xpass recorded | PASS |
| Strict unexpected pass | failed | FAIL |
| Failed attempt then successful retry | failed; both attempts retained | FAIL |
The local report preserves any earlier failure. Legacy grouping retains upstream's priority: ABORTED outranks FAIL. A group containing a skip and failure therefore has ABORTED status, with failure information in comments. This differs from local failure-first grouping. Statuses are not interchangeable with pytest's exit code.
Publishing failure changes an otherwise successful pytest exit to 3; an existing nonzero exit is preserved. Remote imports are withheld after worker crashes, fail-fast/maxfail stops, collection failures (including continue-on-error), interruptions, or when there are no marked results. Local files can still contain partial observations. Reports list observed results, not an inventory of tests that never ran. Hard process termination can prevent any final file from being written.
Requests use a 10-second connect timeout and a 60-second read-inactivity timeout, not a total deadline. Redirects are rejected. Authentication and response shapes are checked. The plugin does not dump raw server responses to shared temporary files. Local file replacement is atomic; failed writes retain any prior report.
Python metadata requires >=3.12, pytest >=9.1.1,<10 and requests >=2.34.2,<3.
CI checks Linux on Python 3.12, 3.13 and 3.14; the tested baseline uses pytest 9.1.1,
requests 2.34.2, xdist 3.8.0 and rerunfailures 16.7. Windows/macOS instructions
are provided but those platforms have not been executed in this validation.
Future dependency versions are not automatically certified.
Subtests and repeat plugins are not supported as distinct case models. Reliable case identity across worker crashes/restarts, cross-session aggregation and per-iteration evidence are outside this release. Crashed-worker reports can lack parameter metadata or Jira keys. Native rows and datasets are unsupported even when a Test type's label is Automatic.
Troubleshooting and migration
| What happened | What to do |
|---|---|
unrecognized arguments |
Activate the intended environment and use python -m pytest. Check python -m pip show pytest-jira-xray-plus. If plugin autoload is disabled, add -p pytest_xray.plugin. |
| Duplicate plugin/import errors | Create a fresh environment with this fork only, not upstream pytest-jira-xray. |
| Duplicate Jira IDs | Add --allow-duplicate-ids only when the cases intentionally share one Test. |
No xray.json |
Use both --jira-xray and --xraypath=xray.json. Case reporting is separate. |
| Cannot write a report | Check permissions and distinct paths. Create the case-report parent folder. A previous file retained after failure is not a new result. |
| Comment exceeds the limit | Add --xray-full-report=PATH to retain full text before truncation; inspect the resulting export. |
| Missing authentication variable | Set the variables for your selected auth method. Offline file exports do not need credentials. |
| HTTP 401/403 | Check existing credentials and permissions with the sandbox administrator. Do not paste secrets into issue reports. |
| HTTP 404 or redirect | Check the base URL, context path and Server/DC versus Cloud mode. |
| Certificate error | Configure the trusted CA bundle; do not bypass verification. |
| Timeout, invalid JSON or missing execution ID | Inspect local server/proxy information and check whether the import already succeeded before retrying. |
| No native parameter rows | Expected: this exporter provides aggregate status and optional comment detail, not native iterations. |
From upstream: create a fresh environment and install pytest-jira-xray-plus.
The Python imports, marker and xray plugin name are retained. Check the dependency
requirements, add local reporting as needed, and compare an offline export before
uploading. Do not install both distributions together.
From 0.1.0: 0.1.1 is a documentation patch with unchanged runtime behavior. From 0.1.0a1: later releases add bounded comments, meaningful publishing failures, atomic file export and failure-preserving rerun summaries; update CI expectations.
Advanced: customize the export with hooks
A hook is a Python function pytest calls at a particular point in a run. You do
not need one for the tutorial. If you need custom execution metadata or evidence,
create conftest.py beside test_example.py:
import pytest
from pytest_xray import evidence
def pytest_xray_results(results, session):
results["info"]["revision"] = "tutorial-revision"
@pytest.hookimpl(hookwrapper=True)
def pytest_runtest_makereport(item, call):
outcome = yield
report = outcome.get_result()
if report.when == "call":
report.evidences = getattr(report, "evidences", []) + [
evidence.text(data="synthetic evidence", filename="example.txt")
]
python -m pytest -q test_example.py --jira-xray --xraypath=xray.json --allow-duplicate-ids --xray-parameter-details
Expected: 2 passed. xray.json contains info.revision: "tutorial-revision"
and two evidence entries, one per case, on the combined Test result. The evidence
text is encoded in the payload. These are Test Run attachments, not per-iteration
attachments. cases.json is not written by this command.
pytest_xray_results edits the final legacy payload before archival and comment
bounds are applied. The report hook attaches evidence after pytest creates each
call report. Hooks execute trusted local Python and can change correctness or
expose data; use only the fields and content you intend to export. Delete this
optional conftest.py when finished with the example.
Project and development
This is an independent Apache-2.0 fork of
pytest-jira-xray, based on
6ac1117585da590001cdbc2bf261b78683c19dcb (upstream's 0.9.4 line).
Report issues with synthetic examples through
GitHub issues.
From a checkout in a fresh environment:
python -m pip install -e . -r requirements-tests.txt build==1.6.1 setuptools==84.0.0 wheel==0.48.0 twine==7.0.0
python -m pytest -q
python tools/reproduce.py src --details
python tools/validate_offline.py
python tools/build_release.py release-dist
python -m twine check release-dist/*
Tests use synthetic data, blocked transport or a localhost fake service. Release evidence distinguishes independent checks from user-reported validation. The sandbox capability procedure is optional research, not implemented native support. It requests no private data. Publishing uses the existing manual workflow, reviewed commit and artifact hashes.
Metadata
Release files for pytest-jira-xray-plus 0.1.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 | |
|---|---|---|---|
| pytest_jira_xray_plus-0.1.1.tar.gz | 73.9 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| pytest_jira_xray_plus-0.1.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 107.7 kB
Release files / pytest_jira_xray_plus-0.1.1.tar.gz
| Download URL | pytest_jira_xray_plus-0.1.1.tar.gz |
|---|---|
| Size | 73.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
4028c60385b2756c42b3e0077ba370d93b75b234d96dcd215bede86f5afa58f6
|
|
BLAKE2b-256 checksum How to use checksums |
317f967ae12c14e51f8990c2f4b58d3790f2101d6ef7fdb047ae8c012fdb447e
|
| 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 15, 2026.
Transparency logRelease files / pytest_jira_xray_plus-0.1.1-py3-none-any.whl
| Download URL | pytest_jira_xray_plus-0.1.1-py3-none-any.whl |
|---|---|
| Size | 33.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
3452b95760f3a2281c6182a733ca2452003e6ae2ad05315570dd0e237ebc96cd
|
|
BLAKE2b-256 checksum How to use checksums |
3025b1506c56c3f97065b4ece25c053f93fbee41c145af0842ace76e6b22a6fa
|
| 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 15, 2026.
Transparency log