Metadata-Version: 2.4
Name: grailx_client
Version: 0.1.1
Summary: Python SDK for the Grail-X Platform API
Author: Xcidic - GrailX
License: MIT
Project-URL: Homepage, https://github.com/xcidic/project-grailx-sdk-python
Keywords: grailx,sdk,api,grailx_client
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Requires-Python: >=3.8
Description-Content-Type: text/markdown
Requires-Dist: requests>=2.25.0
Requires-Dist: aiohttp>=3.8.0
Requires-Dist: typing-extensions>=4.0.0; python_version < "3.10"
Provides-Extra: dev
Requires-Dist: pytest>=7.0.0; extra == "dev"
Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
Requires-Dist: mypy>=1.0.0; extra == "dev"
Requires-Dist: responses>=0.23.0; extra == "dev"
Requires-Dist: aioresponses>=0.7.0; extra == "dev"

# Grail-X Python SDK

The **Grail-X Python SDK** is a client library for interacting with the [Grail-X Platform API](https://apidoc.stag.grailx.xcidic.com/). The client SDK is available as a Python package at [PyPI](https://pypi.org/project/grailx-client/). It handles authentication, request retries, typed response parsing, and supports both synchronous and asynchronous usage.

## Installation

Install from the repository (or from PyPI once published):

```bash
pip install grailx_client
```

For development, install the package with extras:

```bash
pip install -e '.[dev]'
```

## Requirements

- Python 3.8+
- `requests` (sync HTTP)
- `aiohttp` (async HTTP)

## Quick start

```python
from grailx_client import GrailXClient

client = GrailXClient(api_key="your_api_key")
products = client.products.list(limit=50, region="ID")
for product in products.data:
    print(product.product_id, product.name)
```

## Configuration

Create a client with the API key and optional settings:

```python
client = GrailXClient(
    api_key="your_api_key",
    base_url="https://dummy-gw.stag.grailx.xcidic.com",
    timeout_ms=30000,          # request timeout in milliseconds
    retries=3,                 # retry attempts for transient failures
    retry_delay_ms=1000,       # initial retry delay
    retry_backoff="linear",    # or "exponential"
    retry_backoff_factor=2.0,
    verify_ssl=True,
    headers={
        "X-Custom-Header": "value",
    },
)
```

| Option                 | Description                                       | Default                                   |
| ---------------------- | ------------------------------------------------- | ----------------------------------------- |
| `api_key`              | API key used in `Authorization: Bearer <api_key>` | `None`                                    |
| `base_url`             | API base URL                                      | `https://dummy-gw.stag.grailx.xcidic.com` |
| `timeout_ms`           | Request timeout in milliseconds                   | `None` (no timeout)                       |
| `retries`              | Number of retries for transient failures          | `3`                                       |
| `retry_delay_ms`       | Initial retry delay in milliseconds               | `1000`                                    |
| `retry_backoff`        | Backoff strategy: `"linear"` or `"exponential"`   | `"linear"`                                |
| `retry_backoff_factor` | Multiplier applied to retry delay                 | `2.0`                                     |
| `verify_ssl`           | Verify SSL certificates                           | `True`                                    |
| `headers`              | Additional headers for every request              | `{}`                                      |

The SDK does not generate or require `X-Tenant-ID`, `X-User-ID`, `X-Key-Scopes`, or `X-Request-ID`. Pass them in the `headers` dictionary when needed.

## Resources

The client exposes one resource attribute for each API module:

| Resource                 | Description                          | OpenAPI tag     |
| ------------------------ | ------------------------------------ | --------------- |
| `client.encode`          | Encode data into watermarked images  | Encode          |
| `client.verify`          | Verify encoded data and enrich       | Verify          |
| `client.scans`           | Scan history and details             | Scans           |
| `client.batch_encode`    | Batch encode multiple images         | Batch Encode    |
| `client.products`        | Create and list products             | Products        |
| `client.product_batches` | Create and manage product batches    | Product Batches |
| `client.audit_trail`     | Query and create request logs        | Audit Trail     |
| `client.events`          | Ingest analytics events              | Events          |
| `client.supply_chain`    | Supply chain nodes and relationships | Supply Chain    |
| `client.alert`           | Alert delivery targets               | Alerting        |
| `client.analytics`       | Scan and anomaly analytics           | Analytics       |

## Usage examples

### List products

```python
products = client.products.list(limit=50, region="ID")
for product in products.data:
    print(product.product_id, product.name)

# pagination information is available when the API returns it
print(products.pagination)
```

### Create a product

```python
product = client.products.create(
    body={
        "product_id": "abc123",
        "name": "My Product",
        "sku": "SKU123",
        "manufacturer": "Acme",
        "expected_regions": ["ID", "SG"],
    }
)
print(product.data.product_id)
```

### Encode data

```python
encoded = client.encode.encode(
    body={
        "image_url": "https://example.com/source.png",
        "version": "v1",
        "product_id": "abc123",
        "batch_id": "batch-001",
        "region": "ID",
    }
)
print(encoded.data.watermark_id, encoded.data.watermarked_image_url)
```

### Verify data

```python
verified = client.verify.verify(
    body={
        "image_url": "https://example.com/scan.png",
        "location": {"latitude": -6.2, "longitude": 106.8},
    }
)
print(verified.data.present, verified.data.confidence)
print(verified.data.product.name)
```

### Scan history

```python
scans = client.scans.list(
    product_id="abc123",
    region="ID",
    time_range_start="2024-01-01T00:00:00Z",
    time_range_end="2024-12-31T23:59:59Z",
)
for scan in scans.data:
    print(scan.id, scan.confidence, scan.geo_matched)
```

### Submit a batch encoding job

```python
batch = client.batch_encode.submit(
    body={
        "items": [
            {
                "image_url": "https://example.com/1.png",
                "payload": {
                    "version": "v1",
                    "product_id": "abc123",
                    "batch_id": "batch-001",
                    "region": "ID",
                },
            }
        ],
        "callback_url": "https://example.com/callback",
    }
)
print(batch.data.batch_id)

# poll the status
status = client.batch_encode.get_status(batch.data.batch_id)
print(status.data.status)
```

## Async usage

For async applications, use `AsyncGrailXClient`:

```python
import asyncio
from grailx_client import AsyncGrailXClient

async def main():
    async with AsyncGrailXClient(api_key="your_api_key") as client:
        products = await client.products.list(limit=50, region="ID")
        for product in products.data:
            print(product.product_id, product.name)

asyncio.run(main())
```

All resource methods have the same names and signatures as the sync client.

## Error handling

The SDK raises `GrailXAPIError` for non-2xx responses:

```python
from grailx_client import GrailXClient, GrailXAPIError

client = GrailXClient(api_key="your_api_key")

try:
    product = client.products.get("non-existent-id")
except GrailXAPIError as exc:
    print(exc.status_code)
    if exc.error:
        print(exc.error.error.code)
        print(exc.error.error.message)
```

The SDK automatically retries on transient failures:

- Network errors (connection failures, timeouts)
- HTTP `429 Too Many Requests`
- HTTP `5xx` server errors

## Typed responses

Every response is wrapped in a `Response[T]` object:

```python
response = client.products.list()

response.data       # typed payload
response.request_id # X-Request-ID echoed by the API
response.pagination # pagination info for list endpoints
response.raw_response # raw JSON response dict
```

For list endpoints, `response.data` is a list of typed models. For most single-object endpoints, `response.data` is a single typed model.

## Closing the client

The sync client can be used as a context manager:

```python
with GrailXClient(api_key="your_api_key") as client:
    client.products.list()
```

For async, use the async context manager:

```python
async with AsyncGrailXClient(api_key="your_api_key") as client:
    await client.products.list()
```

You can also close the underlying HTTP session manually:

```python
client.close()

await async_client.aclose()
```

## Development

Create a virtual environment and install dependencies:

```bash
python -m venv venv
source venv/bin/activate
pip install -e '.[dev]'
```

Run tests:

```bash
python -m pytest ./tests
```

Run type checking:

```bash
python -m mypy grailx_client
```

## Publishing

1. Build the package:

   ```bash
   python -m build
   ```

1. Check the package:

   ```bash
   python -m twine check dist/*
   ```

1. Upload to PyPI:

   ```bash
   export TWINE_USERNAME=__token__
   export TWINE_PASSWORD=your-pypi-api-token
   python -m twine upload dist/*
   ```

## License

For detailed API parameters, see the bundled OpenAPI spec at [docs/openapi.bundle.json](docs/openapi.bundle.json) or [online documentation](https://apidoc.stag.grailx.xcidic.com/).
