<div align="center">
  <img src="./docs/assets/logo.png" alt="pdView logo" width="92">
  
  <h1>pd4castr API SDK</h1>
  
  <p>Official Python client for the pd4castr API</p>
  
  <p>
    <a href="#installation">Installation</a> •
    <a href="#quick-start">Quick Start</a> •
    <a href="#usage">Usage</a> •
    <a href="#api-reference">API Reference</a>
  </p>
</div>

---

## Installation

> Requires Python >= 3.8

```bash
pip install pd4castr-api-sdk
```

## Quick Start

Authenticate using your [pd4castr](https://v2.pd4castr.com.au) API credentials:

```python
from pd4castr_api_sdk import Client

client = Client(
    client_id="my-client-id",
    client_secret="my-client-secret",
)

models = client.get_models()
print(f"Found {len(models)} models")

client.close()
```

### Context Manager

The client implements context manager protocol for automatic cleanup:

```python
with Client(
    client_id="my-client-id",
    client_secret="my-client-secret",
) as client:
    models = client.get_models()
```

## Usage

### List all Models

[Reference: `Client.getModels`](#get_models)

Retrieve all available models, optionally filtered by time horizon.

```python
# Get all models
models = client.get_models()

# Filter by time horizon
models = client.get_models(time_horizon="day_ahead")
```

### Get a Model

[Reference: `Client.get_model`](#clientgetmodel)

Retrieve detailed information for a specific model.

```python
model = client.get_model("model-id")
print(f"Model: {model.displayName}")
```

### Get Current User

[Reference: `Client.get_current_user`](#clientgetcurrentuser)

Retrieve information about the currently authenticated user.

```python
user = client.get_current_user()
print(f"User: {user.email}")
if user.organisation:
    print(f"Organisation: {user.organisation.displayName}")
```

### Error Handling

[Reference: Exceptions](#exceptions)

Handle common error cases with typed exceptions.

```python
from pd4castr_api_sdk import NotFoundError, ApiError

try:
    model = client.get_model("invalid-id")
except NotFoundError:
    print("Model not found")
except ApiError as e:
    print(f"API error {e.status_code}: {e.message}")
```

## API Reference

**Client:**

- [`Client`](#client) - Main API client
- [`Client#getModels`](#clientgetmodels) - List all models
- [`Client#getModel`](#clientgetmodel) - Get a specific model
- [`Client#getCurrentUser`](#clientgetcurrentuser) - Get the current user

**Types:**

- [`Model`](#model) - Model information and related types
- [`User`](#user) - User information and related types

**Exceptions:**

- [`ApiError`](#apierror) - Base API error
- [`NotFoundError`](#notfounderror) - Resource not found error

### `Client`

```python
Client(
    client_id: str,
    client_secret: str,
    timeout: float = 30.0,
)
```

Main client class for interacting with the pd4castr API. Supports context
manager protocol for automatic cleanup.

**Configuration:**

- `client_id` - OAuth client ID for authentication
- `client_secret` - OAuth client secret for authentication
- `timeout` - Request timeout in seconds (default: 30.0)

### `Client#getModels`

List all models with optional time horizon filter.

`Client#getModels(*, time_horizon: str | None = None) -> list[`[`Model`](#model)`]`

**Parameters:**

- `time_horizon` (optional) - Filter models by time horizon:
  - `day_ahead`
  - `week_ahead`
  - `quarterly`
  - `historical`

**Returns:** A list of [`Model`](#model) instances

---

### `Client#getModel`

Get a specific model by ID.

`Client#get_model(model_id: str) ->`[`Model`](#model)

**Parameters:**

- `model_id` - Unique model identifier

**Returns:** [`Model`](#model) instance

**Raises:**

- [`NotFoundError`](#notfounderror) if model doesn't exist

---

### `Client#getCurrentUser`

Get the currently authenticated user.

`Client#get_current_user() ->`[`User`](#user)

**Returns:** [`User`](#user) instance

---

### Types

All types are implemented as
[pydantic models](https://docs.pydantic.dev/latest/).

#### `Model`

```python
class Model(BaseModel):
    id: str
    modelGroupId: str
    name: str
    displayName: str
    displayTimezone: str
    revision: int
    dockerImage: str
    notes: str
    createdAt: str
    horizon: TimeHorizon
    modelMetadata: ModelMetadata
    outputSpecification: list[ColumnSpecification]
    tags: list[str]
    sensitivities: list[SensitivitySpecification]
    outputFileFormat: FileFormat
    forecastVariable: ForecastVariableSpecification

class TimeHorizon(BaseModel):
    name: str
    shortName: str
    key: str

class ModelMetadata(BaseModel):
    description: str | None
    releaseDate: str | None
    resolution: str | None
    baseComparisonModel: str | None

class ColumnSpecification(BaseModel):
    name: str
    type: str
    ui: UISpecification | None

class UISpecification(BaseModel):
    seriesKey: bool | None
    colours: list[str] | None

class SensitivitySpecification(BaseModel):
    name: str
    categories: list[str]

class ForecastVariableSpecification(BaseModel):
    name: str
    shortName: str
    key: str

class FileFormat(Enum):
    json = "json"
    csv = "csv"
    parquet = "parquet"
```

#### `User`

```python
class User(BaseModel):
    id: str
    email: str
    organisation: Organisation | None

class Organisation(BaseModel):
    id: str
    displayName: str
    slug: str
    domains: list[str]
    permissions: list[str]
```

### Exceptions

#### `ApiError`

Base exception for API errors.

```python
class ApiError(Exception):
    status_code: int
    message: str
    body: dict | None
```

#### `NotFoundError`

Resource not found error (HTTP 404). Inherits from [`ApiError`](#apierror).

```python
class NotFoundError(ApiError):
    pass
```

## Contributing

See [CONTRIBUTING.md](./CONTRIBUTING.md) for development setup, testing, and
contribution guidelines.
