Metadata-Version: 2.5
Name: veypost
Version: 0.1.1
Summary: Python client for the Veypost public publishing API
Classifier: Programming Language :: Python :: 3
Classifier: Typing :: Typed
Requires-Python: >=3.11
Requires-Dist: httpx<1,>=0.27
Provides-Extra: dev
Requires-Dist: build>=1.2; extra == 'dev'
Requires-Dist: mypy>=1.11; extra == 'dev'
Requires-Dist: pytest>=8; extra == 'dev'
Description-Content-Type: text/markdown

# Veypost Python SDK

Python SDK (`0.1.1`) for the public publishing API. Requires Python 3.11+. JavaScript, analytics and server-side bulk jobs remain outside this release.

## Install

Once version `0.1.1` is published to PyPI:

```bash
pip install veypost
```

## Install from this checkout

From the repository root, in your own virtual environment:

```bash
python -m pip install -e ./packages/python-sdk
export VEYPOST_API_KEY="YOUR_API_KEY"
export VEYPOST_API_URL="http://localhost:8000"
# Optional: select an explicitly allowed workspace.
# export VEYPOST_WORKSPACE_ID="YOUR_WORKSPACE_UUID"
```

A local backend needs its existing public API feature configuration, migrations, eligible subscription and scoped key; installing this client does not enable those gates.

## Connect and discover accounts

```python
import os
from veypost import Veypost

with Veypost(api_key=os.environ["VEYPOST_API_KEY"]) as client:
    accounts = client.accounts.list(limit=10)
    for account in accounts.data["items"]:
        print(account["id"], account["provider"], account["display_name"])
```

`Veypost()` reads `VEYPOST_API_KEY` automatically. Configuration priority is explicit argument, then environment, then built-in default. `base_url` defaults to `https://api.veypost.blanchorizon.com`; set `VEYPOST_API_URL` for local use. The production default is configuration, **not evidence of hosted public API availability**.

```python
import os
from veypost import Veypost

client = Veypost(
    api_key=os.environ["VEYPOST_API_KEY"],
    workspace_id=os.environ.get("VEYPOST_WORKSPACE_ID"),
    base_url="http://localhost:8000",
    timeout=30.0,
    max_retries=2,
    upload_timeout=300.0,
)
try:
    page = client.posts.list(status="draft")
finally:
    client.close()
```

This is a synchronous, server-side client. Use a context manager or `close()`. Use a separate client per concurrent worker; upload helpers transfer sequentially. Environment proxies are disabled and redirects are not followed. HTTPS is required except for loopback HTTP. `base_url` is the API origin, without `/api/public/v1`.

## Publishing workflow

Each Python snippet below opens and closes its own client. Set `VEYPOST_ACCOUNT_ID`
to a discovered account UUID for draft creation. Existing-post examples use
`VEYPOST_POST_ID`; publication commands use a persisted UUID in `VEYPOST_COMMAND_ID`.


1. Select an allowed workspace and discover its accounts; create a draft.
2. Upload files if needed; wait for verification and attachment.
3. Validate the draft and inspect issues.
4. Retrieve the latest ETag.
5. Explicitly request publication or scheduling.
6. Follow the returned publication request and individual delivery states.

Every result is `APIResponse[T]`: typed dictionary `.data`, `.status_code`, `.etag`, `.request_id`, `.idempotency_key`. The response representation hides data because upload responses can contain temporary secrets. Generated TypedDicts are available as `veypost.models`; runtime request validation uses the same bundled OpenAPI contract.

### Create and edit drafts

```python
import os
from uuid import uuid4
from veypost import Veypost

with Veypost(
    api_key=os.environ["VEYPOST_API_KEY"],
    base_url=os.environ["VEYPOST_API_URL"],
    workspace_id=os.environ.get("VEYPOST_WORKSPACE_ID"),
) as client:
    # Save this UUID before sending; reuse it only for an exact retry.
    command_id = str(uuid4())
    draft = client.posts.create(
        base_caption="My first API draft",
        destinations=[{"social_account_id": os.environ["VEYPOST_ACCOUNT_ID"]}],
        idempotency_key=command_id,
    )
    print(draft.data["id"], draft.etag)

    edited = client.posts.update(
        draft.data["id"],
        changes={"base_caption": "My reviewed caption"},
        if_match=draft.etag,
        idempotency_key=str(uuid4()),
    )
    print(edited.data["id"], edited.etag)
```

Set `VEYPOST_ACCOUNT_ID` to an ID from `client.accounts.list()`. All content belongs directly to the selected workspace. Set `workspace_id=...` per call to override the client’s default; the API key must allow that workspace.

Edits send only `changes`. Omit unchanged fields; only `title` may explicitly be `None`. The client requires the exact quoted ETag. On `412`, retrieve and review the post; it never silently refreshes a version and overwrites another edit.

### Upload local files

```python
import os
from pathlib import Path
from veypost import Veypost

with Veypost(
    api_key=os.environ["VEYPOST_API_KEY"],
    base_url=os.environ["VEYPOST_API_URL"],
    workspace_id=os.environ.get("VEYPOST_WORKSPACE_ID"),
) as client:
    post_id = os.environ["VEYPOST_POST_ID"]
    current = client.posts.retrieve(post_id)
    plan = client.uploads.prepare(
        post_id, [Path("photo.jpg")], if_match=current.etag,
    )
    # Save plan.to_dict() privately before sending. Retain IDs and checksums.
    client.uploads.send(plan)
    client.uploads.wait_until_ready(plan, timeout=180)
    current = client.posts.retrieve(post_id)
    print(current.data["id"], current.etag)
```

`prepare` hashes files in 1 MiB blocks, validates supported MIME types/sizes and generates stable reservation IDs without a network call. `send` rechecks file content, reserves a bounded batch, and streams bytes. Signed resumable uploads use 6 MiB chunks and reconcile ambiguous failures with `HEAD`. Signed `PUT` is supported when no resumable ticket is returned. Only the API-issued storage headers are used; the Veypost key is never attached to storage requests. Resumable session URLs stay in memory; a restored plan reserves the same IDs and can obtain fresh capabilities, but does not promise cross-process byte-offset recovery. Retrying after a response loss may require checking current attachment state first.

A batch permits at most 20 reservation items, while aggregate post/file/provider limits still apply. This is multiple-file upload for **one post**, not a bulk-post API. Existing attachment positions start at zero; pass `start_position` when appending. For a cover, use `UploadFile(path, purpose="cover", video_id=..., frame_ms=...)`.

`send` completion means bytes transferred, not verified or published. `wait_until_ready` watches only the plan's attachment IDs and stops on `attention`/`removed`; a timeout does not cancel work. Inspect `client.media.attachments(post_id)` and `client.media.retrieve(media_id)` for recovery. Never log upload response `.data`, capability URLs or tokens. The SDK redacts signed query tokens in HTTPX INFO logging; applications must not enable custom header/body logging of secrets.

Low-level APIs are also available: `client.media.reserve_uploads(post_id, items=..., if_match=..., idempotency_key=...)`, `client.media.attachments(...)`, `client.media.retrieve(...)`.

### Publish immediately

```python
import os
from veypost import Veypost

with Veypost(
    api_key=os.environ["VEYPOST_API_KEY"],
    base_url=os.environ["VEYPOST_API_URL"],
    workspace_id=os.environ.get("VEYPOST_WORKSPACE_ID"),
) as client:
    post_id = os.environ["VEYPOST_POST_ID"]
    validation = client.posts.validate(post_id)
    if not validation.data["can_publish_now"]:
        raise RuntimeError("Review validation issues")
    current = client.posts.retrieve(post_id)
    receipt = client.posts.publish(
        post_id,
        if_match=current.etag,
        confirm_live_publish=True,
        idempotency_key=os.environ["VEYPOST_COMMAND_ID"],
    )
    status = client.publication_requests.retrieve(post_id, receipt.data["id"])
    print(status.data["request"]["state"])
    print(status.data["post"]["deliveries"])
```

A `202` means accepted, not published. Worker execution, provider permissions, account/format readiness and subscription quotas remain authoritative. The SDK does not repeatedly submit publication to wait for success. Inspect request/post/delivery states, including partial failures and unknown provider outcomes.

### Schedule a post

```python
import os
from veypost import Veypost

with Veypost(
    api_key=os.environ["VEYPOST_API_KEY"],
    base_url=os.environ["VEYPOST_API_URL"],
    workspace_id=os.environ.get("VEYPOST_WORKSPACE_ID"),
) as client:
    post_id = os.environ["VEYPOST_POST_ID"]
    validation = client.posts.validate(post_id)
    if not validation.data["can_schedule"]:
        raise RuntimeError("Review validation issues")
    current = client.posts.retrieve(post_id)
    receipt = client.posts.schedule(
        post_id,
        local_datetime="2030-11-03T10:00:00",
        timezone="Europe/Paris",
        fold=0,
        if_match=current.etag,
        idempotency_key=os.environ["VEYPOST_COMMAND_ID"],
    )
    print(receipt.data["id"], receipt.data["state"])
```

Schedule values are local wall-clock time plus an IANA timezone; the server rejects past times and daylight-saving gaps. `fold` selects the occurrence of an ambiguous time. Validate `can_schedule` first.

Withdrawal removes an eligible pending request. `cancel_schedule` cancels eligible scheduled post/delivery work. Deleting an eligible post is a separate command and returns `.data is None` for HTTP 204. These do not promise remote deletion of already-published social posts. Work already submitting/reconciling may not be cancellable. To change a pending schedule, withdraw its request, retrieve/review the draft and latest ETag, then submit a new schedule; the SDK does not disguise that multi-step process as an atomic reschedule.

### Withdraw, cancel or delete

Set `VEYPOST_ACTION` to `withdraw`, `cancel`, or `delete`. Withdrawal also needs
`VEYPOST_REQUEST_ID` from the accepted request. Each revisioned change retrieves a
fresh ETag; review the post before cancelling or deleting it.

```python
import os
from veypost import Veypost

with Veypost(
    api_key=os.environ["VEYPOST_API_KEY"],
    base_url=os.environ["VEYPOST_API_URL"],
    workspace_id=os.environ.get("VEYPOST_WORKSPACE_ID"),
) as client:
    post_id = os.environ["VEYPOST_POST_ID"]
    # Choose one action, then inspect the returned state before another change.
    action = os.environ["VEYPOST_ACTION"]  # withdraw, cancel, or delete
    if action == "withdraw":
        result = client.publication_requests.withdraw(
            post_id, os.environ["VEYPOST_REQUEST_ID"],
        )
    elif action == "cancel":
        current = client.posts.retrieve(post_id)
        result = client.posts.cancel_schedule(
            post_id, if_match=current.etag,
            idempotency_key=os.environ["VEYPOST_COMMAND_ID"],
        )
    elif action == "delete":
        current = client.posts.retrieve(post_id)
        result = client.posts.delete(post_id, if_match=current.etag)
    else:
        raise ValueError("Choose withdraw, cancel, or delete")
    print(result.status_code, result.data)
```

## Errors and safe retries

```python
import os
from veypost import APIError, ConnectionError
from veypost import Veypost

with Veypost(
    api_key=os.environ["VEYPOST_API_KEY"],
    base_url=os.environ["VEYPOST_API_URL"],
    workspace_id=os.environ.get("VEYPOST_WORKSPACE_ID"),
) as client:
    try:
        result = client.posts.create(
            idempotency_key=os.environ["VEYPOST_COMMAND_ID"],
        )
    except APIError as error:
        print(error.status_code, error.code, error.request_id)
    except ConnectionError as error:
        # The command may have been accepted. Keep error.idempotency_key.
        # Inspect status or retry the identical command; do not publish again.
        raise
```

Retryable 429/503 and transport failures are retried up to `max_retries` only for reads, validation or commands carrying an idempotency key. Retries preserve the exact serialized body, workspace, revision and key. A server Retry-After above 30 seconds is surfaced to the caller rather than shortened. State conflicts, malformed responses and unkeyed delete/withdraw commands are not automatically retried. Exceptions carry a generated idempotency key for recovery; caller-supplied persisted IDs are preferable for durable jobs. A bounded in-memory guard catches changed input for recently used command IDs; the server enforces durable idempotency. No API keys, request bodies or signed URLs are included in SDK exception messages.

Additional exceptions: `ProtocolError`, `UploadError`, `WaitTimeout`, all under `VeypostError`. HTTP response data is typed but is not a provider-success guarantee. Read timeouts bound individual network operations; complete workflow duration includes retries and processing.

## Pagination

```python
import os
from veypost import Veypost

with Veypost(
    api_key=os.environ["VEYPOST_API_KEY"],
    base_url=os.environ["VEYPOST_API_URL"],
    workspace_id=os.environ.get("VEYPOST_WORKSPACE_ID"),
) as client:
    for account in client.accounts.iter_all(limit=50):
        print(account["id"])
    for post in client.posts.iter_all(status="scheduled"):
        print(post["id"])
```

`list()` returns one page; `iter_all()` lazily follows cursors and rejects repeated cursors. The key and owner plan share normal API/dashboard/MCP quotas.

## Runnable examples

Examples default to `http://localhost:8000` unless `VEYPOST_API_URL` is set. All mutation examples require `--execute`; commands also require a caller-supplied `--command-id` where applicable. They do not run during installation.

```bash
python packages/python-sdk/examples/discover.py
python packages/python-sdk/examples/create_draft.py --help
python packages/python-sdk/examples/upload_media.py --help
python packages/python-sdk/examples/publish.py --help
python packages/python-sdk/examples/schedule.py --help
python packages/python-sdk/examples/manage_post.py --help
```

## Development and contract maintenance

```bash
uv sync --project packages/python-sdk --extra dev
uv run --project packages/python-sdk pytest packages/python-sdk/tests
python packages/python-sdk/scripts/generate_contract.py --check
uv build --project packages/python-sdk --out-dir /tmp/veypost-sdk-dist
```

After an intentional OpenAPI change, run `python packages/python-sdk/scripts/generate_contract.py`, review generated models/contract, and update adapters/examples/tests. The SDK has no backend application import or runtime dependency on repository paths. Tests substitute API and storage transports; no real provider publishing or hosted acceptance is implied.
