Metadata-Version: 2.4
Name: licentric
Version: 0.1.0
Summary: Python SDK for the Licentric software licensing API
Project-URL: Homepage, https://licentric.com
Project-URL: Documentation, https://licentric.com/docs
Project-URL: Repository, https://github.com/AbleVarghese/licentric
Project-URL: Changelog, https://github.com/AbleVarghese/licentric/blob/main/CHANGELOG.md
Project-URL: Bug Tracker, https://github.com/AbleVarghese/licentric/issues
Author: Able Varghese
License-Expression: MIT
License-File: LICENSE
Keywords: api,licensing,licentric,sdk,software-licensing
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
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
Classifier: Typing :: Typed
Requires-Python: >=3.9
Requires-Dist: httpx>=0.27
Requires-Dist: pydantic>=2.0
Provides-Extra: dev
Requires-Dist: cryptography>=42.0; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Requires-Dist: respx>=0.21; extra == 'dev'
Provides-Extra: offline
Requires-Dist: cryptography>=42.0; extra == 'offline'
Description-Content-Type: text/markdown

# Licentric Python SDK

[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

Official Python client for the [Licentric](https://licentric.com) software licensing API. Validate licenses, manage machine activations, send heartbeats, and check out offline license files.

> **Note:** This package is not yet published to PyPI. Install from source (see below).

## Installation

```bash
# Install from source (not yet published to PyPI)
cd sdks/python
pip install -e .
```

**Requirements:** Python 3.9+

## Quick Start

```python
from licentric import Licentric

client = Licentric(api_key="lk_live_...")

# Validate a license key
result = client.validate(key="LIC-XXXX-XXXX-XXXX")
if result.valid:
    print(f"License is valid — code: {result.code}")
```

## Machine Activation

```python
from licentric import Licentric
from licentric.fingerprint import fingerprint

client = Licentric(api_key="lk_live_...")

# Activate a machine
machine = client.activate(
    key="LIC-XXXX-XXXX-XXXX",
    fingerprint=fingerprint(),
    name="production-server",
    platform="linux",
)
print(f"Activated machine: {machine.id}")

# Send heartbeat to keep the machine alive
client.heartbeat(machine_id=machine.id, key="LIC-XXXX-XXXX-XXXX")

# Deactivate when done
client.deactivate(machine_id=machine.id, key="LIC-XXXX-XXXX-XXXX")
```

## Fetching License Details

`get_license` and `get_license_by_key` return the full license — including
all activated machines and the bound policy snapshot — in a single API call.
Useful for dashboards, admin UIs, and any flow that needs to inspect the
ruleset without making three sequential requests.

```python
client = Licentric(api_key="lk_live_...")

# By internal license ID
license = client.get_license("lic_abc123")

# Or by raw license key (URL-encoded server-side)
license = client.get_license_by_key("LIC-XXXX-XXXX-XXXX-XXXX")

print(f"Status: {license.status}")
print(f"Active machines: {len(license.machines or [])}")
print(f"Max machines allowed: {license.policy.max_machines}")
print(f"Offline allowed: {license.policy.offline_allowed}")
print(f"Feature flags: {license.policy.metadata.get('feature_flags', [])}")
```

The raw `key` and `key_hash` fields are stripped server-side — `License`
returned here never contains the secret material.

## Offline License Files

Check out a signed license file for offline validation:

```python
license_file = client.checkout(
    license_id="uuid-of-license",
    fingerprint=fingerprint(),
    ttl=1209600,  # 14 days
)
print(f"Certificate: {license_file.certificate}")
```

## Offline Caching

Cache validation results locally with HMAC-SHA256 tamper detection:

```python
from licentric.cache import ValidationCache

cache = ValidationCache()

# Store a validation result (default TTL: 15 minutes)
cache.set(key_hash="sha256_of_license_key", result={"valid": True, "code": "VALID"})

# Retrieve (returns None if expired or tampered)
cached = cache.get(key_hash="sha256_of_license_key")
if cached:
    print(cached.data)
```

## Error Handling

All API errors inherit from `LicentricError` with structured fields:

```python
from licentric import (
    Licentric,
    AuthenticationError,
    LicenseRevokedError,
    LicenseSuspendedError,
    LicenseExpiredError,
    NotFoundError,
    RateLimitError,
)
import time

client = Licentric(api_key="lk_live_...")

try:
    machine = client.activate(key="LIC-XXXX", fingerprint="abc123")
except LicenseRevokedError:
    # Permanent — show "license has been revoked, contact support"
    print("License revoked")
except LicenseSuspendedError:
    # Temporary — usually a payment issue; surface "billing on hold"
    print("License suspended")
except LicenseExpiredError:
    # Renewal flow
    print("License expired — please renew")
except RateLimitError as e:
    # Retry-eligible after backoff
    time.sleep(e.retry_after)
except AuthenticationError:
    # Configuration problem — fail immediately
    print("Invalid API key")
```

### HTTP-status exceptions (raised by HTTP code)

| Exception | HTTP Status | When |
|-----------|-------------|------|
| `AuthenticationError` | 401 | Invalid or missing API key |
| `ForbiddenError` | 403 | Authenticated but lacks permission/scope |
| `NotFoundError` | 404 | Resource does not exist |
| `ValidationError` | 400 | Invalid request parameters (`details` has Zod issues) |
| `ConflictError` | 409 | Generic state conflict (no specific license-state code) |
| `RateLimitError` | 429 | Too many requests (use `retry_after`) |
| `LicentricError` | 5xx + other | Base class — also raised for unknown statuses |

### License-state exceptions (raised by body `code`, regardless of HTTP status)

These subclass `LicentricError` directly — NOT the HTTP-status classes — so
catching `ConflictError` will NOT catch them. Catch by license-state class
explicitly:

| Exception | API code | When |
|-----------|----------|------|
| `LicenseRevokedError` | `LICENSE_REVOKED` | License has been revoked (permanent) |
| `LicenseSuspendedError` | `LICENSE_SUSPENDED` | License is suspended (e.g. payment failure — temporary) |
| `LicenseExpiredError` | `LICENSE_EXPIRED` | License past expiration and policy disallows continued use |
| `LicenseBannedError` | `LICENSE_BANNED` | License banned for abuse (terminal — no recovery) |

### Recovery classification — what to retry vs fail immediately

| Strategy | Exceptions |
|----------|------------|
| **Grace-eligible** (use cached validation if available, retry on next request) | Network errors, 5xx `LicentricError`, `RateLimitError` after backoff completes |
| **Fail immediately** (do not retry; surface to end user) | `LicenseRevokedError`, `LicenseSuspendedError`, `LicenseExpiredError`, `LicenseBannedError`, `AuthenticationError`, `NotFoundError`, `ValidationError` |

## API Reference

### `Licentric(api_key, base_url?, timeout?)`

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `api_key` | `str` | — | Your API key (`lk_live_...` or `lk_test_...`) |
| `base_url` | `str` | `https://licentric.com` | API base URL |
| `timeout` | `float` | `30.0` | Request timeout in seconds |

### Methods

| Method | Auth | Description |
|--------|------|-------------|
| `validate(key, fingerprint?, entitlements?)` | License key | Validate a license key |
| `activate(key, fingerprint, name?, platform?)` | License key | Activate a machine |
| `deactivate(machine_id, key)` | License key | Remove a machine activation |
| `heartbeat(machine_id, key)` | License key | Send a machine heartbeat |
| `checkout(license_id, fingerprint, ttl?)` | API key | Check out an offline license file |
| `close()` | — | Close the HTTP client |

The client supports context manager usage:

```python
with Licentric(api_key="lk_live_...") as client:
    result = client.validate(key="LIC-XXXX-XXXX-XXXX")
```

## Machine Fingerprinting

The `fingerprint()` function generates a stable SHA-256 hash identifying the current machine:

| Platform | Source |
|----------|--------|
| macOS | Hardware UUID via `ioreg` |
| Linux | `/etc/machine-id` or Docker container ID |
| Windows | Product UUID via `wmic` |
| Fallback | `hostname:username` hash |

## Type Safety

This package includes a `py.typed` marker ([PEP 561](https://peps.python.org/pep-0561/)) and ships with full type annotations. Works with mypy, pyright, and other type checkers out of the box.

## Documentation

- [Getting Started Guide](https://licentric.com/docs/getting-started/quickstart)
- [Python SDK Reference](https://licentric.com/docs/sdks/python)
- [API Reference](https://licentric.com/api/reference)

## License

MIT
