Metadata-Version: 2.3
Name: ezeas-client
Version: 0.1.7
Summary: Ezeas Client API SDK (Python, async)
Author: michael@blueskycreations.com.au
Requires-Python: >=3.9,<4.0
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Requires-Dist: httpx (>=0.27.0)
Description-Content-Type: text/markdown

# Ezeas Client SDK (Python)

Minimal async client for the ezeas Client API using HS256 JWT.

This SDK is intended for backend integrations (Python 3.9+). It handles:

- Generating and caching short-lived JWT access tokens using `client_id` and `client_secret`
- Sending authenticated requests to the ezeas Client API
- Minimal error handling for business errors (4xx) vs internal errors (5xx)

## Installation

From PyPI (production):

```bash
pip install ezeas-client
```

> Requires Python **3.9+**.

## Quick start

```python
import asyncio
from ezeas_client import EzeasClient

async def main() -> None:
    client_id = "your-client-id"
    client_secret = "your-client-secret"

    async with EzeasClient(
        client_id=client_id,
        client_secret=client_secret,
    ) as client:
        # Create contact
        contact_result = await client.create_contact(
            {
                "external_id": "EXT-123",
                "employee_number": "EMP-001",
                "first_name": "Jane",
                "last_name": "Doe",
                "mobile": "0400000000",
                "email": "jane.doe@example.com",
                "tenure_date": "2025-11-01",
            }
        )
        print("Contact result:", contact_result)

        # Create timesheet
        timesheet_result = await client.create_timesheet(
            {
                "job_code": "JOB-001",
                "timesheet_entries": [
                    {
                        "date": "2025-11-01",
                        "hours_ordinary": 8.0,
                        "hours_time_and_half": 2.0,
                        "hours_double_time": 0.0,
                    },
                    {
                        "date": "2025-11-02",
                        "hours_ordinary": 7.5,
                        "hours_time_and_half": 0.0,
                        "hours_double_time": 0.0,
                    },
                ],
            }
        )
        print("Timesheet result:", timesheet_result)

if __name__ == "__main__":
    asyncio.run(main())
```

## Types

The SDK uses `TypedDict` internally. The logical shapes are:

```python
from typing import TypedDict, List

class ContactInput(TypedDict):
    external_id: str
    employee_number: str
    first_name: str
    last_name: str
    mobile: str
    email: str
    tenure_date: str


class TimesheetEntry(TypedDict):
    date: str
    hours_ordinary: float
    hours_time_and_half: float
    hours_double_time: float


class TimesheetInput(TypedDict):
    job_code: str
    timesheet_entries: List[TimesheetEntry]
```

You can pass either these shapes or any plain `dict` with the same keys.

## API

### `EzeasClient(client_id, client_secret)`

Creates a new async client instance.

- `client_id`: string issued by ezeas
- `client_secret`: shared secret issued by ezeas

The recommended usage is via `async with`:

```python
async with EzeasClient(client_id, client_secret) as client:
    ...
```

### `await client.create_contact(data)`

Creates a contact for the account associated with this `client_id`.

- `data`: `ContactInput` (or a plain dict with the same shape)

Returns the parsed JSON response from the ezeas API (or `None` if the response has no body).

### `await client.create_timesheet(data)`

Creates a timesheet for the account associated with this `client_id`.

- `data`: `TimesheetInput` (or a plain dict with the same shape)

Returns the parsed JSON response from the ezeas API (or `None` if the response has no body).

## Error handling

Internally, the SDK uses `httpx.AsyncClient`. The current behaviour is:

- For **2xx** responses:
  - Parses and returns JSON (or `None` if the body is empty).
- For **4xx** responses:
  - Raises `Exception` with the backend error payload as text.
- For **5xx** responses:
  - Raises `Exception` with a message in the form:
    - `"Internal server error <status>: <body>"`

You can wrap calls in your own try/except logic to map these to your application's error model.

## Token format

The SDK generates short-lived JWT access tokens using HS256:

- Header:
  - `alg`: `"HS256"`
  - `typ`: `"JWT"`
- Payload:
  - `sub`: `client_id`
  - `client_id`: `client_id`
  - `iat`: issued-at (seconds since epoch)
  - `exp`: expiry (seconds since epoch, typically `iat + 900`)

The token is signed with `client_secret`.

The ezeas API validates this token server-side and resolves the associated account from the client credentials.

