Skip to main content

Sweet Potato Authentication & Payment Service Python client

Project description

spaps (Python client)

Python SDK for SPAPS-compatible APIs. The distribution name is spaps; the import path is spaps_client.

Examples in this README use placeholders such as user@example.com, admin@example.com, spaps_test_key, and https://api.example.test. Replace them with values from your own deployment.

Install

pip install spaps

This package targets Python 3.9+.

When It Fits

Need Package gives you
Sync and async clients SpapsClient and AsyncSpapsClient with a similar surface
Typed responses Pydantic models for auth, sessions, payments, entitlements, webhooks, and more
HTTP control points Retry config, logging hooks, injected httpx clients, and custom token storage
Narrow integrations Standalone helpers for device flow, webhooks, users, email, entitlements, marketing, and permission checks

Quick Start

from spaps_client import SpapsClient

client = SpapsClient(
    base_url="http://localhost:3301",
    api_key="spaps_test_key",
)

try:
    tokens = client.auth.sign_in_with_password(
        email="user@example.com",
        password="correct-horse-battery-staple",
    )
    session = client.sessions.get_current_session()
    products = client.payments.list_products(
        category="subscription",
        active=True,
        limit=5,
    )

    print(tokens.user.email)
    print(session.session_id)
    print(products.total)
finally:
    client.close()

Async Example

import asyncio

from spaps_client import AsyncSpapsClient


async def main() -> None:
    client = AsyncSpapsClient(
        base_url="http://localhost:3301",
        api_key="spaps_test_key",
    )
    try:
        await client.auth.sign_in_with_password(
            email="user@example.com",
            password="correct-horse-battery-staple",
        )
        sessions = await client.sessions.list_sessions()
        print(sessions.total)
    finally:
        await client.aclose()


asyncio.run(main())

Core Surface

High-Level Clients

Client Best for
SpapsClient Sync auth, sessions, payments, usage, whitelist, secure messages, issue reporting, marketing, metrics, skill evals, and support telemetry
AsyncSpapsClient Async auth, sessions, payments, usage, whitelist, secure messages, issue reporting, metrics, entitlements, skill evals, dayrate, and users

Standalone Helpers

Helper Purpose
EntitlementsClient / AsyncEntitlementsClient Resource entitlements, purchase history, manual grants, and revokes
UsageClient / AsyncUsageClient Secret-key usage authorization and immutable usage recording
EmailClient / AsyncEmailClient Template lookup, preview, and send flows
UsersClient / AsyncUsersClient Batch user and email lookups
MarketingClient Browser-safe marketing event emission and server-side experiment results
DeviceFlowClient Device-code login workflows
PermissionChecker Role and admin convenience checks
verify_spaps_webhook Signature verification for incoming SPAPS webhooks

Configuration

Constructor values override package defaults.

from spaps_client import SpapsClient, RetryConfig, default_logging_hooks

client = SpapsClient(
    base_url="https://api.example.test",
    api_key="spaps_sec_example",
    retry_config=RetryConfig(max_attempts=4, backoff_factor=0.2),
    logging_hooks=default_logging_hooks(),
)

Common parameters:

Parameter Purpose
base_url Target API origin
api_key Application or service API key
request_timeout Per-request timeout
token_storage Custom token persistence backend
http_client Injected httpx.Client or httpx.AsyncClient
retry_config Retry and backoff policy
logging_hooks Structured request and response logging callbacks

Common Flows

Magic Links and Password Reset

from spaps_client import SpapsClient

client = SpapsClient(
    base_url="http://localhost:3301",
    api_key="spaps_test_key",
)

try:
    client.auth.send_magic_link(email="user@example.com")
    client.auth.request_password_reset(email="user@example.com")
    client.auth.confirm_password_reset(
        token="reset-token-from-email",
        new_password="Sup3rStrong!",
    )
finally:
    client.close()

Authenticated Stripe Checkout

create_checkout_session is the convenience path for authenticated checkout. Pass the server-managed Stripe price_id; the client sends the active Stripe checkout contract with one line item for that price.

from spaps_client import SpapsClient

client = SpapsClient(
    base_url="https://api.example.test",
    api_key="spaps_test_key",
)

try:
    client.auth.sign_in_with_password(
        email="user@example.com",
        password="correct-horse-battery-staple",
    )
    checkout = client.payments.create_checkout_session(
        price_id="price_123",
        mode="subscription",
        success_url="https://app.example.test/success",
        cancel_url="https://app.example.test/cancel",
        trial_period_days=14,
    )
    print(checkout.checkout_url)
finally:
    client.close()

Server-Side Usage Control

Use usage from a trusted backend with a secret SPAPS key. Browser apps should call their own backend first; that backend asks SPAPS to authorize the work, runs the work only on an allow or warning decision, then records the actual usage with a stable idempotency key.

from spaps_client import SpapsClient

client = SpapsClient(
    base_url="https://api.example.test",
    api_key="spaps_sec_example",
)

try:
    authorization = client.usage.authorize_usage(
        feature_key="assistant_tokens",
        resource_type="company",
        resource_id="company_123",
        subject_user_id="user_123",
        dimensions={"requests": 1, "input_tokens": 1200},
    )
    if authorization.decision == "blocked":
        message = authorization.reasons[0].message if authorization.reasons else "Usage not authorized"
        raise RuntimeError(message)

    result = run_assistant_job()

    client.usage.record_usage(
        authorization_ref=authorization.authorization_ref,
        idempotency_key=result.job_id,
        feature_key="assistant_tokens",
        resource_type="company",
        resource_id="company_123",
        subject_user_id="user_123",
        dimensions={
            "input_tokens": result.input_tokens,
            "output_tokens": result.output_tokens,
        },
        metadata={"job_id": result.job_id},
    )
finally:
    client.close()

For dashboard and policy views, use client.usage.get_features(), client.usage.get_status(...), and client.usage.get_history(...) from the same trusted backend context.

Marketing Events

Use client.marketing.emit_event(...) with a publishable key to record anonymous attribution touches or experiment exposures. Use a secret key from a trusted server or agent to read experiment results and the current conservative stop signal.

from spaps_client import SpapsClient

browser_client = SpapsClient(
    base_url="https://api.example.test",
    api_key="spaps_pub_example",
)

try:
    browser_client.marketing.emit_event(
        anon_id="anon_01HY...",
        event_type="experiment_exposure",
        experiment_id="landing-hero-copy",
        variant_id="treatment",
        dedupe_key="landing-hero-copy:anon_01HY:treatment",
    )
finally:
    browser_client.close()

agent_client = SpapsClient(
    base_url="https://api.example.test",
    api_key="spaps_sec_example",
)

try:
    results = agent_client.marketing.get_experiment_results("landing-hero-copy")
    print(results.decision.recommendation, results.decision.winner_variant_id)
finally:
    agent_client.close()

Permission Checks With Explicit Admin Config

from spaps_client import PermissionChecker

checker = PermissionChecker(customAdmins=["admin@example.com"])
role = checker.getRole("operator@example.com")

if checker.requiresAdmin({"email": "operator@example.com"}):
    raise PermissionError(
        checker.getErrorMessage("admin", role, action="change billing settings")
    )

Issue Reporting Voice Token

Voice issue reporting uses a short-lived SPAPS token for ElevenLabs Scribe. Keep the ElevenLabs API key on the SPAPS server.

from spaps_client import SpapsClient

client = SpapsClient(
    base_url="https://api.example.test",
    api_key="spaps_pub_example",
)

try:
    client.set_tokens(access_token="end-user-access-token")
    voice_token = client.issue_reporting.create_voice_token()
    print(voice_token.provider, voice_token.model_id)
finally:
    client.close()

Issue Reporting Screenshot Attachments

Screenshots are uploaded as private pending hosted assets first. Create, update, or reply calls then attach those IDs; raw image bytes, data URLs, and base64 payloads should not be stored in issue notes or target metadata.

from pathlib import Path

from spaps_client import SpapsClient

client = SpapsClient(
    base_url="https://api.example.test",
    api_key="spaps_pub_example",
)

try:
    client.set_tokens(access_token="end-user-access-token")
    attachment = client.issue_reporting.upload_issue_report_attachment(
        file=Path("protocol-save-failure.png").read_bytes(),
        filename="protocol-save-failure.png",
        mime_type="image/png",
    )

    issue = client.issue_reporting.create_issue_report(
        target={
            "component_key": "patient_protocol_widget",
            "component_label": "Patient Protocol Widget",
            "page_url": "/patients/123/protocol",
            "surface_ref": "daily-log",
            "metadata": {"section": "daily log"},
        },
        note="The save action silently fails after I edit today's protocol note.",
        reporter_role_hint="practitioner",
        attachment_ids=[attachment.id],
    )

    access = client.issue_reporting.get_issue_report_attachment_access(
        attachment_id=attachment.id,
    )
    print(issue.id, access.expires_in_seconds)
finally:
    client.close()

SPAPS accepts PNG, JPEG, and WebP screenshots up to 10 MiB each, with at most 5 retained screenshots per report. The hosted object remains private; callers fetch a short-lived access URL after normal issue-reporting authorization succeeds. SPAPS does not redact screenshot contents, so host apps should warn users when a capture may include sensitive data.

Skill Evals

Skill eval helpers wrap the SPAPS blind review endpoints for agent-skill logs. Paid case creation accepts a PAYMENT-SIGNATURE through payment_signature. Reviewers submit valuable and not_valuable marks, and submitters read those marks through an insight inbox before applying skill changes.

from spaps_client import SpapsClient

client = SpapsClient(
    base_url="https://api.example.test",
    api_key="spaps_pub_example",
)

try:
    created = client.skill_evals.create_case(
        {
            "title": "Docs skill comparison",
            "task_claim": "Compare both implementations.",
            "success_criteria": ["Finds repo boundaries"],
            "candidates": [
                {
                    "candidate_id": "A",
                    "output_ref": "spaps-artifact://case/a",
                    "evidence_summary": "Validation passed",
                    "artifact_hash": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
                    "artifact_mime": "text/markdown",
                    "jsonl_log_ref": "spaps-artifact://logs/a.jsonl",
                    "jsonl_log_hash": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
                    "skill_ref": "skill://docs-review",
                    "skill_version_ref": "skill://docs-review/v2.0",
                    "skill_version_digest": "sha256:1111111111111111111111111111111111111111111111111111111111111111",
                    "model_id": "openai/gpt-5.4",
                    "effort_level": "medium",
                    "provenance_ref": "skill://private/a",
                },
                {
                    "candidate_id": "B",
                    "output_ref": "spaps-artifact://case/b",
                    "evidence_summary": "Validation passed",
                    "artifact_hash": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
                    "artifact_mime": "text/markdown",
                    "jsonl_log_ref": "spaps-artifact://logs/b.jsonl",
                    "jsonl_log_hash": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
                    "skill_ref": "skill://docs-review",
                    "skill_version_ref": "skill://docs-review/v2.1",
                    "skill_version_digest": "sha256:2222222222222222222222222222222222222222222222222222222222222222",
                    "model_id": "openai/gpt-5.4",
                    "effort_level": "medium",
                    "provenance_ref": "skill://private/b",
                },
            ],
            "case_policy": {
                "access_mode": "team_private",
                "allowed_model_efforts": [
                    {"model_id": "openai/gpt-5.4", "effort_level": "medium"}
                ],
                "participant_allowlist": ["reviewer-actor-id"],
            },
            "idempotency_key": "eval-create-001",
        },
        payment_signature=signed_payment,
    )
    room = client.skill_evals.get_review_room(created["case_id"])
    print(room["reviewer_state"])
    review = client.skill_evals.submit_review(
        created["case_id"],
        {
            "review_marks": [
                {
                    "candidate_id": "B",
                    "kind": "valuable",
                    "note": "B checks the configured docs path before recommending an edit.",
                    "reason_code": "prevents_wrong_repo_patch",
                    "confidence": "high",
                    "criterion": "Finds repo boundaries",
                }
            ]
        },
    )
    inbox = client.skill_evals.get_insights(created["case_id"])
    print(inbox["valuable"][0]["jsonl_log_ref"], review["review_mark_counts"])
    client.skill_evals.respond_to_review(
        created["case_id"],
        inbox["valuable"][0]["source_review_id"],
        {
            "response": "applied",
            "reason": "Updated the skill from the concrete log-backed insight.",
            "applied_insight_ref": inbox["valuable"][0]["insight_ref"],
            "skill_change_ref": "skill://docs-review/v2.1",
            "skill_version_before": "skill://docs-review/v2.0",
            "skill_version_after": "skill://docs-review/v2.1",
            "jsonl_log_ref": "spaps-artifact://logs/apply.jsonl",
            "jsonl_log_hash": (
                "sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
            ),
            "model_id": "openai/gpt-5.4",
            "effort_level": "medium",
        },
    )
finally:
    client.close()

Webhook Verification

from spaps_client import verify_spaps_webhook

payload = verify_spaps_webhook(
    body=request_body_bytes,
    signature=request_headers["X-SPAPS-Signature"],
    secret="whsec_example",
)

print(payload.type)

Validation

From the repository root:

npm run lint:python-client
npm run typecheck:python-client
npm run test:python-client

For local package work:

cd packages/python-client
pip install -e '.[dev]'

Troubleshooting

ValueError: Access token not found

Authenticate first with client.auth... helpers, or seed tokens with your configured token storage.

401 or 403 responses

Check the API key, access token, and endpoint role requirements for the target environment.

Hosted examples fail against localhost

Set base_url="http://localhost:3301" and use a local development key or test credentials.

I need more control over HTTP behavior

Inject a custom httpx client or configure RetryConfig and logging hooks.

Which client should I start with?

Use SpapsClient unless your service is already async end to end.

Limitations

  • Some specialty integrations live as standalone helpers instead of methods on the main client.
  • Endpoint coverage tracks the active SPAPS backend surface; new backend features may appear here incrementally.
  • You still need to provide environment-specific API keys, tokens, and deployment URLs.

FAQ

Is the package name different from the import path?

Yes. Install spaps; import from spaps_client.

Does it support async codebases?

Yes. Use AsyncSpapsClient and the async helper classes.

Is webhook verification included?

Yes. Use verify_spaps_webhook.

Can I replace the default token storage?

Yes. Pass a custom token_storage implementation.

Does it retry requests automatically?

It can. Pass RetryConfig if you want retry and backoff behavior.

Metadata

  • package_name: spaps
  • latest_version: 0.5.0
  • minimum_runtime: Python >=3.9
  • api_base_url: https://api.sweetpotato.dev

About Contributions

About Contributions: Please don't take this the wrong way, but I do not accept outside contributions for any of my projects. I simply don't have the mental bandwidth to review anything, and it's my name on the thing, so I'm responsible for any problems it causes; thus, the risk-reward is highly asymmetric from my perspective. I'd also have to worry about other "stakeholders," which seems unwise for tools I mostly make for myself for free. Feel free to submit issues, and even PRs if you want to illustrate a proposed fix, but know I won't merge them directly. Instead, I'll have Claude or Codex review submissions via gh and independently decide whether and how to address them. Bug reports in particular are welcome. Sorry if this offends, but I want to avoid wasted time and hurt feelings. I understand this isn't in sync with the prevailing open-source ethos that seeks community contributions, but it's the only way I can move at this velocity and keep my sanity.

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

spaps-0.5.1.tar.gz (105.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

spaps-0.5.1-py3-none-any.whl (98.8 kB view details)

Uploaded Python 3

File details

Details for the file spaps-0.5.1.tar.gz.

File metadata

  • Download URL: spaps-0.5.1.tar.gz
  • Upload date:
  • Size: 105.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.13

File hashes

Hashes for spaps-0.5.1.tar.gz
Algorithm Hash digest
SHA256 6262d2d8f50db680f94910d7d17e5b16c30f2e7d4af199046c943ce8de815f67
MD5 52961d0b86d4505589c71ecffe7a1dc8
BLAKE2b-256 0fe37b4906617357ed34500795194573d93342d5d92079652804f246b9152b24

See more details on using hashes here.

File details

Details for the file spaps-0.5.1-py3-none-any.whl.

File metadata

  • Download URL: spaps-0.5.1-py3-none-any.whl
  • Upload date:
  • Size: 98.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.13

File hashes

Hashes for spaps-0.5.1-py3-none-any.whl
Algorithm Hash digest
SHA256 bda3ff053c87684cd35db3b723b5bc3265b1787ba9c7f91270d43bb0b3e493e7
MD5 8b9ae069c03d6f1fa1782c3dc3c33f43
BLAKE2b-256 5fd0aa0a995245d66583514e69cacbd632dc84ec70485be1c4ca4b8adcf19bae

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page