Metadata-Version: 2.4
Name: pylicensify
Version: 1.0.0
Summary: Client SDK for PyLicense — verify software license keys with tamper-proof, signed responses.
Author: PyLicense
License: MIT
Project-URL: Homepage, https://license.pyobfuscate.com
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 [PyLicense](https://license.pyobfuscate.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
```

## Quick start

Get your **public key** from the dashboard → *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

```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).
