Metadata-Version: 2.4
Name: opedd
Version: 0.2.1
Summary: Official Python SDK for the Opedd content licensing API (buyer-side)
Project-URL: Homepage, https://opedd.com
Project-URL: Documentation, https://docs.opedd.com
Project-URL: Repository, https://github.com/Opedd/opedd-python
Project-URL: Issues, https://github.com/Opedd/opedd-python/issues
Project-URL: Changelog, https://github.com/Opedd/opedd-python/blob/main/CHANGELOG.md
Author-email: Opedd <support@opedd.com>
License: MIT
License-File: LICENSE
Keywords: ai-licensing,ai-training,content-licensing,opedd,rag
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.10
Requires-Dist: httpx>=0.27.0
Provides-Extra: dev
Requires-Dist: mypy>=1.8.0; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.23.0; extra == 'dev'
Requires-Dist: pytest-httpx>=0.30.0; extra == 'dev'
Requires-Dist: pytest>=8.0.0; extra == 'dev'
Requires-Dist: ruff>=0.4.0; extra == 'dev'
Description-Content-Type: text/markdown

# opedd

[![PyPI](https://img.shields.io/pypi/v/opedd.svg)](https://pypi.org/project/opedd/)
[![Python](https://img.shields.io/pypi/pyversions/opedd.svg)](https://pypi.org/project/opedd/)
[![License](https://img.shields.io/pypi/l/opedd.svg)](https://github.com/Opedd/opedd-python/blob/main/LICENSE)

Official Python SDK for the [Opedd](https://opedd.com) content licensing API (buyer-side).

Opedd is programmatic licensing infrastructure between AI buyers and publishers — rights, usage tracking, and payment. "Stripe for content licensing."

## Install

```bash
pip install opedd
```

Requires Python 3.10+.

## Quickstart

```python
from opedd import Opedd

client = Opedd(buyer_token="opedd_buyer_live_...")

# Fetch a single licensed article
article = client.content.get("article-uuid")
print(article["title"], article["author"], article["word_count"])

# Stream the full licensed catalog as NDJSON
client = Opedd(access_key="ent_xxx", buyer_email="eng@yourlab.com")
for row in client.feed.stream_ndjson(limit=5000):
    train_model.ingest(row["content"], metadata=row)

# Pull a procurement-defense compliance dossier
client = Opedd(buyer_jwt="eyJhbGc...")
dossier = client.compliance.report(from_="2026-04-01", to="2026-04-30")
print(f"Retrievals: {dossier['dossier_metadata']['summary']['total_retrievals']}")
```

### Phase 12 Wave 1 + 3 surfaces (0.2.0)

```python
# Catalog scoping — discover what a publisher licenses (public, no auth needed for the call)
manifest = client.rsl.get(publisher_id="8268c353-ffa3-4db3-bbb2-90ddbbb43e41")
for license_type, terms in manifest["permits"].items():
    if terms["permitted"]:
        print(f"{license_type}: {terms['endpoint']}")

# CDSM Article 4(3) provenance — signed JSON-LD receipt for EU regulator verification
signed = client.rsl.get(publisher_id="8268c353-...", jsonld=True)
assert signed["opedd:legalBasis"].startswith("EU CDSM")
print("HMAC signature:", signed["opedd:signature"]["signature"])

# EU AI Act Article 53 attestation — buyer-side regulator-facing JWT
client = Opedd(buyer_jwt="eyJhbGc...")
attestation = client.compliance.article_53_attestation(
    license_id="11111111-2222-3333-4444-555555555555",
    window_start="2026-02-22T00:00:00Z",
    window_end="2026-05-22T00:00:00Z",
)
jwt_token = attestation["data"]["jwt"]       # Hand to legal/procurement audit committee
claims = attestation["data"]["claims"]
print(f"Events in window: {claims['usage']['events_in_window']}")
print(f"Tempo Merkle root: {claims['tempo']['merkle_root']}")

# Programmatic publisher onboarding — detect platform behind a URL
detection = client.onboarding.detect_platform(url="https://noahpinion.substack.com")
data = detection["data"]
if data["confidence"] == "high":
    print(f"Platform: {data['platform']}, archive via {data['archive_method']}")
else:
    print(data["instructions"])  # human-readable operator copy

# Buyer-discovery catalog browse + URL → licensable check + license verification
directory = client.discovery.publisher_directory(category="finance", min_articles=5, limit=20)
for pub in directory["data"]["publishers"]:
    print(pub["name"], pub["article_count"], pub["pricing"])

try:
    hit = client.discovery.lookup_article(url="https://publisher.com/articles/...")
    print(f"Licensable: ${hit['data']['ai_price']} per article")
except OpeddNotFoundError:
    print("Not in Opedd registry")

check = client.discovery.verify_license(key="OP-XXXX-XXXX")
print(f"Blockchain status: {check['data']['blockchain_status']}")
```

For end-to-end walkthroughs, see the [cookbook](https://docs.opedd.com/cookbook.md).

## Credentials

The SDK supports three credential types depending on which endpoint you call:

| Endpoint | Credential | Construction |
|---|---|---|
| `client.content.get(...)` | Bearer buyer token | `Opedd(buyer_token="opedd_buyer_live_...")` |
| `client.feed.list(...)` / `client.feed.stream_ndjson(...)` | Access key (query param) | `Opedd(access_key="ent_...")` |
| `client.audit.events(...)` | Supabase JWT | `Opedd(buyer_jwt="eyJhbGc...")` |
| `client.compliance.report(...)` | Supabase JWT | `Opedd(buyer_jwt="eyJhbGc...")` |
| `client.compliance.article_53_attestation(...)` | Supabase JWT | `Opedd(buyer_jwt="eyJhbGc...")` |
| `client.licenses.purchase(...)` | None (returns Stripe `client_secret`) | `Opedd(buyer_token="...")` |
| `client.licenses.list()` | Supabase JWT | `Opedd(buyer_jwt="eyJhbGc...")` |
| `client.rsl.get(...)` | None (public) | Any (constructor still requires one) |
| `client.onboarding.detect_platform(...)` | None (public) | Any (constructor still requires one) |
| `client.discovery.publisher_directory(...)` | None (public) | Any (constructor still requires one) |
| `client.discovery.lookup_article(...)` | None (public) | Any (constructor still requires one) |
| `client.discovery.verify_license(...)` | None (public) | Any (constructor still requires one) |

Multiple credentials can be supplied at once and the SDK selects the correct one per endpoint.

### Env-var fallbacks

The constructor reads from these env vars when arguments are omitted:

- `OPEDD_BUYER_TOKEN`
- `OPEDD_BUYER_JWT`
- `OPEDD_ACCESS_KEY`
- `OPEDD_BASE_URL` (default `https://api.opedd.com`)

### Exchanging an access key for a bearer token

```python
client = Opedd.from_access_key(
    access_key="ent_xyz...",
    buyer_email="eng@yourlab.com",
)
# client.buyer_token is now set; you can call /content-delivery
```

## API surface

```python
client.content.get(article_id)

client.feed.list(since=None, cursor=None, limit=200)
client.feed.stream_ndjson(since=None, cursor=None, limit=5000)  # generator

client.audit.events(from_=None, to=None, event_type=None, cursor=None, limit=100)

client.compliance.report(from_, to, cursor=None)
client.compliance.article_53_attestation(
    license_id, content_id=None, window_start=None, window_end=None,
)                                                # 0.2.0 — Phase 12 Wave 1 W1.4

client.licenses.purchase(publisher_ids, buyer_email, buyer_org, ...)
client.licenses.list()

client.rsl.get(publisher_id, jsonld=False)        # 0.2.0 — Phase 12 Wave 1 W1.1
client.onboarding.detect_platform(url)            # 0.2.0 — Phase 12 Wave 3 W3.1

client.discovery.publisher_directory(             # 0.2.0+ — public catalog browse
    category=None, min_articles=None, verified=None, limit=None, offset=None,
)
client.discovery.lookup_article(url)              # 0.2.0+ — URL → licensable check
client.discovery.verify_license(key)              # 0.2.0+ — license key verification
```

All methods return dicts matching the backend wire format. See [docs.opedd.com](https://docs.opedd.com) for full response shapes.

### Regulatory framing (CDSM Article 4 vs EU AI Act Article 53)

Three distinct compliance surfaces — never conflated per `INVARIANTS.md` W1.6:

- **`client.rsl.get(publisher_id, jsonld=True)`** — publisher-side **CDSM Article 4(3)** opt-out declaration (signed JSON-LD receipt over the reservation state).
- **`client.compliance.article_53_attestation(license_id)`** — buyer-side **EU AI Act Article 53** attestation (signed JWT scoped to one license).
- **`client.compliance.report(from_, to)`** — comprehensive procurement-defense dossier covering BOTH frameworks (publisher CDSM reservation honored + buyer Article 53 evidence chain).

These serve different audit-defensibility modes and never share wire format. Method docstrings cite the W1.6 invariant inline.

### Not in SDK

The SDK is **buyer-side** by design (Stripe-style audience separation — different SDK for publishers if/when that ships). Publisher-side write endpoints are reachable via raw HTTP only, not wrapped here. Canonical excluded surface as of 0.2.0:

- **`PATCH /publisher-profile`** (Phase 12 Wave 1 W1.2 — publisher updates `tdm_reservation_signed_at` + `cdsm_tdm_opt_out` from the publisher dashboard). The endpoint exists on the backend but is not yet documented in `opedd-docs/openapi.json` and uses publisher-portal Supabase JWT auth (a credential mode the buyer-side SDK does not carry). The publisher React dashboard at `opedd-frontend` calls it via raw `fetch()`.

Reconsider expanding the SDK surface to publisher-side write endpoints when **both** of these hold: (i) `opedd-docs/openapi.json` includes the `/publisher-profile` PATCH spec, **and** (ii) a buyer integration ask for `tdm_reservation` state surfaces. Tracked in `opedd-backend/docs/cleanup/active-deferrals.md` for the institutional record.

## Error handling

```python
from opedd import (
    OpeddError,
    OpeddAuthError,
    OpeddNotFoundError,
    OpeddRateLimitError,
    OpeddServerError,
    OpeddValidationError,
)

try:
    article = client.content.get(uuid)
except OpeddRateLimitError as e:
    time.sleep(e.retry_after_seconds or 60)
    retry()
except OpeddAuthError:
    refresh_token()
except OpeddNotFoundError:
    log.warning("article gone; skipping")
except OpeddError as e:
    log.error(f"{e} request_id={e.request_id}")
```

Every error carries `status_code`, `request_id`, and `body` for forensic correlation.

## Schema version pinning

The SDK ships pinned to backend schema version **`phase-11-m4`** (`opedd.__schema_version__`).

When the backend bumps `X-Opedd-Schema-Version`, the SDK ships a follow-up release within 1 sprint per the [schema-pin invariant](https://github.com/Opedd/opedd-backend/blob/main/INVARIANTS.md#python-sdk--mcp-server-pin-to-backend-x-opedd-schema-version-phase-11-m6).

Additive field bumps are absorbed transparently (dict pass-through). Subtractive or rename bumps require an SDK release; ensure your `requirements.txt` pins to a tested version.

## Tests

Two test suites:

### Unit tests (run in CI, no live API calls)

```bash
pip install -e ".[dev]"
pytest tests/test_unit.py
```

29+ unit tests covering: client construction, credential precedence, env-var fallback, auth-header building per credential type, HTTP error mapping (401/403/404/400/422/429/5xx), NDJSON streaming + cursor pagination, all 5 namespaces' request shapes.

### Integration tests (manual, before each release)

```bash
export OPEDD_BUYER_JWT="..."     # for /buyer-audit + /buyer-compliance-report
export OPEDD_BUYER_TOKEN="..."   # for /content-delivery
export OPEDD_ACCESS_KEY="..."    # for /enterprise-license GET feed
pytest --integration
```

Live tests against `api.opedd.com`. Read-only. Skipped by default (require `--integration` flag).

Per [release discipline](./RELEASE.md), integration tests must pass locally before any version tag is pushed. CI does NOT run integration tests — per institutional risk discipline, autonomous CI runs against production state can pollute `usage_records`, distort metered-publisher payouts, and fire production API calls at scale.

## Development

```bash
git clone https://github.com/Opedd/opedd-python.git
cd opedd-python
pip install -e ".[dev]"
pytest tests/test_unit.py            # unit only (default)
pytest --integration                  # unit + integration (requires env vars)
ruff check src/ tests/                # lint
mypy src/                             # type-check
```

## Contributing

Issues and PRs welcome at [github.com/Opedd/opedd-python](https://github.com/Opedd/opedd-python). For broader questions about Opedd as a platform, email [support@opedd.com](mailto:support@opedd.com).

## License

[MIT](./LICENSE)

## See also

- [docs.opedd.com](https://docs.opedd.com) — full API reference + cookbook
- [opedd-mcp](https://github.com/Opedd/opedd-mcp) — Model Context Protocol server for Claude Desktop / Cursor
- [opedd-backend](https://github.com/Opedd/opedd-backend) — backend implementation (private)
