Skip to main content

bb-sapi-python-sdk

Python SDK for the Blue Billywig Streaming API (SAPI).

Features

  • HOTP authentication (HMAC-SHA1, 120-second window) — same scheme as the PHP SDK and MCP server
  • Synchronous HTTP client built on requests
  • Full analytics API: views, range, inits, faceted breakdowns
  • Convenience helpers: top videos, unique viewers, viewcount reach (≥X%), per-video ad stats
  • LineItem version history for resolving which creative ran during a report period
  • Generic entity CRUD: get, list, search, create, update, delete, action
  • Version history for any entity via client.versions(entity, id)
  • TUS file upload: upload_file() and create_mediaclip() with S3 multipart support
  • Clean exception hierarchy

Installation

pip install bb-sapi-python-sdk

Or from source:

git clone https://github.com/bluebillywig/bb-sapi-python-sdk.git
cd bb-sapi-python-sdk
pip install -e ".[dev]"

Quick start

from bb_sapi import SapiClient

client = SapiClient(
    base_url="https://mypublication.bbvms.com",
    shared_secret="490-55c491d354cfefb9b4d26cf22fbdd0a1",
)

# Top 10 videos this month
top = client.analytics.top_videos("2026-01-01", "2026-03-31", limit=10)
for v in top:
    print(v["id"], v["views"])

# Unique viewers for a specific video
unique = client.analytics.unique_viewers("mediaclip", "2026-01-01", "2026-03-31",
                                         entity_id="12345")
print(f"Unique viewers: {unique}")

# Viewcount reach thresholds
reach = client.analytics.viewcount_reach("12345", "2026-01-01", "2026-03-31")
# {20: 1200, 40: 900, 60: 700, 80: 400, 95: 150}

# Per-video ad impressions + VAST quartiles
ad = client.analytics.ad_stats_per_video("12345", "2026-01-01", "2026-03-31")
print(ad["impressions"])          # sum of lineitemInits counts
print(ad["vastQuartiles"]["50"])  # sessions where VAST 50% quartile fired

# Unique ad reach for a specific lineitem
reach = client.analytics.unique_ad_reach("12345", "starcasino_preroll",
                                          "2026-01-01", "2026-03-31")
print(f"Unique viewers who saw the ad: {reach}")

Authentication

The SAPI uses HOTP (HMAC-SHA1) with a 120-second time window.

Obtain your shared secret from your Blue Billywig account: Account Settings → API Keys → Show Secret

Format: {id}-{hex_secret} — e.g. 490-55c491d354cfefb9b4d26cf22fbdd0a1

client = SapiClient(
    base_url="https://mypublication.bbvms.com",
    shared_secret="490-55c491d354cfefb9b4d26cf22fbdd0a1",
)

Never commit your shared secret. Use environment variables:

export SAPI_BASE_URL=https://mypublication.bbvms.com
export SAPI_SHARED_SECRET=490-55c491d354cfefb9b4d26cf22fbdd0a1

Analytics

Views with faceted breakdown

body = client.analytics.views(
    "mediaclip",
    from_date="2026-01-01",
    to_date="2026-03-31",
    facets=["eid", "uid", "country"],
    facetconfig={
        "eid": {"limit": 50},
        "uid": {"limit": 0, "metric": "unique"},
    },
)
# body["total"]          → total view sessions
# body["facets"]["eid"]  → [{value, count}, ...]
# body["facets"]["unique_uid"] → unique viewer count

Time-series (range)

items = client.analytics.range(
    "mediaclip",
    from_date="2026-01-01",
    to_date="2026-03-31",
    granularity="day",
)
# items["items"] → [{datetime, total}, ...]

Analytics facets reference

Facet What it returns
eid Views per video ID
title Views per video title
uid (+ metric: "unique") Unique viewer count
avgViewTime Average view time in seconds
completed Completed views (boolean breakdown)
exactPercentageViewed Distribution of highest playback position (0–100%)
lineitemInits Sessions per BB-managed lineitem (ad impressions per video)
vastQuartiles VAST IAB quartile completions per video (25/50/75/100%)
country, region, city Geographic breakdown
domain, referrer Traffic source breakdown
deviceType, mobileBrand Device breakdown
osName, browserName Platform breakdown

Ad analytics notes

Per-video impressions — use lineitemInits from the analytics API. The bb-backend ad-stats API (/v1/ad-stats) silently ignores MediaClip entity filters: unknown types are dropped by the DTO, the filter builder returns empty combinations, and the service falls back to publication totals. This is an architectural limitation (Redis keys are scoped to AdUnit/AdSchedule/LineItem only).

lineitemInits is always tracked regardless of where the creative is hosted.

Creative identity (which video played as the ad) is only resolvable when the creative is BB-hosted (VAST/VPAID/SIMID served from the OVP).

exactPercentageViewed = highestTo / clipDuration × 100 — the highest playback position reached in the session, not the exit point. A viewer who skips to 80% counts as having reached 80%.

Entity operations

# Fetch a single entity
clip = client.get("mediaclip", "12345")

# List with filters
clips = client.list("mediaclip", limit=20, sort="createddate DESC",
                     filters={"status": "published"})

# Search (Solr query syntax)
results = client.search("title:football", entity_type="MediaClip", limit=10)

# Create
new_clip = client.create("mediaclip", {"title": "New Video"})

# Update
client.update("mediaclip", "12345", {"title": "Updated Title"})

# Delete (soft)
client.delete("mediaclip", "12345")

# Delete (permanent)
client.delete("mediaclip", "12345", purge=True)

# Entity action
client.action("mediaclip", "12345", "publish", method="PUT")

# Version history (works for any entity: mediaclip, lineitem, playout, player, ...)
versions = client.versions("mediaclip", "12345")
# [{"id": "...", "date": "2026-01-15", "isLatest": False}, ...]

File uploads (TUS)

The SAPI uses the TUS protocol backed by S3 multipart upload.

Upload a file without creating a mediaclip entity

Use this for creatives, thumbnails, subtitle files, and images — anything where the entity already exists or is managed separately.

result = client.upload_file(
    "/tmp/ad_creative.mp4",
    use_type="commercial",       # "commercial" (ad) or "editorial" (content)
    mediaclip_id="12345",        # optional: attach to existing mediaclip
)
print(result.tus_upload_id)      # TUS upload ID
print(result.s3_key)             # S3 object key

Create a mediaclip with a video file

Full OVP6 workflow: creates the mediaclip entity first, then uploads the file.

result = client.create_mediaclip(
    "/tmp/match_recap.mp4",
    title="Match Recap",
    description="Highlights from the match",
    tags=["football", "highlights"],
    status="draft",              # "draft" or "published"
    on_progress=lambda done, total: print(f"{done / total * 100:.0f}%"),
)
print(result.mediaclip_id)       # e.g. "12345"

UploadResult

Both methods return an UploadResult:

result.tus_upload_id    # SAPI TUS upload ID
result.upload_identifier
result.mediaclip_id     # set only by create_mediaclip()
result.file_name
result.file_size        # bytes
result.content_type
result.s3_key

How it works

POST /sapi/tus                    ← create upload, get presigned S3 URLs
  Upload-Metadata: filename <b64>, filetype <b64> [, mediaclipId <b64>]

PUT  <presigned_url>  (×N parts)  ← upload chunks directly to S3
  ← collect ETag from each response

POST /sapi/tus/{id}/complete      ← finalise multipart upload
  [{PartNumber, ETag}, ...]

LineItem version history

from bb_sapi.entities.lineitem import LineItem

li = LineItem(client)

# Which creative(s) ran during the report period?
creatives = li.creatives_for_period(
    "starcasino_preroll",
    from_date="2026-01-01",
    to_date="2026-03-31",
)
# [{"version_id": "...", "date": "2026-01-15",
#   "creative_id": "7019583", "vast_url": "https://..."}]

When the creative changed mid-period, multiple entries are returned — one per distinct creative that was active.

Examples

See examples/analytics_export.py for a complete Excel analytics export with per-video ad metrics, VAST quartile data, viewcount reach, and pre-roll creative breakdown.

SAPI_BASE_URL=https://mypub.bbvms.com \
SAPI_SHARED_SECRET=490-... \
SAPI_FROM_DATE=2026-01-01 \
SAPI_TO_DATE=2026-03-31 \
python examples/analytics_export.py

Development

git clone https://github.com/bluebillywig/bb-sapi-python-sdk.git
cd bb-sapi-python-sdk
pip install -e ".[dev]"
pytest

Publishing to PyPI

One-time setup

  1. Create a PyPI account and enable 2FA.

  2. Create an API token at PyPI → Account settings → API tokens (scope: entire account for first upload, then restrict to this project).

  3. Install build tools:

    pip install build twine
    
  4. Store credentials in ~/.pypirc (or use TWINE_USERNAME/TWINE_PASSWORD env vars):

    [pypi]
    username = __token__
    password = pypi-AgEIcHlwaS5vcmc...
    

Release workflow

  1. Bump the version in pyproject.toml and bb_sapi/__init__.py:

    # pyproject.toml
    version = "0.2.0"
    
    # bb_sapi/__init__.py
    __version__ = "0.2.0"
    
  2. Commit and tag:

    git add pyproject.toml bb_sapi/__init__.py
    git commit -m "Release v0.2.0"
    git tag v0.2.0
    git push && git push --tags
    
  3. Build source distribution and wheel:

    python -m build
    # produces dist/bb_sapi_python_sdk-0.2.0.tar.gz
    #          dist/bb_sapi_python_sdk-0.2.0-py3-none-any.whl
    
  4. Upload to PyPI:

    twine upload dist/*
    
  5. Verify: pip install bb-sapi-python-sdk==0.2.0

Test on TestPyPI first (optional)

twine upload --repository testpypi dist/*
pip install --index-url https://test.pypi.org/simple/ bb-sapi-python-sdk

License

MIT

Metadata

Release files for bb-sapi-python-sdk 1.0.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for bb-sapi-python-sdk 1.0.0
File Size Uploaded
bb_sapi_python_sdk-1.0.0.tar.gz 23.7 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for bb-sapi-python-sdk 1.0.0
File Interpreter ABI Platform
bb_sapi_python_sdk-1.0.0-py3-none-any.whl Python 3 none any Details

Total release size: 45.6 kB

Release files / bb_sapi_python_sdk-1.0.0.tar.gz

Download URL bb_sapi_python_sdk-1.0.0.tar.gz
Size 23.7 kB
Tags Source
SHA-256 checksum
How to use checksums
02583c5194ec42ecc6c652ce3f3dff0fbdb6bc0fea045c001abc5d6609d2e783
BLAKE2b-256 checksum
How to use checksums
854c7b3e6cd9fbfb5fb2d4bc4971fd642e33b496a267b6db264e482027566633
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.12.10

Release files / bb_sapi_python_sdk-1.0.0-py3-none-any.whl

Download URL bb_sapi_python_sdk-1.0.0-py3-none-any.whl
Size 21.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
bcc82e8e0122ce6f4499a49c4dfcde6466995727a3fd4baae4244615bb4a9280
BLAKE2b-256 checksum
How to use checksums
9a0e22aa5f5ced2951bc0ee83ccda0d7bac575f255db227c5fa9b6c285a0e97d
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.12.10

Release history Release notifications | RSS feed

This release

1.0.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page