diff --git a/.github/workflows/publish-to-pypi.yml b/.github/workflows/publish-to-pypi.yml
new file mode 100644
index 0000000..a032f26
--- /dev/null
+++ b/.github/workflows/publish-to-pypi.yml
@@ -0,0 +1,95 @@
+name: Publish Python release
+
+on:
+ push:
+ tags:
+ - "v*"
+ workflow_dispatch:
+
+concurrency:
+ group: publish-${{ github.ref }}
+ cancel-in-progress: false
+
+permissions:
+ contents: read
+
+jobs:
+ build:
+ name: Build release
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
+ with:
+ persist-credentials: false
+
+ - name: Set up Python
+ uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
+ with:
+ python-version: "3.12"
+
+ - name: Verify tag matches package version
+ if: github.event_name == 'push'
+ shell: bash
+ run: |
+ package_version=$(python -c 'import tomllib; print(tomllib.load(open("pyproject.toml", "rb"))["project"]["version"])')
+ test "v${package_version}" = "${GITHUB_REF_NAME}"
+
+ - name: Install build frontend
+ run: python -m pip install --disable-pip-version-check build
+
+ - name: Build wheel and source release
+ run: python -m build
+
+ - name: Store release packages
+ uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5
+ with:
+ name: python-release-packages
+ path: dist/
+ if-no-files-found: error
+ retention-days: 1
+
+ publish-to-pypi:
+ name: Publish release to PyPI
+ if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
+ needs: build
+ runs-on: ubuntu-latest
+ environment:
+ name: pypi
+ url: https://pypi.org/p/livepeer-gateway
+ permissions:
+ id-token: write
+
+ steps:
+ - name: Download release packages
+ uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6
+ with:
+ name: python-release-packages
+ path: dist/
+
+ - name: Publish release to PyPI
+ uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # release/v1
+
+ publish-to-testpypi:
+ name: Publish release to TestPyPI
+ if: github.event_name == 'workflow_dispatch'
+ needs: build
+ runs-on: ubuntu-latest
+ environment:
+ name: testpypi
+ url: https://test.pypi.org/p/livepeer-gateway
+ permissions:
+ id-token: write
+
+ steps:
+ - name: Download release packages
+ uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6
+ with:
+ name: python-release-packages
+ path: dist/
+
+ - name: Publish release to TestPyPI
+ uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # release/v1
+ with:
+ repository-url: https://test.pypi.org/legacy/
diff --git a/.gitignore b/.gitignore
index d496538..3f4e68a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,6 +6,10 @@
# Python cache
__pycache__/
*.pyc
+.coverage
+.pytest_cache/
+dist/
+htmlcov/
# Generated files
lp_rpc.proto
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..67074c8
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,34 @@
+# Changelog
+
+All notable changes to this project will be documented in this file. The
+format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
+and this project adheres to [Semantic Versioning](https://semver.org/).
+
+## [1.0.0] - 2026-08-11
+
+The first stable release of the Livepeer Python SDK.
+
+### Added
+
+- Live Runner registration, discovery, session reservation, raw calls, proxy
+ calls, and session lifecycle events.
+- Scope startup for application and serverless runners.
+- BYOC inference and training jobs, including signed payments, payment refresh,
+ status polling, and completion waits.
+- Live video-to-video jobs with capability-aware orchestrator discovery,
+ ordered fallback selection, token-based configuration, and remote-signer
+ payments.
+- Multi-track media publishing and media output APIs for bytes, decoded frames,
+ and demuxed packets.
+- Trickle channels for control messages, events, JSON Lines, keepalives, and
+ observable publisher/subscriber statistics.
+- Orchestrator information, capability discovery, TLS trust-on-first-use, and
+ typed SDK errors.
+
+### Changed
+
+- Declared the generated gRPC client's actual minimum runtime versions:
+ `grpcio>=1.76.0` and `protobuf>=6.31.1`.
+- Completed the package metadata and documented installation from PyPI.
+
+[1.0.0]: https://github.com/livepeer/livepeer-python-gateway/releases/tag/v1.0.0
diff --git a/PUBLISHING.md b/PUBLISHING.md
new file mode 100644
index 0000000..ce9bb37
--- /dev/null
+++ b/PUBLISHING.md
@@ -0,0 +1,62 @@
+# Publishing releases
+
+Releases are built and published by
+[`publish-to-pypi.yml`](.github/workflows/publish-to-pypi.yml). The workflow
+uses PyPI Trusted Publishing, so no PyPI API token is stored in GitHub.
+
+## Production release
+
+1. Update the version in `pyproject.toml` and `uv.lock`.
+2. Add the dated release notes to `CHANGELOG.md`.
+3. Merge the release commit into `main` and ensure the test workflow passes.
+4. Create and push an annotated tag matching the package version:
+
+ ```bash
+ git switch main
+ git pull --ff-only github main
+ git tag -a v1.0.0 -m "Livepeer Python SDK 1.0.0"
+ git push github v1.0.0
+ ```
+
+5. Approve the deployment in the protected `pypi` GitHub Environment.
+6. Verify the release at .
+
+The workflow rejects tags that do not exactly match `v` followed by the
+version in `pyproject.toml`. The publishing action generates and uploads PyPI
+digital attestations for both release files.
+
+## TestPyPI
+
+Run the `Publish Python release` workflow manually from the Actions tab.
+Manual runs build and upload only to TestPyPI. Each package version can be
+uploaded only once, so bump to a new pre-release version before repeating a
+TestPyPI upload.
+
+
+Initial repository setup (maintainers only)
+
+Create these GitHub Environments in the repository settings:
+
+- `pypi`: add a required reviewer and restrict deployment to version tags.
+- `testpypi`: manual approval is optional because this target is only available
+ through a manually dispatched workflow.
+
+Register a pending GitHub Trusted Publisher at
+ with:
+
+| Field | Value |
+|---|---|
+| PyPI project name | `livepeer-gateway` |
+| Owner | `livepeer` |
+| Repository | `livepeer-python-gateway` |
+| Workflow | `publish-to-pypi.yml` |
+| Environment | `pypi` |
+
+Optionally register the same pending publisher at
+, using the `testpypi`
+environment. PyPI and TestPyPI require separate accounts and publisher setup.
+
+The workflow file must be present on the repository's default branch before a
+Trusted Publisher can use it.
+
+
diff --git a/README b/README
index 58ee734..af5018d 100644
--- a/README
+++ b/README
@@ -1,6 +1,39 @@
-Generate protobufs:
+# Livepeer Python SDK
+The official Python SDK for the Livepeer network. Submit AI and video compute
+jobs directly to orchestrators, use a remote signer for payment, and stream
+media and control data over Livepeer's trickle protocol.
+
+## Requirements
+
+- Python 3.12 or newer
+
+## Installation
+
+Install the stable release from PyPI:
+
+```bash
+python -m pip install livepeer-gateway
```
+
+The import package is named `livepeer_gateway`:
+
+```python
+from livepeer_gateway import StartJobRequest, start_lv2v
+```
+
+## Development
+
+Install the locked development dependencies with
+[uv](https://docs.astral.sh/uv/):
+
+```bash
+uv sync --locked --group test
+```
+
+Generate protobufs after installing the code-generation extra:
+
+```bash
uv sync --extra dev
uv run generate-lp-rpc
```
@@ -9,39 +42,39 @@ uv run generate-lp-rpc
Install the locked test dependencies and run the complete pytest suite:
-```
+```bash
uv sync --locked --group test
uv run --group test pytest
```
Pass a test file or node ID to pytest for a focused run:
-```
+```bash
uv run --group test pytest tests/test_live_runner.py
uv run --group test pytest tests/test_live_runner.py::TestLiveRunnerHelpers::test_parse_go_duration
```
Run the suite with the configured line and branch coverage:
-```
+```bash
uv run --group test pytest --cov=livepeer_gateway --cov-branch --cov-report=term-missing
```
## Usage Examples
First install dependencies for example code
-```
+```bash
uv sync --extra examples
```
Get orchestrator info, offchain mode
-```
+```bash
uv run examples/get_orchestrator_info.py localhost:8935
```
On-chain mode with a remote signer
-```
+```bash
uv run examples/get_orchestrator_info.py --signer ""
# Use a custom discovery endpoint to filter orchestrators
@@ -50,22 +83,22 @@ uv run examples/get_orchestrator_info.py --signer "" '"
```
Write raw frames to a LiveVideoToVideo job
-```
+```bash
uv run examples/write_frames.py localhost:8935
```
Capture MacOS camera frames and publish via write_frame
-```
+```bash
uv run examples/camera_capture.py localhost:8935
```
Capture MacOS camera frames and subscribe to media output (stdout or file)
-```
+```bash
uv run examples/camera_capture.py localhost:8935 --output - | ffplay -fflags nobuffer -flags low_delay -probesize 32 -i -
uv run examples/camera_capture.py localhost:8935 --output out.ts
```
@@ -78,13 +111,13 @@ async with job.media_output() as output:
```
Composite camera input and decoded output side-by-side with PTS delta
-```
-uv sync --group examples
+```bash
+uv sync --extra examples
uv run examples/in_out_composite.py localhost:8935
```
Subscribe to a LiveVideoToVideo trickle events channel
-```
+```bash
uv run examples/subscribe_events.py localhost:8935
```
diff --git a/pyproject.toml b/pyproject.toml
index 2311b42..cd7813c 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,21 +4,44 @@ build-backend = "hatchling.build"
[project]
name = "livepeer-gateway"
-version = "0.1.0"
+version = "1.0.0"
+description = "Official Python SDK for submitting AI and video jobs to the Livepeer network"
+readme = { file = "README", content-type = "text/markdown" }
+license = { file = "LICENSE" }
+authors = [
+ { name = "Livepeer Foundation" },
+]
requires-python = ">=3.12"
+keywords = ["livepeer", "video", "ai", "sdk"]
+classifiers = [
+ "Development Status :: 5 - Production/Stable",
+ "Framework :: AsyncIO",
+ "License :: OSI Approved :: MIT License",
+ "Operating System :: OS Independent",
+ "Programming Language :: Python :: 3",
+ "Programming Language :: Python :: 3.12",
+ "Topic :: Multimedia :: Video",
+ "Topic :: Software Development :: Libraries :: Python Modules",
+]
dependencies = [
- "grpcio>=1.65.0",
- "protobuf>=4.25.0",
+ "grpcio>=1.76.0",
+ "protobuf>=6.31.1",
"aiohttp>=3.9.0",
"av>=11.0.0",
]
+[project.urls]
+Homepage = "https://livepeer.org"
+Repository = "https://github.com/livepeer/livepeer-python-gateway"
+Issues = "https://github.com/livepeer/livepeer-python-gateway/issues"
+Changelog = "https://github.com/livepeer/livepeer-python-gateway/blob/main/CHANGELOG.md"
+
[project.scripts]
generate-lp-rpc = "livepeer_gateway.codegen:main"
[project.optional-dependencies]
dev = [
- "grpcio-tools>=1.65.0",
+ "grpcio-tools>=1.76.0",
]
examples = [
@@ -38,7 +61,19 @@ test = [
]
[tool.hatch.build.targets.wheel]
-packages = ["src/livepeer_gateway", "src/net"]
+packages = ["src/livepeer_gateway"]
+
+[tool.hatch.build.targets.sdist]
+include = [
+ "/CHANGELOG.md",
+ "/LICENSE",
+ "/README",
+ "/examples",
+ "/pyproject.toml",
+ "/src",
+ "/tests",
+ "/uv.lock",
+]
[tool.pytest.ini_options]
minversion = "8.3"
diff --git a/src/livepeer_gateway/byoc.py b/src/livepeer_gateway/byoc.py
index 4d27781..272d668 100644
--- a/src/livepeer_gateway/byoc.py
+++ b/src/livepeer_gateway/byoc.py
@@ -41,7 +41,7 @@
from urllib.parse import urlparse
from urllib.request import Request, urlopen
-from .orchestrator import _http_origin, _parse_http_url, discover_orchestrators
+from .orchestrator import _http_origin, discover_orchestrators
from .errors import LivepeerGatewayError, NoOrchestratorAvailableError, OrchestratorRejection
_LOG = logging.getLogger(__name__)
@@ -163,8 +163,6 @@ def _create_byoc_payment(
Returns dict with Livepeer-Payment and Livepeer-Segment headers.
"""
- from .remote_signer import get_orch_info_sig, _freeze_headers, PaymentSession
- from .orchestrator import _http_origin
from .orch_info import get_orch_info
# Step 1: Get OrchestratorInfo via gRPC (port 8935)
diff --git a/uv.lock b/uv.lock
index 2238021..d941737 100644
--- a/uv.lock
+++ b/uv.lock
@@ -432,7 +432,7 @@ wheels = [
[[package]]
name = "livepeer-gateway"
-version = "0.1.0"
+version = "1.0.0"
source = { editable = "." }
dependencies = [
{ name = "aiohttp" },
@@ -464,11 +464,11 @@ test = [
requires-dist = [
{ name = "aiohttp", specifier = ">=3.9.0" },
{ name = "av", specifier = ">=11.0.0" },
- { name = "grpcio", specifier = ">=1.65.0" },
- { name = "grpcio-tools", marker = "extra == 'dev'", specifier = ">=1.65.0" },
+ { name = "grpcio", specifier = ">=1.76.0" },
+ { name = "grpcio-tools", marker = "extra == 'dev'", specifier = ">=1.76.0" },
{ name = "numpy", marker = "extra == 'examples'", specifier = ">=2.2.6" },
{ name = "opencv-python-headless", marker = "extra == 'examples'", specifier = ">=4.13.0.90" },
- { name = "protobuf", specifier = ">=4.25.0" },
+ { name = "protobuf", specifier = ">=6.31.1" },
]
provides-extras = ["dev", "examples"]