Metadata-Version: 2.3
Name: ebarimt-pos-sdk
Version: 0.4.1
Summary: Python SDK for Ebarimt POS API 3.0
Author: SenergyTech
License: MIT
Requires-Dist: authlib>=1.7.0
Requires-Dist: httpx>=0.27.0
Requires-Dist: pydantic>=2.7.0
Requires-Dist: sphinx>=8.1.3 ; extra == 'docs'
Requires-Dist: myst-parser>=4.0.1 ; extra == 'docs'
Requires-Dist: sphinx-rtd-theme>=3.1.0 ; extra == 'docs'
Requires-Python: >=3.10
Provides-Extra: docs
Description-Content-Type: text/markdown

# ebarimt-pos-sdk

[![codecov](https://codecov.io/gh/Amraa1/ebarimt-pos-sdk/graph/badge.svg?token=EZ18HFDG46)](https://codecov.io/gh/Amraa1/ebarimt-pos-sdk)
[![Python](https://img.shields.io/badge/python-3.10%2B-blue.svg)](https://www.python.org/downloads/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](./LICENSE)

A modern, async-first Python SDK for the **Ebarimt POS API 3.0** — Mongolia's electronic receipting (e-баримт) platform.
It wraps both the public OAuth2 API and the local REST API exposed by POS devices behind a single, typed, ergonomic
interface.

> 📖 Full documentation: <https://ebarimt-pos-sdk.readthedocs.io/mn/latest/> 📘 Ebarimt POS API 3.0 reference:
> <https://developer.itc.gov.mn/docs/ebarimt-api/inbishdm2zj3x-pos-api-3-0-sistemijn-api-holbolt-zaavruud> 🇲🇳 Mongolian
> version of this README: [README_MN.md](./README_MN.md)

---

## Features

- **Async-first** — every operation exposes both a synchronous and an `async` variant (`method()` / `amethod()`), built
  on `httpx`.
- **Strictly typed** — Pydantic v2 models with camelCase ↔ snake_case aliasing; type-checked with `ty`.
- **Two focused clients** — `EbarimtApiClient` for the public OAuth2 API, `EbarimtRestClient` for the local POS REST
  API.
- **Managed OAuth2** — built-in password-grant flow with automatic token refresh and proactive expiry handling.
- **Resilient transport** — configurable retry with exponential backoff on 5xx and network errors, per-request timeouts,
  and TLS verification.
- **Structured errors** — a clear exception hierarchy distinguishing transport, HTTP, decode, validation, and business
  failures. Sensitive headers and tokens are automatically redacted from error output.
- **Environment presets** — one-line switch between `STAGING` and `PRODUCTION` endpoints via `create_api_settings`.

---

## Installation

```bash
pip install ebarimt-pos-sdk
```

Or with `uv`:

```bash
uv add ebarimt-pos-sdk
```

Requires Python 3.10 or newer.

---

## Quick Start

### Local REST client — issue a receipt

```python
from ebarimt_pos_sdk import EbarimtRestClient, RestClientSettings

settings = RestClientSettings(base_url="http://localhost:1234")

with EbarimtRestClient(settings) as client:
    receipt = client.receipt.create({
        "branch_no": "001",
        "total_amount": 10000,
        "merchant_tin": "1234567890",
        "pos_no": "POS001",
        "type": "B2C_RECEIPT",
        "bill_id_suffix": "A",
        "receipts": [{
            "total_amount": 10000,
            "tax_type": "VAT_ABLE",
            "merchant_tin": "1234567890",
            "items": [{
                "name": "Product",
                "measure_unit": "ш",
                "qty": 1,
                "unit_price": 10000,
                "total_amount": 10000,
            }],
        }],
    })
    print(receipt.id, receipt.qr_data)
```

Async variant: swap `with` for `async with` and call `await client.receipt.acreate(...)`.

```python
import asyncio
from ebarimt_pos_sdk import EbarimtRestClient, RestClientSettings

async def main() -> None:
    async with EbarimtRestClient(RestClientSettings(base_url="http://localhost:1234")) as client:
        receipt = await client.receipt.acreate(payload)
        print(receipt.id)

asyncio.run(main())
```

### Public API client — look up a TIN

Use the factory to target a specific environment without hard-coding URLs:

```python
from ebarimt_pos_sdk import (
    EbarimtApiClient,
    Environment,
    create_api_settings,
)

settings = create_api_settings(
    Environment.PRODUCTION,  # or Environment.STAGING
    client_id="your_client_id",
    username="your_username",
    password="your_password",
)

with EbarimtApiClient(settings=settings) as client:
    info = client.tin_info.read("1234567890")
    print(info.data)
```

#### Routing through a proxy

The public API may only be reachable from a specific region (e.g. Mongolia). Route the API client through an
HTTP/SOCKS proxy with the `proxy` argument:

```python
with EbarimtApiClient(settings, proxy="http://user:pass@mn-proxy:8080") as client:
    info = client.tin_info.read("1234567890")
```

`proxy` accepts a URL string or an `httpx.Proxy`; SOCKS proxies need the `httpx[socks]` extra. It cannot be
combined with an injected `sync_client`/`async_client` — set the proxy on that client instead. The local
`EbarimtRestClient` has no `proxy` argument (it talks to a POS device on your own network).

---

## Clients at a glance

| Client              | Authentication        | Available resources                                              |
| ------------------- | --------------------- | ---------------------------------------------------------------- |
| `EbarimtRestClient` | None (local network)  | `receipt`, `info`, `send_data`, `bank_accounts`                  |
| `EbarimtApiClient`  | OAuth2 password grant | `district_code`, `tin_info`, `merchant_info`, `product_tax_code` |

Both clients support synchronous and asynchronous context managers, share a common settings base (timeouts, TLS,
headers, retry policy), and reuse the same error hierarchy.

---

## Error handling

The SDK surfaces failures through a focused exception hierarchy, so you can react at the level of abstraction that
matters to your code:

```text
PosApiError
├── PosApiTransportError    # network / timeout / DNS / TLS
├── PosApiDecodeError       # response body was not valid JSON
├── PosApiHttpError         # non-2xx response from server
├── PosApiBusinessError     # 2xx, but domain-level failure in payload
└── PosApiValidationError   # Pydantic validation of request or response
```

```python
from ebarimt_pos_sdk import (
    PosApiBusinessError,
    PosApiHttpError,
    PosApiTransportError,
    PosApiValidationError,
)

try:
    receipt = client.receipt.create(payload)
except PosApiValidationError as e:
    # Bad shape — fix the request before resending
    for err in e.errors:
        print(err["loc"], err["msg"])
except PosApiBusinessError as e:
    # Server accepted the request but rejected it on business grounds
    print(e.status, e.code, e.message)
except PosApiHttpError as e:
    # 4xx / 5xx — includes safe, redacted request/response context
    print(e)
except PosApiTransportError:
    # Network layer — safe to retry later
    raise
```

`Authorization` headers and token-bearing query parameters are redacted automatically in every error's string
representation.

---

## Configuration

All settings are immutable dataclasses — construct once and pass to the client. Timeouts, TLS verification, custom
headers, and retry behaviour are shared across both clients via `BaseSettings`.

```python
from ebarimt_pos_sdk import RestClientSettings
from ebarimt_pos_sdk.settings import RetrySettings

settings = RestClientSettings(
    base_url="http://localhost:1234",
    timeout_s=5.0,
    verify_tls=True,
    headers={"X-Request-Source": "pos-42"},
    retry=RetrySettings(
        max_retries=3,
        backoff_base_seconds=1.0,
        retryable_statuses=frozenset({500, 502, 503, 504}),
    ),
)
```

The default retry policy — 3 attempts with exponential backoff on 5xx and network errors — is suitable for most
deployments.

---

## Logging

The SDK logs through the standard library under the `ebarimt_pos_sdk` namespace and follows library-logging
hygiene: a `NullHandler` is attached, so **nothing is emitted until your application configures logging**. The SDK
never adds handlers or sets levels itself.

```python
import logging

logging.basicConfig(level=logging.INFO)
logging.getLogger("ebarimt_pos_sdk").setLevel(logging.DEBUG)
```

What you get:

- **`DEBUG`** — one line per request and response: `→ POST /rest/receipt [a1b2c3d4]` / `← 200 in 42ms [a1b2c3d4]`.
- **`WARNING`** — one line per retried attempt: `retry 1/3 after 503, sleeping 1.00s [a1b2c3d4]`.
- Failures are **not** logged — they raise a typed `PosApiError`. The same `request_id` is available as
  `error.request_id`, so an exception in production correlates with the log lines.

Each record also carries structured fields (`request_id`, `http_method`, `http_status`, `duration_ms`, `attempt`)
via `extra=`, ready for JSON log pipelines.

**Safety:** records are metadata only — method, status, timing, ids. Headers and request/response bodies are never
logged. URLs have sensitive query parameters (tokens, secrets) masked. Path segments are left intact; the only
identifiers the SDK puts in a path are TINs, which are public.

To also see httpx's own output, configure its loggers directly — `httpx` at `INFO` emits an `HTTP Request:` line
(note: the **raw** URL, unredacted), and `httpcore` at `DEBUG` emits connection/wire detail:

```python
import logging.config

logging.config.dictConfig({
    "version": 1,
    "handlers": {"default": {"class": "logging.StreamHandler"}},
    "loggers": {
        "ebarimt_pos_sdk": {"handlers": ["default"], "level": "DEBUG"},
        "httpx": {"handlers": ["default"], "level": "INFO"},
        "httpcore": {"handlers": ["default"], "level": "WARNING"},
    },
})
```

---

## Validation philosophy

The SDK validates **structure, not policy**:

- ✅ Field shapes, regex for stable identifiers (TIN, branch codes), enums, and basic numeric constraints (`>= 0`).
- ❌ Business rules, cross-field dependencies, reference-table lookups, or any government policy that changes out of
  band.

This boundary is intentional. The server owns business rules; the SDK owns shape correctness. That keeps the SDK stable
when rules change.

---

## Development

```bash
# Install all dependencies, including dev extras
uv sync --dev

# Run the unit test suite
uv run pytest -m "not integration"

# Run with coverage
uv run pytest -m "not integration" --cov

# Lint and format
uv run ruff check
uv run ruff format

# Type check
uv run ty check
```

Integration tests (marked `@pytest.mark.integration`) require a live PosAPI server and credentials; they are excluded
from CI by default.

See [CONTRIBUTING.md](./CONTRIBUTING.md) for contribution guidelines and [CHANGELOG.md](./CHANGELOG.md) for release
notes.

---

## License

Released under the [MIT License](./LICENSE).
