Skip to content

Valentina Noir Python Client

The valentina-python-client package is an async and sync Python client for the Valentina Noir REST API. It wraps the API in type-safe Pydantic models, handles pagination and retries automatically, and lets you focus on building your application instead of managing HTTP requests.

Note

This client library is a convenience for Python developers. It isn't required to use the Valentina Noir API — you can integrate with any HTTP client in any language by following the full API documentation.

Why Use This Client?

You can call the Valentina Noir API directly with any HTTP library, but this client removes the boilerplate:

  • Type safety — Every request and response is validated through Pydantic models, catching errors early and giving you full IDE autocompletion.
  • Automatic pagination — Stream through large result sets with iter_all() or fetch everything at once with list_all(), without writing pagination logic yourself.
  • Built-in retries — Rate limits (429), server errors (5xx), and network failures are retried automatically with exponential backoff.
  • Async and sync — Built on httpx2 with both async (VClient) and sync (SyncVClient) clients, so it fits naturally into any Python application — from async frameworks like FastAPI to traditional sync code in Flask, Django, or scripts.
  • Idempotency support — Enable automatic idempotency keys so retried writes don't create duplicate resources.
  • Structured logging — Optional Loguru-based logging with a stdlib bridge for debugging and observability.

Requirements

Installation

# Using uv
uv add valentina-python-client

# Using pip
pip install valentina-python-client

Quick Start

Create a client once at application startup, then access API services from anywhere in your code.

Async

import asyncio
from vclient import VClient, companies_service, campaigns_service

async def main():
    async with VClient(
        base_url="https://api.valentina-noir.com",
        api_key="YOUR_API_KEY",
    ) as client:
        # List all companies you have access to
        companies = companies_service()
        for company in await companies.list_all():
            print(f"Company: {company.name}")

        # Fetch campaigns on behalf of a user
        campaigns = campaigns_service(
            on_behalf_of="USER_ID",
            company_id="COMPANY_ID",
        )
        for campaign in await campaigns.list_all():
            print(f"Campaign: {campaign.name}")

asyncio.run(main())

Sync

from vclient import SyncVClient, sync_companies_service, sync_campaigns_service

with SyncVClient(
    base_url="https://api.valentina-noir.com",
    api_key="YOUR_API_KEY",
) as client:
    # List all companies you have access to
    companies = sync_companies_service()
    for company in companies.list_all():
        print(f"Company: {company.name}")

    # Fetch campaigns on behalf of a user
    campaigns = sync_campaigns_service(
        on_behalf_of="USER_ID",
        company_id="COMPANY_ID",
    )
    for campaign in campaigns.list_all():
        print(f"Campaign: {campaign.name}")

You can also set VALENTINA_CLIENT_BASE_URL and VALENTINA_CLIENT_API_KEY as environment variables and create either client with no arguments.

Embedding Child Resources

When fetching a single character, you can embed related resources directly in the response using the include parameter. This avoids extra API calls when you need character data along with their traits, inventory, notes, or assets.

Async

async with VClient(base_url="...", api_key="...") as client:
    svc = client.characters(on_behalf_of="USER_ID")

    # Fetch character with embedded traits and inventory
    detail = await svc.get("CHARACTER_ID", include=["traits", "inventory"])

    # Embedded resources are available directly
    if detail.traits is not None:
        for trait in detail.traits:
            print(f"{trait.trait.name}: {trait.value}")

    if detail.inventory is not None:
        for item in detail.inventory:
            print(f"{item.name} ({item.type})")

    # Without include, embedded fields are None
    basic = await svc.get("CHARACTER_ID")
    assert basic.traits is None  # not requested

Sync

with SyncVClient(base_url="...", api_key="...") as client:
    svc = client.characters(on_behalf_of="USER_ID")
    detail = svc.get("CHARACTER_ID", include=["traits", "notes"])

Valid include values: "traits", "inventory", "notes", "assets". The CharacterDetail return type is a subclass of Character, so existing code that expects Character continues to work.

Learn More

Detailed guides are available for each aspect of the client:

Topic Description
Configuration Timeouts, retries, environment variables, idempotency, and logging
Sync Client Using the synchronous client for non-async applications
Services Available services, method signatures, scoping, and pagination patterns
Response Models Pydantic model specifications for all API resources
Error Handling Exception hierarchy, HTTP status mapping, and usage examples
Testing Fake client for testing downstream applications against the vclient contract

Resources