Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
f3f1f81
Add TEA client exception hierarchy and Pydantic models
aurangzaib048 Feb 25, 2026
6062852
Add internal HTTP client and discovery functionality
aurangzaib048 Feb 25, 2026
409c9b3
Add TeaClient and download functionality with checksum verification
aurangzaib048 Feb 25, 2026
0040fa7
Add GitHub Actions workflows for CI and CodeQL analysis, and configur…
aurangzaib048 Feb 25, 2026
f961fbf
Address PR review: clean up partial downloads, fix checksum verificat…
aurangzaib048 Feb 25, 2026
4a27168
Address PR review round 2: user-agent, JSON error handling, download …
aurangzaib048 Feb 25, 2026
6292926
Refactor user-agent handling to retrieve package version dynamically
aurangzaib048 Feb 25, 2026
b95d314
Address PR review round 3: follow redirects, discovery error handling…
aurangzaib048 Feb 25, 2026
ab31da5
Refactor HTTP client to use requests library and update dependencies
aurangzaib048 Feb 25, 2026
10438c6
Add integration tests for TEA API client
aurangzaib048 Feb 25, 2026
d0131aa
Enhance README and API client with new features and validations
aurangzaib048 Feb 25, 2026
48bbe32
Update README, enhance discovery functionality, and add UDI support
aurangzaib048 Feb 25, 2026
77d1e69
Enhance CI workflows, update dependencies, and improve error handling
aurangzaib048 Feb 25, 2026
9eec887
Enhance documentation and validation in TEA API client
aurangzaib048 Feb 25, 2026
905b152
Implement TeiType enumeration and refactor checksum handling
aurangzaib048 Feb 25, 2026
0b118aa
Update dependencies, enhance README, and improve validation in API cl…
aurangzaib048 Feb 25, 2026
fb2c1c9
Update version to 0.1.1 in pyproject.toml and uv.lock
aurangzaib048 Feb 26, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
groups:
github-actions:
patterns:
- "*"
- package-ecosystem: "uv"
directory: "/"
schedule:
interval: "weekly"
groups:
uv:
patterns:
- "*"
21 changes: 21 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
name: CI

on:
push:
branches: [master]
pull_request:

jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.11", "3.12", "3.13"]
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: astral-sh/setup-uv@eac588ad8def6316056a12d4907a9d4d84ff7a3b # v7.3.0
- run: uv python install ${{ matrix.python-version }}
- run: uv sync
- run: uv run ruff check .
- run: uv run ruff format --check .
- run: uv run pytest --cov=libtea --cov-report=term-missing
30 changes: 30 additions & 0 deletions .github/workflows/codeql.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
name: CodeQL

on:
push:
branches: [master]
pull_request:
schedule:
- cron: "0 6 * * 1"

jobs:
analyze:
runs-on: ubuntu-latest
permissions:
security-events: write
strategy:
matrix:
language: [python]
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

- name: Initialize CodeQL
uses: github/codeql-action/init@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4.32.4
with:
languages: ${{ matrix.language }}

- name: Autobuild
uses: github/codeql-action/autobuild@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4.32.4

- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4.32.4
12 changes: 6 additions & 6 deletions .github/workflows/pypi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@ jobs:
permissions:
id-token: write
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

- uses: astral-sh/setup-uv@v5
- uses: astral-sh/setup-uv@eac588ad8def6316056a12d4907a9d4d84ff7a3b # v7.3.0

- name: Determine version
id: version
Expand All @@ -37,7 +37,7 @@ jobs:
run: uv build

- name: Publish to TestPyPI
uses: pypa/gh-action-pypi-publish@release/v1
uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0
with:
repository-url: https://test.pypi.org/legacy/

Expand All @@ -50,12 +50,12 @@ jobs:
permissions:
id-token: write
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

- uses: astral-sh/setup-uv@v5
- uses: astral-sh/setup-uv@eac588ad8def6316056a12d4907a9d4d84ff7a3b # v7.3.0

- name: Build package
run: uv build

- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0
233 changes: 219 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,34 +1,239 @@
# libtea

Python client library for the [Transparency Exchange API (TEA)](https://transparency.exchange/).
[![CI](https://github.com/sbomify/py-libtea/actions/workflows/ci.yaml/badge.svg)](https://github.com/sbomify/py-libtea/actions/workflows/ci.yaml)
[![PyPI version](https://img.shields.io/pypi/v/libtea.svg)](https://pypi.org/project/libtea/)
[![Python](https://img.shields.io/pypi/pyversions/libtea.svg)](https://pypi.org/project/libtea/)
[![License](https://img.shields.io/github/license/sbomify/py-libtea.svg)](https://github.com/sbomify/py-libtea/blob/master/LICENSE)

Python client library for the [Transparency Exchange API (TEA)](https://transparency.exchange/) v0.3.0-beta.2.

TEA is an open standard for discovering and retrieving software transparency artifacts (SBOMs, VEX, build metadata) for any software product or component. A [TEI identifier](https://github.com/CycloneDX/transparency-exchange-api/blob/main/discovery/readme.md) resolves via DNS to the right endpoint, similar to how email uses MX records — so consumers can fetch artifacts without knowing which server hosts them.

**Specification:** [Ecma TC54-TG1](https://tc54.org/tea/) | [OpenAPI spec](https://github.com/CycloneDX/transparency-exchange-api)

> **Status**: Alpha — API is subject to change.

### Features

- Auto-discovery via `.well-known/tea` and TEI URNs
- Products, components, releases, and versioned collections
- Search by PURL, CPE, or TEI identifier
- Artifact download with on-the-fly checksum verification (MD5 through BLAKE2b)
- Typed Pydantic v2 models with full camelCase/snake_case conversion
- Structured exception hierarchy with error context
- Bearer token isolation — tokens are never sent to artifact download hosts

## Installation

```bash
pip install libtea
```

## Development
## Quick start

This project uses [uv](https://docs.astral.sh/uv/) for dependency management.
```python
from libtea import TeaClient

```bash
# Install dependencies
uv sync
# Auto-discover from a domain's .well-known/tea
with TeaClient.from_well_known("example.com", token="your-bearer-token") as client:
# Browse a product
product = client.get_product("product-uuid")
print(product.name)

# Get a component release with its latest collection
cr = client.get_component_release("release-uuid")
for artifact in cr.latest_collection.artifacts:
print(artifact.name, artifact.type)
```

Or connect directly to a known endpoint:

```python
client = TeaClient(
base_url="https://api.example.com/tea/v0.3.0-beta.2",
token="your-bearer-token",
timeout=30.0,
)
```

Using `from_well_known`, you can also override the spec version and timeout:

```python
client = TeaClient.from_well_known(
"example.com",
token="your-bearer-token",
timeout=15.0,
version="0.3.0-beta.2", # default
)
```

## Usage

### Search

```python
with TeaClient.from_well_known("example.com") as client:
# Search by PURL
results = client.search_products("PURL", "pkg:pypi/requests")
for product in results.results:
print(product.name, product.uuid)

# Search product releases (with pagination)
releases = client.search_product_releases(
"PURL", "pkg:pypi/requests@2.31.0",
page_offset=0, page_size=100,
)
print(releases.total_results)
```

### Products and releases

```python
with TeaClient.from_well_known("example.com") as client:
product = client.get_product("product-uuid")
print(product.name, product.identifiers)

releases = client.get_product_releases("product-uuid", page_size=25)
for release in releases.results:
print(release.version, release.created_date)

# Single product release
pr = client.get_product_release("release-uuid")
print(pr.version, pr.components)

# Product release collections
latest = client.get_product_release_collection_latest("release-uuid")
all_versions = client.get_product_release_collections("release-uuid")
specific = client.get_product_release_collection("release-uuid", 3)
```

### Components

```python
with TeaClient(base_url="https://api.example.com/tea/v0.3.0-beta.2") as client:
component = client.get_component("component-uuid")
releases = client.get_component_releases("component-uuid")

# Get a component release with its latest collection
cr = client.get_component_release("release-uuid")
print(cr.release.version, len(cr.latest_collection.artifacts))
```

### Collections and artifacts

# Run tests
uv run pytest
```python
with TeaClient(base_url="https://api.example.com/tea/v0.3.0-beta.2") as client:
collection = client.get_component_release_collection_latest("release-uuid")
for artifact in collection.artifacts:
print(artifact.name, artifact.type)

# Lint
uv run ruff check .
# All collection versions for a component release
all_versions = client.get_component_release_collections("release-uuid")

# Format check
uv run ruff format --check .
# Specific collection version
collection_v3 = client.get_component_release_collection("release-uuid", 3)
```

### Downloading artifacts with checksum verification

```python
from pathlib import Path

with TeaClient(base_url="https://api.example.com/tea/v0.3.0-beta.2") as client:
artifact = client.get_artifact("artifact-uuid")
fmt = artifact.formats[0]

# Downloads and verifies checksums on-the-fly; returns the dest path
path = client.download_artifact(
fmt.url,
Path("sbom.json"),
verify_checksums=fmt.checksums,
)
```

Supported checksum algorithms: MD5, SHA-1, SHA-256, SHA-384, SHA-512, SHA3-256, SHA3-384, SHA3-512, BLAKE2b-256, BLAKE2b-384, BLAKE2b-512. BLAKE3 is recognized in the model but not verifiable (Python's `hashlib` has no BLAKE3 support — a clear error is raised).

Artifact downloads use a separate unauthenticated HTTP session so the bearer token is never leaked to third-party hosts (CDNs, Maven Central, etc.). On checksum mismatch, the downloaded file is automatically deleted.

### Discovery

```python
from libtea.discovery import parse_tei, fetch_well_known, select_endpoint

# Parse a TEI URN
tei_type, domain, identifier = parse_tei(
"urn:tei:purl:cyclonedx.org:pkg:pypi/cyclonedx-python-lib@8.4.0"
)

# Low-level: fetch and select an endpoint manually
well_known = fetch_well_known("example.com")
endpoint = select_endpoint(well_known, "0.3.0-beta.2")
print(endpoint.url, endpoint.priority)

# Discover product releases by TEI
with TeaClient(base_url="https://api.example.com/tea/v0.3.0-beta.2") as client:
results = client.discover("urn:tei:uuid:example.com:d4d9f54a-abcf-11ee-ac79-1a52914d44b")
for info in results:
print(info.product_release_uuid, info.servers)
```

Supported TEI types: `uuid`, `purl`, `hash`, `swid`, `eanupc`, `gtin`, `asin`, `udi`.

## Error handling

# Build
uv build
All exceptions inherit from `TeaError`:

```python
from libtea.exceptions import TeaError, TeaNotFoundError, TeaChecksumError

try:
product = client.get_product("unknown-uuid")
except TeaNotFoundError as exc:
print(exc.error_type) # "OBJECT_UNKNOWN" or "OBJECT_NOT_SHAREABLE"
except TeaError:
print("Something went wrong")
```

Exception hierarchy:

| Exception | When |
|-----------|------|
| `TeaConnectionError` | Network failure or timeout |
| `TeaAuthenticationError` | HTTP 401/403 |
| `TeaNotFoundError` | HTTP 404 (`.error_type` has the TEA error code) |
| `TeaRequestError` | Other HTTP 4xx |
| `TeaServerError` | HTTP 5xx |
| `TeaDiscoveryError` | Invalid TEI, `.well-known` failure, or no compatible endpoint |
| `TeaChecksumError` | Checksum mismatch (`.algorithm`, `.expected`, `.actual`) |
| `TeaValidationError` | Malformed server response |
| `TeaInsecureTransportWarning` | Warning emitted when using plaintext HTTP |

Using a bearer token over plaintext HTTP raises `ValueError` immediately — HTTPS is required for authenticated requests.

## Requirements

- Python >= 3.11
- [requests](https://requests.readthedocs.io/) >= 2.31.0 for HTTP
- [Pydantic](https://docs.pydantic.dev/) >= 2.1.0 for data models

## Not yet supported

- Publisher API (spec is consumer-only in beta.2)
- Async client
- CLE (Common Lifecycle Enumeration) endpoints
- Mutual TLS (mTLS) authentication
- Endpoint failover with retry

## Development

This project uses [uv](https://docs.astral.sh/uv/) for dependency management.

```bash
uv sync # Install dependencies
uv run pytest # Run tests (with coverage)
uv run ruff check . # Lint
uv run ruff format --check . # Format check
uv build # Build wheel and sdist
```

## License
Expand Down
Loading