Metadata-Version: 2.4
Name: ordercloud-python
Version: 2026.4.1
Summary: Idiomatic Python SDK for Sitecore OrderCloud
Project-URL: Homepage, https://github.com/markcassidyconsulting/ordercloud-python
Project-URL: Repository, https://github.com/markcassidyconsulting/ordercloud-python
Project-URL: Changelog, https://github.com/markcassidyconsulting/ordercloud-python/blob/main/CHANGELOG.md
Project-URL: Issues, https://github.com/markcassidyconsulting/ordercloud-python/issues
Author-email: Mark Cassidy Consulting <mark.cassidy@markcassidyconsulting.com>
License-Expression: MIT
License-File: LICENSE
Keywords: api,async,commerce,ecommerce,ordercloud,pydantic,sdk,sitecore
Classifier: Development Status :: 4 - Beta
Classifier: Framework :: Pydantic :: 2
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 :: Internet :: WWW/HTTP
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.10
Requires-Dist: httpx>=0.28.1
Requires-Dist: pydantic>=2.13.0
Provides-Extra: codegen
Requires-Dist: jinja2>=3.1.6; extra == 'codegen'
Provides-Extra: dev
Requires-Dist: mypy>=1.20.1; extra == 'dev'
Requires-Dist: pytest-asyncio>=1.3.0; extra == 'dev'
Requires-Dist: pytest-cov>=7.1.0; extra == 'dev'
Requires-Dist: pytest>=9.0.3; extra == 'dev'
Requires-Dist: respx>=0.23.1; extra == 'dev'
Requires-Dist: ruff>=0.15.10; extra == 'dev'
Provides-Extra: examples
Requires-Dist: python-dotenv>=1.2.2; extra == 'examples'
Description-Content-Type: text/markdown

# ordercloud-python

[![CI](https://github.com/markcassidyconsulting/ordercloud-python/actions/workflows/ci.yml/badge.svg)](https://github.com/markcassidyconsulting/ordercloud-python/actions/workflows/ci.yml)
[![CodeQL](https://github.com/markcassidyconsulting/ordercloud-python/actions/workflows/codeql.yml/badge.svg)](https://github.com/markcassidyconsulting/ordercloud-python/actions/workflows/codeql.yml)
[![codecov](https://codecov.io/gh/markcassidyconsulting/ordercloud-python/graph/badge.svg)](https://codecov.io/gh/markcassidyconsulting/ordercloud-python)
[![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/markcassidyconsulting/ordercloud-python/badge)](https://scorecard.dev/viewer/?uri=github.com/markcassidyconsulting/ordercloud-python)
![Python](https://img.shields.io/badge/python-3.10%20%7C%203.11%20%7C%203.12%20%7C%203.13-blue)
[![PyPI](https://img.shields.io/pypi/v/ordercloud-python)](https://pypi.org/project/ordercloud-python/)
[![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)

A fully typed, async-first Python SDK for [Sitecore OrderCloud](https://ordercloud.io).

**Complete API coverage** — all 632 operations across 60 resources, generated from the official OpenAPI spec. Built for modern Python:

- **Async and sync clients** — `async with OrderCloudClient(...)` or `with SyncOrderCloudClient(...)`. Same API shape, your choice of runtime.
- **Pydantic v2 models** — every API resource is a typed, validated model. snake_case fields, PascalCase aliases for API compatibility.
- **Typed extended properties** — `Product[MyXpModel]` gives you type-safe access to OrderCloud's `xp` fields.
- **Auto-pagination** — `async for product in paginate(client.products.list)` handles page iteration automatically.
- **Retry with backoff** — configurable retries on 429/5xx with exponential backoff and `Retry-After` support.
- **Middleware hooks** — intercept requests and responses for logging, metrics, or header injection.
- **Structured logging** — standard Python `logging` module, DEBUG/WARNING levels.
- **Full type annotations** — `py.typed` marker for downstream type checking with mypy, pyright, etc.
- **784 tests, 97% coverage** — 759 unit tests (mocked HTTP) + 25 integration tests (live sandbox).

## Installation

```bash
pip install ordercloud-python
```

Requires Python 3.10+.

## Quick Start

### Async (default)

```python
import asyncio
from ordercloud import OrderCloudClient

async def main():
    async with OrderCloudClient.create(
        client_id="YOUR_CLIENT_ID",
        client_secret="YOUR_CLIENT_SECRET",
    ) as client:
        # List products
        products = await client.products.list(page_size=10)
        for p in products.items:
            print(f"{p.id}: {p.name}")

        # Create a product
        from ordercloud.models import Product
        product = await client.products.create(Product(
            name="My Product",
            active=True,
        ))
        print(f"Created: {product.id}")

asyncio.run(main())
```

### Sync

```python
from ordercloud import SyncOrderCloudClient

with SyncOrderCloudClient.create(
    client_id="YOUR_CLIENT_ID",
    client_secret="YOUR_CLIENT_SECRET",
) as client:
    products = client.products.list(page_size=10)
    for p in products.items:
        print(f"{p.id}: {p.name}")
```

The sync client wraps the async client internally — same features, same API shape, no `await`.

## Configuration

| Parameter | Default | Description |
|-----------|---------|-------------|
| `client_id` | *(required)* | OAuth2 client ID |
| `client_secret` | `""` | OAuth2 client secret (empty for public clients) |
| `base_url` | `https://api.ordercloud.io/v1` | API base URL |
| `auth_url` | `https://auth.ordercloud.io/oauth/token` | OAuth2 token endpoint |
| `scopes` | `["FullAccess"]` | OAuth2 scopes to request |
| `timeout` | `30.0` | HTTP request timeout (seconds) |
| `max_retries` | `0` | Max retries on 429/5xx (0 = disabled) |
| `retry_backoff` | `0.5` | Base delay in seconds for exponential backoff |

### Regional Environments

| Environment | API Base URL | Auth URL |
|-------------|-------------|----------|
| US Production | `https://api.ordercloud.io/v1` | `https://auth.ordercloud.io/oauth/token` |
| US Sandbox | `https://sandboxapi.ordercloud.io/v1` | `https://sandboxauth.ordercloud.io/oauth/token` |
| Europe West Production | `https://westeurope-production.ordercloud.io/v1` | `https://westeurope-production-auth.ordercloud.io/oauth/token` |
| Europe West Sandbox | `https://westeurope-sandbox.ordercloud.io/v1` | `https://westeurope-sandbox-auth.ordercloud.io/oauth/token` |
| Australia East Production | `https://australiaeast-production.ordercloud.io/v1` | `https://australiaeast-production-auth.ordercloud.io/oauth/token` |
| Japan East Production | `https://japaneast-production.ordercloud.io/v1` | `https://japaneast-production-auth.ordercloud.io/oauth/token` |

## Typed Extended Properties (xp)

OrderCloud models support extended properties (`xp`) — arbitrary JSON attached to any resource. By default, `xp` is `dict[str, Any]`. You can type it with a Pydantic model:

```python
from pydantic import BaseModel
from ordercloud.models import Product

class MyProductXp(BaseModel):
    color: str
    weight_kg: float

# Create with typed xp
product = Product[MyProductXp](
    name="Widget",
    xp=MyProductXp(color="red", weight_kg=1.5),
)
product.xp.color  # str, not Any

# Deserialise with typed xp (API responses use PascalCase — the SDK handles both)
data = {"Name": "Widget", "xp": {"color": "blue", "weight_kg": 2.0}}
product = Product[MyProductXp].model_validate(data)
product.xp.color  # "blue"
```

Unparameterized usage (`Product(xp={"anything": True})`) still works — fully backward compatible. PascalCase field names are accepted as aliases for construction and deserialization (e.g. `Product(Name="Widget")` still works), but snake_case is the canonical Python form.

## Auto-Pagination

Iterate through all pages automatically:

```python
from ordercloud import paginate

# Async
async for product in paginate(client.products.list, search="widget"):
    print(product.name)

# Works with positional args too
async for order in paginate(client.orders.list, OrderDirection.Incoming):
    print(order.id)
```

For the sync client:

```python
from ordercloud import paginate_sync

for product in paginate_sync(client.products.list, search="widget"):
    print(product.name)
```

## Retry Logic

Enable automatic retries on transient failures (429 rate limit, 5xx server errors):

```python
client = OrderCloudClient.create(
    client_id="...",
    client_secret="...",
    max_retries=3,       # Retry up to 3 times
    retry_backoff=0.5,   # 0.5s, 1s, 2s exponential backoff
)
```

Respects `Retry-After` headers. Never retries on 4xx client errors (400, 401, 403, 404, etc.).

## Structured Logging

The SDK logs via Python's standard `logging` module under the `ordercloud` logger:

```python
import logging
logging.basicConfig(level=logging.DEBUG)

# Or configure just the SDK logger
logging.getLogger("ordercloud").setLevel(logging.DEBUG)
```

| Level | What's logged |
|-------|--------------|
| `DEBUG` | Every request (`Request: GET /products`) and response (`Response: GET /products 200`) |
| `WARNING` | Retry attempts with status code and backoff delay |

## Middleware Hooks

Register hooks to intercept requests and responses:

```python
from ordercloud import RequestContext, ResponseContext

async def add_correlation_id(ctx: RequestContext) -> None:
    ctx.headers["X-Correlation-ID"] = generate_id()

async def log_timing(ctx: ResponseContext) -> None:
    print(f"{ctx.request.method} {ctx.request.path} -> {ctx.response.status_code}")

client.add_before_request(add_correlation_id)
client.add_after_response(log_timing)
```

Before-request hooks receive a mutable `RequestContext` — modify `headers`, `params`, or `json` before the request is sent. After-response hooks receive a `ResponseContext` with the request details and response. Hooks are called on every attempt, including retries.

## API Coverage

The SDK covers **all 60 resources** and **632 operations** in the OrderCloud API. Models and resource clients are generated from the official OpenAPI v3 spec (version 1.0.445).

### Core Commerce

| Resource | Operations | Highlights |
|----------|-----------|------------|
| Products | 18 | CRUD, variants, specs, suppliers, assignments |
| Orders | 29 | CRUD, submit, approve, decline, cancel, complete, forward, split, ship, promotions |
| Line Items | 9 | CRUD, shipping address management, cross-order listing |
| Cart | 37 | Full shopping cart lifecycle, checkout, payments, promotions |
| Bundles | 12 | CRUD, product/catalog assignments |
| Catalogs | 15 | CRUD, product/bundle/category assignments |
| Categories | 15 | CRUD, hierarchical with depth control, assignments |

### Buyers & Users

| Resource | Operations | Highlights |
|----------|-----------|------------|
| Buyers | 7 | CRUD, seller relationships |
| Buyer Groups | 6 | CRUD |
| Users | 11 | CRUD, access tokens, move, cross-buyer listing |
| User Groups | 9 | CRUD, user assignments |
| Me | 80 | Full buyer-perspective API (addresses, orders, products, subscriptions, etc.) |
| Admin Users | 8 | CRUD, token revocation, account unlock |
| Admin User Groups | 9 | CRUD, user assignments |

### Pricing & Promotions

| Resource | Operations | Highlights |
|----------|-----------|------------|
| Price Schedules | 8 | CRUD, price breaks |
| Promotions | 9 | CRUD, assignments |
| Discounts | 9 | CRUD, assignments |
| Specs | 15 | CRUD, options, product assignments |

### Fulfillment

| Resource | Operations | Highlights |
|----------|-----------|------------|
| Shipments | 12 | CRUD, items, ship-from/ship-to addresses |
| Payments | 7 | CRUD, transactions |
| Order Returns | 14 | CRUD, submit, approve, decline, complete, cancel |

### Organisation & Security

| Resource | Operations | Highlights |
|----------|-----------|------------|
| Suppliers | 9 | CRUD, buyer relationships |
| Security Profiles | 9 | CRUD, assignments |
| API Clients | 15 | CRUD, secrets, assignments |
| Addresses | 9 | CRUD, assignments |
| Cost Centers | 9 | CRUD, assignments |
| Credit Cards | 9 | CRUD, assignments |
| Spending Accounts | 9 | CRUD, assignments |

### Integrations & Infrastructure

| Resource | Operations | Highlights |
|----------|-----------|------------|
| Webhooks | 6 | CRUD |
| Integration Events | 10 | CRUD, calculate, estimate shipping |
| Message Senders | 11 | CRUD, assignments, CC listeners |
| Subscriptions | 6 | CRUD |
| Entity Syncs | 40 | Full sync infrastructure |
| Delivery Configurations | 6 | CRUD |
| Inventory Records | 18 | CRUD, variant records, assignments |

## Usage Examples

### Products

```python
# List with search and pagination
products = await client.products.list(
    search="widget",
    search_on="Name,Description",
    sort_by="Name",
    page=1,
    page_size=20,
)
print(f"Found {products.meta.total_count} products")

# Get by ID
product = await client.products.get("my-product-id")

# Create
from ordercloud.models import Product
product = await client.products.create(Product(
    id="my-product",
    name="Widget",
    description="A fine widget",
    active=True,
))

# Update (PUT — full replace)
product = await client.products.save("my-product", Product(
    name="Updated Widget",
    active=True,
))

# Patch (partial update)
product = await client.products.patch("my-product", {"Description": "An even finer widget"})

# Delete
await client.products.delete("my-product")
```

### Orders

```python
from ordercloud.models import Order, OrderDirection

# List incoming orders
orders = await client.orders.list(OrderDirection.Incoming, page_size=50)

# Create an outgoing order
order = await client.orders.create(
    OrderDirection.Outgoing,
    Order(comments="Rush delivery"),
)

# Order workflow
order = await client.orders.submit(OrderDirection.Outgoing, order.id)
order = await client.orders.approve(OrderDirection.Incoming, order.id)
order = await client.orders.complete(OrderDirection.Incoming, order.id)
```

### Line Items

```python
from ordercloud.models import LineItem, OrderDirection

# Add a line item to an order
line_item = await client.line_items.create(
    OrderDirection.Outgoing, "order-id",
    LineItem(product_id="my-product", quantity=3),
)

# List line items on an order
line_items = await client.line_items.list(OrderDirection.Outgoing, "order-id")
for li in line_items.items:
    print(f"  {li.product_id} x{li.quantity}")
```

### Catalogs and Categories

```python
from ordercloud.models import Catalog, Category

# Create a catalog
catalog = await client.catalogs.create(Catalog(
    name="Spring Collection",
    active=True,
))

# Create a category within it
category = await client.categories.create(catalog.id, Category(
    name="New Arrivals",
    active=True,
))

# List categories (with depth control)
categories = await client.categories.list(catalog.id, depth="all")
```

### Filtering

All `list()` methods accept a `filters` dict for server-side filtering:

```python
# Products with Active=true and Name starting with "Widget"
products = await client.products.list(filters={
    "Active": True,
    "Name": "Widget*",
})

# Orders with Total > 100
orders = await client.orders.list(filters={"Total": ">100"})
```

## Error Handling

```python
from ordercloud import OrderCloudError, AuthenticationError

try:
    product = await client.products.get("nonexistent")
except AuthenticationError as e:
    # 401 or 403
    print(f"Auth failed: {e}")
except OrderCloudError as e:
    # Any other API error (4xx/5xx)
    print(f"API error {e.status_code}: {e}")
    for error in e.errors:
        print(f"  {error.error_code}: {error.message}")
```

## Code Generation

Models and resource clients are generated from the OrderCloud OpenAPI v3 spec using the included codegen tool:

```bash
pip install -e ".[codegen]"
python -m tools.codegen --spec path/to/ordercloud-openapi-v3.json --output src/ordercloud
```

The codegen pipeline: **OpenAPI JSON** -> parser -> intermediate representation -> transformer -> Jinja2 templates -> Python source -> ruff format. Hand-written infrastructure (`shared.py`, `base.py`, `auth.py`, `http.py`, `config.py`, `errors.py`, `middleware.py`, `sync_client.py`) is preserved — only model and resource files are generated.

## Development

```bash
git clone https://github.com/markcassidyconsulting/ordercloud-python.git
cd ordercloud-python
pip install -e ".[dev,examples,codegen]"
```

### Running Tests

```bash
# Unit tests only (mocked HTTP, no network calls — fast)
pytest tests/ --ignore=tests/integration

# Unit tests with coverage
pytest tests/ --ignore=tests/integration --cov=ordercloud --cov-report=term-missing

# Lint and format
ruff check src/ tests/
ruff format --check src/ tests/

# Type checking
mypy src/
```

### Integration Tests

Integration tests run against a live OrderCloud sandbox and are **skipped automatically** when credentials are not set. They never run by accident.

**Setup:**

1. Create a `.env` file at the repo root (gitignored):

```env
ORDERCLOUD_TEST_CLIENT_ID=your-sandbox-client-id
ORDERCLOUD_TEST_CLIENT_SECRET=your-sandbox-client-secret
ORDERCLOUD_TEST_BASE_URL=https://sandboxapi.ordercloud.io/v1
ORDERCLOUD_TEST_AUTH_URL=https://sandboxauth.ordercloud.io/oauth/token
```

2. Run:

```bash
pytest tests/integration/ -v
```

The test suite is self-bootstrapping — it uses the SDK itself to create all test data from a single admin API client credential. All test resources use an `inttest-` ID prefix and are cleaned up automatically.

> **Why `ORDERCLOUD_TEST_*`?** The `TEST_` prefix prevents the integration tests from running against a production OrderCloud instance if you happen to have `ORDERCLOUD_CLIENT_ID` set in your environment for normal SDK usage.

### Test Suite

784 tests across 12 modules.

**Unit tests (759)** — mocked HTTP via [respx](https://lundberg.github.io/respx/), no network calls:

| Module | Tests | Purpose |
|--------|-------|---------|
| `test_auth.py` | 13 | OAuth2 token management |
| `test_http.py` | 16 | HTTP client, error parsing, retries |
| `test_models.py` | 28 | Model round-trips, enums, xp, ListPage |
| `test_resources.py` | 22 | Representative resource operations |
| `test_resource_coverage.py` | 632 | All 60 resources, all 632 operations |
| `test_sync_client.py` | 48 | Sync wrapper, pagination |

**Integration tests (25)** — live sandbox, skipped when credentials are absent:

| Module | Tests | Purpose |
|--------|-------|---------|
| `test_auth.py` | 4 | Client credentials grant, token caching |
| `test_crud.py` | 7 | Products, Buyers, Catalogs, Categories, Users |
| `test_pagination.py` | 3 | Auto-pagination, search, list metadata |
| `test_query_params.py` | 3 | Assignment lifecycle, DELETE with query params |
| `test_errors.py` | 5 | Error parsing, structured API errors |
| `test_sync_client.py` | 3 | Sync CRUD, pagination, errors |

**Coverage** (97% overall, 90% threshold enforced in CI):

| Module | Coverage |
|--------|----------|
| `auth.py` | 100% |
| `client.py` | 100% |
| `config.py` | 100% |
| `errors.py` | 100% |
| `http.py` | 97% |
| `middleware.py` | 100% |
| `sync_client.py` | 100% |
| `resources/base.py` | 100% |
| `models/shared.py` | 100% |
| All 37 model modules | 100% |

## Contributing

Bug reports and feature requests are welcome via [GitHub Issues](https://github.com/markcassidyconsulting/ordercloud-python/issues). If you're interested in the internals, the codegen pipeline in `tools/codegen/` is a good starting point. See the [Changelog](CHANGELOG.md) for release history.

## License

MIT
