Metadata-Version: 2.4
Name: precoro-sdk
Version: 0.1.0
Summary: Python SDK for Precoro API
Author-email: Precoro SDK Team <support@precoro.com>
License-Expression: MIT
Project-URL: Homepage, https://github.com/precoro/precoro-python-sdk
Project-URL: Documentation, https://github.com/precoro/precoro-python-sdk#readme
Project-URL: Repository, https://github.com/precoro/precoro-python-sdk
Project-URL: Issues, https://github.com/precoro/precoro-python-sdk/issues
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
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: Typing :: Typed
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: requests>=2.28.0
Provides-Extra: dev
Requires-Dist: pytest>=7.0; extra == "dev"
Requires-Dist: responses>=0.23; extra == "dev"
Requires-Dist: ruff>=0.4; extra == "dev"
Requires-Dist: mypy>=1.0; extra == "dev"
Requires-Dist: types-requests; extra == "dev"
Dynamic: license-file

# Precoro Python SDK

A production-ready synchronous Python client for the [Precoro API](https://help.precoro.com/using-api-in-precoro).

## Features

- **Synchronous** blocking I/O using `requests`
- **Full type hints** (PEP 484) for excellent IDE support
- **Automatic retry logic** with exponential backoff for rate limits and server errors
- **Pagination support** with auto-pagination helper methods
- **Comprehensive error handling** with custom exception hierarchy
- **Thread-safe** when using separate client instances per thread

## Installation

```bash
# Install in editable mode with development dependencies
pip install -e ".[dev]"

# Or install from PyPI (when published)
pip install precoro-sdk
```

## Quick Start

```python
from precoro import PrecoroClient

# Initialize the client
client = PrecoroClient(
    token="your_company_token",
    email="your@email.com",
    base_url="https://api.precoro.com",  # or https://api.precoro.us
)

# Fetch a page of invoices
invoices = client.invoices.list(per_page=50)
print(f"Total invoices: {invoices['meta']['pagination']['total']}")

# Iterate through invoice data
for invoice in invoices['data']:
    print(f"Invoice {invoice['idn']}: ${invoice['totalAmount']}")

# Get a specific invoice by IDN
invoice = client.invoices.get("12345")
print(invoice['vendorName'])

# Auto-paginate through all invoices with filters
all_approved = client.invoices.list_all(
    status=[2],  # 2 = approved
    modified_since="2025-01-01T00:00:00"
)
print(f"Found {len(all_approved)} approved invoices")

# Set external ID for integration
client.invoices.set_external_id(
    idn="12345",
    external_id="QB-INV-789",
    integration_log="Synced from QuickBooks"
)

# Always close the client when done
client.close()
```

### Using Context Manager (Recommended)

```python
from precoro import PrecoroClient

with PrecoroClient(token="xxx", email="user@example.com") as client:
    invoices = client.invoices.list(per_page=100)
    # Client automatically closes when exiting the context
```

## API Documentation

### PrecoroClient

Main client class for accessing Precoro API resources.

**Parameters:**
- `token` (str): Company API token (X-AUTH-TOKEN header)
- `email` (str): User email address (email header)
- `base_url` (str): API base URL. Options:
  - `https://api.precoro.com` (International, default)
  - `https://api.precoro.us` (United States)
- `timeout` (tuple[float, float]): HTTP timeout as (connect, read) in seconds. Default: `(5.0, 30.0)`
- `max_retries` (int): Maximum retry attempts for retryable errors. Default: `3`

**Resources:**

| Attribute | Resource | Endpoint |
|---|---|---|
| `client.invoices` | Invoices & Credit Notes | `/invoices` |
| `client.purchase_orders` | Purchase Orders | `/purchaseorders` |
| `client.purchase_requisitions` | Purchase Requisitions | `/purchaserequisitions` |
| `client.request_for_proposals` | Requests for Proposals | `/requestforproposals` |
| `client.receipts` | Receipts | `/receipts` |
| `client.expenses` | Expenses | `/expenses` |
| `client.payments` | Payments & Expense Payments | `/payments`, `/expensePayments` |
| `client.budgets` | Budgets | `/budgets` |
| `client.warehouse_requests` | Warehouse Requests | `/warehouserequests` |
| `client.ocr` | AP Documents (OCR) | `/ap/documents` |
| `client.inventory` | Stock Transfers, Stock-takings, Warehouse Items | `/stocktransfers`, `/stock_takings` |
| `client.suppliers` | Suppliers | `/suppliers` |
| `client.items` | Catalog Items | `/items` |
| `client.supplier_custom_fields` | Supplier Custom Fields | `/suppliercustomfields` |
| `client.item_custom_fields` | Item Custom Fields | `/itemcustomfields` |
| `client.document_custom_fields` | Document Custom Fields | `/documentcustomfields` |
| `client.units` | Measurement Units | `/units` |
| `client.contracts` | Contracts | `/contracts` |
| `client.payment_terms` | Payment Terms | `/paymentterms` |
| `client.locations` | Company Locations | `/locations` |
| `client.users` | Company Users | `/users` |
| `client.legal_entities` | Legal Entities | `/legalentities` |
| `client.warehouses` | Warehouses | `/warehouses` |
| `client.taxes` | Taxes | `/taxes` |
| `client.attachments` | Attachments | `/attachments` |
| `client.approval` | Approval (approve / reject / revise) | per document type |

### InvoicesResource

Methods for interacting with invoices.

#### `list(page, per_page, **filters)`

Fetch a single page of invoices.

**Parameters:**
- `page` (int): Page number (1-indexed). Default: `1`
- `per_page` (int): Items per page (10, 20, 50, 100, 200). Default: `100`
- `modified_since` (str, optional): Filter for invoices modified after datetime (format: `"2022-12-12T00:00:00"`)
- `approval_left_date` (str, optional): Filter for invoices approved after datetime
- `approval_right_date` (str, optional): Filter for invoices approved before datetime
- `status` (list[int], optional): Filter by status IDs (e.g., `[2, 4]`)
- `logic_type` (list[int], optional): Filter by logic type (0=standard, 1=credit note, 2=PO-based)

**Returns:** `dict` with `"data"` (list of invoices) and `"meta"` (pagination info)

#### `list_all(per_page, **filters)`

Auto-paginate through all invoices. Accepts same filters as `list()` except `page`.

**Returns:** `list[dict]` - Flat list of all invoices

#### `get(idn)`

Fetch a single invoice by IDN.

**Parameters:**
- `idn` (str): Internal company identifier

**Returns:** `dict` - Invoice object (not wrapped in "data")

#### `set_external_id(idn, external_id, integration_log=None)`

Set the externalId field on an invoice.

**Parameters:**
- `idn` (str): Internal company identifier
- `external_id` (str): External system identifier
- `integration_log` (str, optional): Integration log message

**Returns:** `dict` - Updated invoice object

## Error Handling

The SDK provides a comprehensive exception hierarchy:

```python
from precoro import (
    PrecoroError,              # Base exception
    PrecoroAPIError,           # Base for HTTP errors
    PrecoroAuthenticationError, # 401/403 errors
    PrecoroRateLimitError,     # 429 errors
    PrecoroClientError,        # Other 4xx errors
    PrecoroServerError,        # 5xx errors
    PrecoroConnectionError,    # Network failures
)

try:
    invoice = client.invoices.get("12345")
except PrecoroAuthenticationError:
    print("Invalid credentials")
except PrecoroRateLimitError as e:
    print(f"Rate limited. Retry after {e.retry_after} seconds")
except PrecoroServerError as e:
    print(f"Server error: {e.status_code}")
except PrecoroConnectionError:
    print("Network connection failed")
except PrecoroError as e:
    print(f"Unexpected error: {e}")
```

### Automatic Retries

The client automatically retries:
- **429 (Rate Limit)**: Respects `Retry-After` header, falls back to exponential backoff
- **5xx (Server Errors)**: Exponential backoff with jitter
- **Connection/Timeout Errors**: Exponential backoff with jitter

**Not retried:**
- 401/403 (Authentication errors)
- Other 4xx (Client errors)

## Logging

The SDK uses Python's standard logging module with the logger name `"precoro"`.

```python
import logging

# Enable debug logging to see all HTTP requests
logging.basicConfig(level=logging.DEBUG)

# Or configure just the Precoro logger
logger = logging.getLogger("precoro")
logger.setLevel(logging.DEBUG)
handler = logging.StreamHandler()
handler.setFormatter(logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s'))
logger.addHandler(handler)

# Now all requests will be logged
client = PrecoroClient(token="xxx", email="user@example.com")
invoices = client.invoices.list()
# Output: 2025-02-16 10:30:00 - precoro - DEBUG - GET https://api.precoro.com/invoices -> 200 (0.45s)
```

**Log Levels:**
- `DEBUG`: HTTP requests (method, URL, status, response time)
- `WARNING`: Retry attempts (reason, attempt number, delay)
- `ERROR`: Final failures after exhausting retries

## Rate Limits

Precoro API rate limits:
- **60 requests/minute** (1 req/sec)
- **1500 requests/hour**
- **3000 requests/day**
- **Duplicate requests**: 1/min, 30/hour

The SDK handles rate limiting automatically with retries.

## Development

```bash
# Install dependencies
pip install -e ".[dev]"

# Run tests
pytest

# Run type checker
mypy precoro

# Run linter
ruff check precoro

# Format code
ruff format precoro
```

## Thread Safety

**Warning:** The client uses `requests.Session` which is **not thread-safe**. Create a separate client instance for each thread:

```python
from threading import Thread
from precoro import PrecoroClient

def fetch_invoices():
    # Create a new client per thread
    client = PrecoroClient(token="xxx", email="user@example.com")
    invoices = client.invoices.list()
    client.close()

threads = [Thread(target=fetch_invoices) for _ in range(5)]
for t in threads:
    t.start()
for t in threads:
    t.join()
```

## License

MIT

## Support

For issues and questions, please visit [GitHub Issues](https://github.com/precoro/precoro-python-sdk/issues).
