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.
- Client queries with a TEI or PURL identifier
- Server resolves the package via the PyPI JSON API
- 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 - If range requests aren't supported (some CDN configurations), falls back to downloading the full wheel
- SBOM files are extracted and mapped to TEA entities
- Everything is cached in Redis and served as TEA-compliant responses
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
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
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.
- Python 3.14+
- uv
- Redis
git clone https://github.com/sbomify/pypi-tea.git
cd pypi-tea
uv sync
uv run uvicorn pypi_tea.app:appThe server starts at http://localhost:8000. Visit the root URL for a live statistics dashboard.
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 |
| 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) |
| Endpoint | Description |
|---|---|
GET / |
Statistics dashboard (Tailwind + Chart.js) |
GET /stats |
Raw statistics JSON |
GET /stats/timeseries |
Time-bucketed statistics (5-min intervals, 24h retention) |
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"| 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.
A systemd service file and setup script are included in deploy/.
# As root:
sudo ./deploy/setup.sh
sudo systemctl start pypi-teaThe 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/1Logs:
journalctl -u pypi-tea -f# 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/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.
Apache-2.0