Metadata-Version: 2.4
Name: pylicensify
Version: 1.0.1
Summary: Client SDK for Licers — verify software license keys with tamper-proof, signed responses.
Author: Licers
License: MIT
Project-URL: Homepage, https://licers.com
Project-URL: Documentation, https://licers.com/docs
Keywords: license,licensing,activation,drm,software-licensing
Classifier: Programming Language :: Python :: 3
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Requires-Python: >=3.8
Description-Content-Type: text/markdown
Requires-Dist: requests>=2.25
Requires-Dist: cryptography>=41.0

# pylicensify

Client SDK for [Licers](https://licers.com) — verify software license keys
with **tamper-proof, signed responses**. A cracked "fake server" can't grant
access, because every response is verified against your public key.

```bash
pip install pylicensify
```

> **Brand vs. endpoint:** Licers lives at **[licers.com](https://licers.com)**
> (dashboard, docs, account). The SDK talks to the validation API at
> `https://license.pyobfuscate.com` by default — a stable, long-lived host your
> shipped apps rely on. Override it only for self-hosting (see below).

## Quick start

Get your **public key** from the [Licers dashboard](https://licers.com) →
*Integration* page (safe to embed in your distributed app; the private key never
leaves the server).

```python
from pylicensify import LicenseClient

client = LicenseClient(public_key="YOUR_PUBLIC_KEY")

result = client.validate("PY-XXXXXXXXXXXXXXXX")
if result:
    print("Licensed. Entitlements:", result.features)
    if result.feature("pro"):
        enable_pro_features()
else:
    print("Not licensed:", result.error)
    raise SystemExit(1)
```

`validate()` sends a nonce, verifies the Ed25519 signature, and rejects stale /
replayed responses automatically. It raises `SignatureError` on tampering and
`NetworkError` if the server is unreachable.

## Feature gating

```python
result = client.validate(key)
seats = result.feature("seats", 1)
if result.feature("pro"):
    ...
```

## Offline license files (air-gapped)

Issue a signed `.lic` file from a license row in the dashboard, ship it with the
app, and verify it locally with **zero network calls**:

```python
from pylicensify import verify_offline_license

data = verify_offline_license("YOUR_PUBLIC_KEY", "license.lic")
print("Valid for:", data.get("customer"), data.get("features"))
```

Checks signature, expiry, device binding, and OS lock.

## Floating / concurrent licenses

For keys with a concurrent-session limit — the SDK checks out a seat, heartbeats
in the background, and releases on exit:

```python
from pylicensify import LicenseClient, SeatUnavailable

client = LicenseClient(public_key="YOUR_PUBLIC_KEY")
try:
    with client.session("PY-XXXX...") as sess:
        print("Seat acquired:", sess.seats)   # {'used': 1, 'max': 5}
        run_app()                              # released automatically on exit
except SeatUnavailable:
    print("All seats are in use — try again later.")
```

## Update checks

```python
info = client.check_update("PY-XXXX...", current_version="1.0.0")
if info["update_available"]:
    print("New version:", info["version"], info["notes"])
    # info["download_url"] is a license-gated download link
```

## Self-hosted / custom domain

By default the SDK calls the managed cloud API at `https://license.pyobfuscate.com`
(your account and dashboard live at [licers.com](https://licers.com)). To target a
self-hosted deployment, pass `api_url`:

```python
client = LicenseClient(public_key="...", api_url="https://license.yourdomain.com")
```

## Hardware ID

By default the SDK derives a machine id from `uuid.getnode()`. Pass your own for
a stronger fingerprint:

```python
client.validate(key, hwid=my_custom_fingerprint())
```

## Errors

| Exception | Meaning |
|---|---|
| `InvalidLicense` | Server rejected the key (invalid/expired/revoked/blocked) |
| `SignatureError` | Signature/nonce/timestamp failed — forged or replayed |
| `NetworkError` | Couldn't reach the license server |
| `SeatUnavailable` | Floating license has no free seats |

All inherit from `LicenseError`.

## Hardening

Obfuscate your client so the check itself can't be trivially patched out —
e.g. with [PyObfuscate](https://pyobfuscate.com).
