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"]