Skip to main content

TEA Server for PyPI Package SBOMs

Project description

pypi-tea

CI PyPI version Python License sbomified

A Transparency Exchange API (TEA) server that extracts and serves SBOMs from PyPI packages.

Python wheels can include SBOMs in .dist-info/sboms/ (PEP 770). pypi-tea makes these discoverable through the TEA protocol — give it a PURL like pkg:pypi/requests@2.31.0 and it returns any SBOMs found in the package's wheel files.

How it works

  1. Client queries with a TEI or PURL identifier
  2. Server resolves the package via the PyPI JSON API
  3. Wheel files are inspected using HTTP range requests (via remotezip) to avoid downloading full wheels — only the ZIP central directory (~16KB) is fetched to check for .dist-info/sboms/ entries
  4. If range requests aren't supported (some CDN configurations), falls back to downloading the full wheel
  5. SBOM files are extracted and mapped to TEA entities
  6. Everything is cached in Redis and served as TEA-compliant responses

Architecture

graph LR
    Client -->|TEI / PURL| App["pypi-tea<br/>(FastAPI)"]
    App -->|JSON API| PyPI
    App -->|Range request<br/>or full GET| Wheels["Wheel files"]
    App <-->|Caching| Redis

Caching

All data is stored in Redis. Nothing is persisted to disk — Redis is the sole data store.

Data Redis key pattern TTL Description
PyPI metadata pypi:{package}:{version} 1 hour JSON API response for a package version
SBOM content sbom:{wheel_url} 24 hours Extracted SBOM files from a wheel (JSON array)
Negative cache neg:{wheel_url} 24 hours Marker for wheels confirmed to have no SBOMs
UUID lookup uuid:{uuid} No expiry Maps a deterministic UUID to entity metadata
Entity index etype:{entity_type} No expiry Redis set of UUIDs per entity type (for listing)
Stats (totals) stats No expiry Redis hash with cumulative hit/miss counters
Stats (time series) stats:ts:{bucket} 24 hours Per-5-minute counter buckets for time-series graphs

Why these TTLs?

  • PyPI metadata changes when new versions are released — 1 hour keeps things reasonably fresh
  • Wheel contents are immutable once published — 24 hours is conservative; SBOMs won't change
  • Negative cache prevents repeatedly downloading wheels that have no SBOMs
  • UUID lookups don't expire because they map deterministic UUIDs to stable data

Statistics

The server tracks cache hit/miss ratios and SBOM availability:

  • Cache metrics: hits and misses for PyPI metadata, SBOM content, and negative cache lookups
  • SBOM availability: how many wheels had SBOMs vs didn't
  • Time series: all counters are also bucketed into 5-minute intervals (24h retention) for trend visualization

Visit GET / for a live dashboard with charts, or GET /stats for raw JSON.

Quick start

Prerequisites

  • Python 3.14+
  • uv
  • Redis

Install and run

git clone https://github.com/sbomify/pypi-tea.git
cd pypi-tea
uv sync
uv run uvicorn pypi_tea.app:app

The server starts at http://localhost:8000. Visit the root URL for a live statistics dashboard.

Configuration

All settings are configurable via environment variables with the PYPI_TEA_ prefix:

Variable Default Description
PYPI_TEA_REDIS_URL redis://localhost:6379/0 Redis connection URL
PYPI_TEA_PYPI_BASE_URL https://pypi.org PyPI API base URL
PYPI_TEA_SERVER_ROOT_URL http://localhost:8000 Public root URL (used in TEA discovery responses)
PYPI_TEA_TEA_SPEC_VERSION 0.3.0-beta.2 TEA spec version to advertise

TEA endpoints

Endpoint Description
GET /discovery?tei=... TEI-based discovery
GET /products List or search products (idType, idValue query params)
GET /product/{uuid} Get a product
GET /product/{uuid}/releases List releases for a product
GET /productReleases List or search product releases
GET /productRelease/{uuid} Get a product release
GET /productRelease/{uuid}/collection/latest Latest SBOM collection
GET /productRelease/{uuid}/collections All collections
GET /productRelease/{uuid}/collection/{version} Collection by version
GET /component/{uuid} Get a component (wheel)
GET /component/{uuid}/releases Component releases
GET /componentRelease/{uuid} Get a component release with collection
GET /componentRelease/{uuid}/collection/latest Latest collection
GET /componentRelease/{uuid}/collections All collections
GET /componentRelease/{uuid}/collection/{version} Collection by version
GET /artifact/{uuid} Get an artifact (SBOM)

Non-TEA endpoints

Endpoint Description
GET / Statistics dashboard (Tailwind + Chart.js)
GET /stats Raw statistics JSON
GET /stats/timeseries Time-bucketed statistics (5-min intervals, 24h retention)

Example

Discover SBOMs for a package:

curl "http://localhost:8000/discovery?tei=urn:tei:purl:localhost:pkg:pypi/cyclonedx-python-lib@8.4.0"

Search by PURL:

curl "http://localhost:8000/products?idType=PURL&idValue=pkg:pypi/cyclonedx-python-lib@8.4.0"

Data model

PyPI Concept TEA Entity UUID derived from
Package (e.g. requests) Product uuid5(NS, "pkg:pypi/requests")
Version (e.g. 2.31.0) ProductRelease uuid5(NS, "pkg:pypi/requests@2.31.0")
Wheel file Component + ComponentRelease uuid5(NS, "wheel:<filename>") / uuid5(NS, <wheel_url>)
SBOM file in wheel Artifact uuid5(NS, "sbom:<wheel_url>:<sbom_path>")

All UUIDs are deterministic (UUID v5 with a fixed namespace) so they're stable across requests and server restarts.

Deployment

A systemd service file and setup script are included in deploy/.

# As root:
sudo ./deploy/setup.sh
sudo systemctl start pypi-tea

The setup script creates a dedicated pypi-tea system user and installs the systemd service. The service uses uvx to run pypi-tea directly from PyPI — no cloning or venv management needed.

Important: Set PYPI_TEA_SERVER_ROOT_URL to your public URL — this is used in TEA discovery responses so clients know where to reach your server.

sudo systemctl edit pypi-tea
[Service]
Environment=PYPI_TEA_SERVER_ROOT_URL=https://tea.example.com
Environment=PYPI_TEA_REDIS_URL=redis://my-redis:6379/1

Logs:

journalctl -u pypi-tea -f

Development

# Install dev dependencies
uv sync --group dev

# Run tests (uses fakeredis, no Redis required)
uv run pytest

# Lint
uv run ruff check src/ tests/

# Format
uv run ruff format src/ tests/

# Type check
uv run mypy src/

Conformance

The test suite runs libtea's conformance checks against an in-process server. 21 of 26 checks pass; 5 CLE (Collection Lifecycle Event) checks are skipped as CLE is not implemented.

License

Apache-2.0

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pypi_tea-0.3.1.tar.gz (57.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pypi_tea-0.3.1-py3-none-any.whl (70.0 kB view details)

Uploaded Python 3

File details

Details for the file pypi_tea-0.3.1.tar.gz.

File metadata

  • Download URL: pypi_tea-0.3.1.tar.gz
  • Upload date:
  • Size: 57.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for pypi_tea-0.3.1.tar.gz
Algorithm Hash digest
SHA256 cb6ab15b642d0f4a773898f3e2368a3f12db5832b4dfabd31536058de88d99da
MD5 314f3a532ce4cd87ac620bc77bd55975
BLAKE2b-256 227dff722711dfa8e7850cd75e758c000d7a85c63f1d507f3e491c4d90eca93d

See more details on using hashes here.

Provenance

The following attestation bundles were made for pypi_tea-0.3.1.tar.gz:

Publisher: pypi.yaml on sbomify/pypi-tea

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file pypi_tea-0.3.1-py3-none-any.whl.

File metadata

  • Download URL: pypi_tea-0.3.1-py3-none-any.whl
  • Upload date:
  • Size: 70.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for pypi_tea-0.3.1-py3-none-any.whl
Algorithm Hash digest
SHA256 0350a278dc3ee21df81ff558b7b8845ec60be9471331e62fc59ed16dc81292a8
MD5 20a3e944eafc3f11102682f54f9a1d2a
BLAKE2b-256 51492f38550bd7a8b809f0e4521cbfd2f1828ef3661b2ea6a0a680d013fd41ba

See more details on using hashes here.

Provenance

The following attestation bundles were made for pypi_tea-0.3.1-py3-none-any.whl:

Publisher: pypi.yaml on sbomify/pypi-tea

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page