Implement IronLicensing Python SDK
- LicenseClient for validation and activation - Feature checking with has_feature/require_feature - Trial management - Purchase/checkout flow - Offline caching with grace period - License change callbacks - Full async support with httpx - Comprehensive README
This commit is contained in:
parent
ba3065912a
commit
21cd3f004a
|
|
@ -0,0 +1,36 @@
|
|||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# Distribution / packaging
|
||||
dist/
|
||||
build/
|
||||
*.egg-info/
|
||||
*.egg
|
||||
|
||||
# Virtual environments
|
||||
.venv/
|
||||
venv/
|
||||
ENV/
|
||||
|
||||
# IDE
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Testing
|
||||
.pytest_cache/
|
||||
.coverage
|
||||
htmlcov/
|
||||
|
||||
# Type checking
|
||||
.mypy_cache/
|
||||
|
||||
# Local storage
|
||||
.ironlicensing/
|
||||
279
README.md
279
README.md
|
|
@ -1,2 +1,277 @@
|
|||
# ironlicensing-python
|
||||
IronLicensing SDK for Python - Software licensing and activation
|
||||
# IronLicensing SDK for Python
|
||||
|
||||
Software licensing and activation SDK for Python applications. Validate licenses, manage activations, check features, and handle trials.
|
||||
|
||||
[](https://pypi.org/project/ironlicensing/)
|
||||
[](https://pypi.org/project/ironlicensing/)
|
||||
[](https://opensource.org/licenses/MIT)
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
pip install ironlicensing
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Validate a License
|
||||
|
||||
```python
|
||||
import ironlicensing
|
||||
|
||||
# Initialize
|
||||
ironlicensing.init("pk_live_xxxxx", "my-app")
|
||||
|
||||
# Validate a license key
|
||||
result = ironlicensing.validate("IRON-XXXX-XXXX-XXXX-XXXX")
|
||||
|
||||
if result.valid:
|
||||
print("License is valid!")
|
||||
print(f"Expires: {result.license.expires_at}")
|
||||
```
|
||||
|
||||
### Activate a License
|
||||
|
||||
```python
|
||||
import ironlicensing
|
||||
|
||||
ironlicensing.init("pk_live_xxxxx", "my-app")
|
||||
|
||||
# Activate on this machine
|
||||
result = ironlicensing.activate("IRON-XXXX-XXXX-XXXX-XXXX")
|
||||
|
||||
if result.valid:
|
||||
print("License activated!")
|
||||
print(f"Activations: {len(result.activations)}")
|
||||
```
|
||||
|
||||
### Check Features
|
||||
|
||||
```python
|
||||
import ironlicensing
|
||||
from ironlicensing import LicenseRequiredError
|
||||
|
||||
ironlicensing.init("pk_live_xxxxx", "my-app")
|
||||
ironlicensing.validate("IRON-XXXX-XXXX-XXXX-XXXX")
|
||||
|
||||
# Check if feature is enabled
|
||||
if ironlicensing.has_feature("premium"):
|
||||
# Enable premium features
|
||||
pass
|
||||
|
||||
# Or require a feature (raises if not available)
|
||||
try:
|
||||
ironlicensing.require_feature("enterprise")
|
||||
# Enterprise code here
|
||||
except LicenseRequiredError:
|
||||
print("Enterprise license required")
|
||||
```
|
||||
|
||||
### Async Support
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
import ironlicensing
|
||||
|
||||
async def main():
|
||||
ironlicensing.init("pk_live_xxxxx", "my-app")
|
||||
|
||||
# Async validation
|
||||
result = await ironlicensing.validate_async("IRON-XXXX-XXXX-XXXX-XXXX")
|
||||
|
||||
# Async activation
|
||||
result = await ironlicensing.activate_async("IRON-XXXX-XXXX-XXXX-XXXX")
|
||||
|
||||
# Async trial
|
||||
result = await ironlicensing.start_trial_async("user@example.com")
|
||||
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
### Using the Client Directly
|
||||
|
||||
```python
|
||||
from ironlicensing import LicenseClient, LicenseOptions
|
||||
|
||||
client = LicenseClient(LicenseOptions(
|
||||
public_key="pk_live_xxxxx",
|
||||
product_slug="my-app",
|
||||
debug=True,
|
||||
))
|
||||
|
||||
# Validate
|
||||
result = client.validate("IRON-XXXX-XXXX-XXXX-XXXX")
|
||||
|
||||
# Check features
|
||||
if client.has_feature("premium"):
|
||||
pass
|
||||
|
||||
# Clean up
|
||||
client.close()
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
```python
|
||||
from ironlicensing import LicenseClient, LicenseOptions
|
||||
|
||||
client = LicenseClient(LicenseOptions(
|
||||
public_key="pk_live_xxxxx", # Required
|
||||
product_slug="my-app", # Required
|
||||
api_base_url="https://api.ironlicensing.com",
|
||||
debug=False,
|
||||
enable_offline_cache=True,
|
||||
cache_validation_minutes=60,
|
||||
offline_grace_days=7,
|
||||
storage_key_prefix="ironlicensing",
|
||||
))
|
||||
```
|
||||
|
||||
### Configuration Options
|
||||
|
||||
| Option | Type | Default | Description |
|
||||
|--------|------|---------|-------------|
|
||||
| `public_key` | str | required | Your public key (pk_live_xxx or pk_test_xxx) |
|
||||
| `product_slug` | str | required | Your product slug |
|
||||
| `api_base_url` | str | https://api.ironlicensing.com | API base URL |
|
||||
| `debug` | bool | False | Enable debug logging |
|
||||
| `enable_offline_cache` | bool | True | Cache validation results |
|
||||
| `cache_validation_minutes` | int | 60 | Cache validity in minutes |
|
||||
| `offline_grace_days` | int | 7 | Offline grace period in days |
|
||||
|
||||
## License Status
|
||||
|
||||
```python
|
||||
LicenseStatus = Literal[
|
||||
"valid", # License is valid
|
||||
"expired", # License has expired
|
||||
"suspended", # License is suspended
|
||||
"revoked", # License is revoked
|
||||
"invalid", # License key is invalid
|
||||
"trial", # Trial license
|
||||
"trial_expired", # Trial has expired
|
||||
"not_activated", # No license activated
|
||||
"unknown", # Unknown status
|
||||
]
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
### Check Features
|
||||
|
||||
```python
|
||||
# Check if feature is enabled
|
||||
if client.has_feature("premium"):
|
||||
enable_premium_features()
|
||||
|
||||
# Get feature details
|
||||
feature = client.get_feature("max-users")
|
||||
if feature and feature.enabled:
|
||||
print(f"Max users: {feature.metadata.get('limit')}")
|
||||
|
||||
# Get all features
|
||||
for f in client.get_features():
|
||||
print(f"{f.name}: {'enabled' if f.enabled else 'disabled'}")
|
||||
```
|
||||
|
||||
### Require Features
|
||||
|
||||
```python
|
||||
from ironlicensing import LicenseRequiredError
|
||||
|
||||
try:
|
||||
client.require_feature("enterprise")
|
||||
# This code only runs if feature is available
|
||||
except LicenseRequiredError:
|
||||
show_upgrade_dialog()
|
||||
```
|
||||
|
||||
## Trial Management
|
||||
|
||||
```python
|
||||
# Start a trial
|
||||
result = ironlicensing.start_trial("user@example.com")
|
||||
|
||||
if result.valid:
|
||||
print("Trial started!")
|
||||
print(f"Expires: {result.license.expires_at}")
|
||||
|
||||
# Check if trial
|
||||
if ironlicensing.is_trial():
|
||||
show_trial_banner(ironlicensing.get_license().expires_at)
|
||||
```
|
||||
|
||||
## Purchase Flow
|
||||
|
||||
```python
|
||||
# Get available tiers
|
||||
tiers = ironlicensing.get_tiers()
|
||||
|
||||
for tier in tiers:
|
||||
print(f"{tier.name}: {tier.price} {tier.currency}/{tier.billing_period}")
|
||||
|
||||
# Start checkout
|
||||
checkout = ironlicensing.start_purchase("tier-pro", "user@example.com")
|
||||
|
||||
if checkout.success:
|
||||
# Open checkout URL
|
||||
print(f"Checkout URL: {checkout.checkout_url}")
|
||||
```
|
||||
|
||||
## Deactivation
|
||||
|
||||
```python
|
||||
# Deactivate on this machine
|
||||
if ironlicensing.deactivate():
|
||||
print("License deactivated")
|
||||
```
|
||||
|
||||
## License Change Events
|
||||
|
||||
```python
|
||||
def on_license_change(license):
|
||||
if license:
|
||||
print(f"License updated: {license.status}")
|
||||
else:
|
||||
print("License removed")
|
||||
|
||||
# Listen for license changes
|
||||
unsubscribe = client.on_license_change(on_license_change)
|
||||
|
||||
# Later: unsubscribe
|
||||
unsubscribe()
|
||||
```
|
||||
|
||||
## Offline Support
|
||||
|
||||
The SDK automatically caches validation results and supports offline usage:
|
||||
|
||||
```python
|
||||
client = LicenseClient(LicenseOptions(
|
||||
public_key="pk_live_xxxxx",
|
||||
product_slug="my-app",
|
||||
enable_offline_cache=True,
|
||||
cache_validation_minutes=60, # Use cache for 60 minutes
|
||||
offline_grace_days=7, # Allow 7 days offline
|
||||
))
|
||||
|
||||
# This will use cache if available
|
||||
result = client.validate()
|
||||
|
||||
if result.cached:
|
||||
print("Using cached validation")
|
||||
```
|
||||
|
||||
## Requirements
|
||||
|
||||
- Python 3.8+
|
||||
- httpx
|
||||
|
||||
## Links
|
||||
|
||||
- [Documentation](https://www.ironlicensing.com/docs)
|
||||
- [Dashboard](https://www.ironlicensing.com)
|
||||
|
||||
## License
|
||||
|
||||
MIT License - see [LICENSE](LICENSE) for details.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,38 @@
|
|||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "ironlicensing"
|
||||
version = "1.0.0"
|
||||
description = "Software licensing and activation SDK for Python"
|
||||
readme = "README.md"
|
||||
license = "MIT"
|
||||
authors = [
|
||||
{ name = "IronServices", email = "support@ironservices.com" }
|
||||
]
|
||||
keywords = ["licensing", "activation", "license-key", "software-licensing", "feature-flags"]
|
||||
classifiers = [
|
||||
"Development Status :: 4 - Beta",
|
||||
"Intended Audience :: Developers",
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.8",
|
||||
"Programming Language :: Python :: 3.9",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
"Typing :: Typed",
|
||||
]
|
||||
requires-python = ">=3.8"
|
||||
dependencies = [
|
||||
"httpx>=0.24.0",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
Homepage = "https://www.ironlicensing.com"
|
||||
Documentation = "https://www.ironlicensing.com/docs"
|
||||
Repository = "https://github.com/IronServices/ironlicensing-python"
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
packages = ["src/ironlicensing"]
|
||||
|
|
@ -0,0 +1,202 @@
|
|||
"""IronLicensing SDK for Python.
|
||||
|
||||
Software licensing and activation SDK.
|
||||
|
||||
Example:
|
||||
>>> import ironlicensing
|
||||
>>>
|
||||
>>> # Initialize
|
||||
>>> ironlicensing.init("pk_live_xxxxx", "my-app")
|
||||
>>>
|
||||
>>> # Validate a license
|
||||
>>> result = ironlicensing.validate("IRON-XXXX-XXXX-XXXX-XXXX")
|
||||
>>> if result.valid:
|
||||
... print("License is valid!")
|
||||
>>>
|
||||
>>> # Check features
|
||||
>>> if ironlicensing.has_feature("premium"):
|
||||
... print("Premium features enabled")
|
||||
"""
|
||||
|
||||
from .types import (
|
||||
LicenseStatus,
|
||||
LicenseType,
|
||||
LicenseOptions,
|
||||
Feature,
|
||||
License,
|
||||
Activation,
|
||||
LicenseResult,
|
||||
CheckoutResult,
|
||||
ProductTier,
|
||||
)
|
||||
from .client import LicenseClient, LicenseRequiredError
|
||||
|
||||
__all__ = [
|
||||
# Types
|
||||
"LicenseStatus",
|
||||
"LicenseType",
|
||||
"LicenseOptions",
|
||||
"Feature",
|
||||
"License",
|
||||
"Activation",
|
||||
"LicenseResult",
|
||||
"CheckoutResult",
|
||||
"ProductTier",
|
||||
# Client
|
||||
"LicenseClient",
|
||||
"LicenseRequiredError",
|
||||
# Global functions
|
||||
"init",
|
||||
"validate",
|
||||
"validate_async",
|
||||
"activate",
|
||||
"activate_async",
|
||||
"deactivate",
|
||||
"deactivate_async",
|
||||
"start_trial",
|
||||
"start_trial_async",
|
||||
"has_feature",
|
||||
"require_feature",
|
||||
"get_feature",
|
||||
"get_features",
|
||||
"get_license",
|
||||
"get_status",
|
||||
"is_licensed",
|
||||
"is_trial",
|
||||
"get_tiers",
|
||||
"get_tiers_async",
|
||||
"start_purchase",
|
||||
"start_purchase_async",
|
||||
]
|
||||
|
||||
_global_client: LicenseClient | None = None
|
||||
|
||||
|
||||
def init(
|
||||
public_key: str,
|
||||
product_slug: str,
|
||||
api_base_url: str = "https://api.ironlicensing.com",
|
||||
debug: bool = False,
|
||||
enable_offline_cache: bool = True,
|
||||
cache_validation_minutes: int = 60,
|
||||
offline_grace_days: int = 7,
|
||||
) -> LicenseClient:
|
||||
"""Initialize the global license client."""
|
||||
global _global_client
|
||||
options = LicenseOptions(
|
||||
public_key=public_key,
|
||||
product_slug=product_slug,
|
||||
api_base_url=api_base_url,
|
||||
debug=debug,
|
||||
enable_offline_cache=enable_offline_cache,
|
||||
cache_validation_minutes=cache_validation_minutes,
|
||||
offline_grace_days=offline_grace_days,
|
||||
)
|
||||
_global_client = LicenseClient(options)
|
||||
return _global_client
|
||||
|
||||
|
||||
def _get_client() -> LicenseClient:
|
||||
if _global_client is None:
|
||||
raise RuntimeError("IronLicensing not initialized. Call init() first.")
|
||||
return _global_client
|
||||
|
||||
|
||||
def validate(license_key: str | None = None) -> LicenseResult:
|
||||
"""Validate a license key (sync)."""
|
||||
return _get_client().validate(license_key)
|
||||
|
||||
|
||||
async def validate_async(license_key: str | None = None) -> LicenseResult:
|
||||
"""Validate a license key (async)."""
|
||||
return await _get_client().validate_async(license_key)
|
||||
|
||||
|
||||
def activate(license_key: str, machine_name: str | None = None) -> LicenseResult:
|
||||
"""Activate a license (sync)."""
|
||||
return _get_client().activate(license_key, machine_name)
|
||||
|
||||
|
||||
async def activate_async(license_key: str, machine_name: str | None = None) -> LicenseResult:
|
||||
"""Activate a license (async)."""
|
||||
return await _get_client().activate_async(license_key, machine_name)
|
||||
|
||||
|
||||
def deactivate() -> bool:
|
||||
"""Deactivate the current license (sync)."""
|
||||
return _get_client().deactivate()
|
||||
|
||||
|
||||
async def deactivate_async() -> bool:
|
||||
"""Deactivate the current license (async)."""
|
||||
return await _get_client().deactivate_async()
|
||||
|
||||
|
||||
def start_trial(email: str) -> LicenseResult:
|
||||
"""Start a trial (sync)."""
|
||||
return _get_client().start_trial(email)
|
||||
|
||||
|
||||
async def start_trial_async(email: str) -> LicenseResult:
|
||||
"""Start a trial (async)."""
|
||||
return await _get_client().start_trial_async(email)
|
||||
|
||||
|
||||
def has_feature(feature_key: str) -> bool:
|
||||
"""Check if a feature is enabled."""
|
||||
return _get_client().has_feature(feature_key)
|
||||
|
||||
|
||||
def require_feature(feature_key: str) -> None:
|
||||
"""Require a feature, raises LicenseRequiredError if not available."""
|
||||
_get_client().require_feature(feature_key)
|
||||
|
||||
|
||||
def get_feature(feature_key: str) -> Feature | None:
|
||||
"""Get a feature by key."""
|
||||
return _get_client().get_feature(feature_key)
|
||||
|
||||
|
||||
def get_features() -> list[Feature]:
|
||||
"""Get all features."""
|
||||
return _get_client().get_features()
|
||||
|
||||
|
||||
def get_license() -> License | None:
|
||||
"""Get the current license."""
|
||||
return _get_client().license
|
||||
|
||||
|
||||
def get_status() -> LicenseStatus:
|
||||
"""Get the current license status."""
|
||||
return _get_client().status
|
||||
|
||||
|
||||
def is_licensed() -> bool:
|
||||
"""Check if the license is valid."""
|
||||
return _get_client().is_licensed
|
||||
|
||||
|
||||
def is_trial() -> bool:
|
||||
"""Check if the license is a trial."""
|
||||
return _get_client().is_trial
|
||||
|
||||
|
||||
def get_tiers() -> list[ProductTier]:
|
||||
"""Get product tiers (sync)."""
|
||||
return _get_client().get_tiers()
|
||||
|
||||
|
||||
async def get_tiers_async() -> list[ProductTier]:
|
||||
"""Get product tiers (async)."""
|
||||
return await _get_client().get_tiers_async()
|
||||
|
||||
|
||||
def start_purchase(tier_id: str, email: str) -> CheckoutResult:
|
||||
"""Start a purchase checkout (sync)."""
|
||||
return _get_client().start_purchase(tier_id, email)
|
||||
|
||||
|
||||
async def start_purchase_async(tier_id: str, email: str) -> CheckoutResult:
|
||||
"""Start a purchase checkout (async)."""
|
||||
return await _get_client().start_purchase_async(tier_id, email)
|
||||
|
|
@ -0,0 +1,160 @@
|
|||
"""License caching for IronLicensing SDK."""
|
||||
|
||||
import json
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from .types import License, LicenseResult, Feature
|
||||
|
||||
|
||||
class LicenseCache:
|
||||
"""Cache for license validation results."""
|
||||
|
||||
def __init__(self, key_prefix: str = "ironlicensing", debug: bool = False):
|
||||
self._key_prefix = key_prefix
|
||||
self._debug = debug
|
||||
self._storage_path = Path.home() / ".ironlicensing"
|
||||
|
||||
def _key(self, name: str) -> str:
|
||||
return f"{self._key_prefix}_{name}"
|
||||
|
||||
def _file_path(self, name: str) -> Path:
|
||||
return self._storage_path / f"{self._key(name)}.json"
|
||||
|
||||
def _read(self, name: str) -> dict[str, Any] | None:
|
||||
try:
|
||||
path = self._file_path(name)
|
||||
if path.exists():
|
||||
return json.loads(path.read_text())
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
def _write(self, name: str, data: dict[str, Any]) -> None:
|
||||
try:
|
||||
self._storage_path.mkdir(parents=True, exist_ok=True)
|
||||
self._file_path(name).write_text(json.dumps(data))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _delete(self, name: str) -> None:
|
||||
try:
|
||||
path = self._file_path(name)
|
||||
if path.exists():
|
||||
path.unlink()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _serialize_license(self, license: License) -> dict[str, Any]:
|
||||
return {
|
||||
"id": license.id,
|
||||
"key": license.key,
|
||||
"status": license.status,
|
||||
"type": license.type,
|
||||
"features": [
|
||||
{
|
||||
"key": f.key,
|
||||
"name": f.name,
|
||||
"enabled": f.enabled,
|
||||
"description": f.description,
|
||||
"metadata": f.metadata,
|
||||
}
|
||||
for f in license.features
|
||||
],
|
||||
"max_activations": license.max_activations,
|
||||
"current_activations": license.current_activations,
|
||||
"created_at": license.created_at.isoformat(),
|
||||
"email": license.email,
|
||||
"name": license.name,
|
||||
"company": license.company,
|
||||
"expires_at": license.expires_at.isoformat() if license.expires_at else None,
|
||||
"last_validated_at": license.last_validated_at.isoformat() if license.last_validated_at else None,
|
||||
"metadata": license.metadata,
|
||||
}
|
||||
|
||||
def _deserialize_license(self, data: dict[str, Any]) -> License:
|
||||
features = [
|
||||
Feature(
|
||||
key=f["key"],
|
||||
name=f["name"],
|
||||
enabled=f["enabled"],
|
||||
description=f.get("description"),
|
||||
metadata=f.get("metadata"),
|
||||
)
|
||||
for f in data.get("features", [])
|
||||
]
|
||||
|
||||
return License(
|
||||
id=data["id"],
|
||||
key=data["key"],
|
||||
status=data["status"],
|
||||
type=data["type"],
|
||||
features=features,
|
||||
max_activations=data["max_activations"],
|
||||
current_activations=data["current_activations"],
|
||||
created_at=datetime.fromisoformat(data["created_at"]),
|
||||
email=data.get("email"),
|
||||
name=data.get("name"),
|
||||
company=data.get("company"),
|
||||
expires_at=datetime.fromisoformat(data["expires_at"]) if data.get("expires_at") else None,
|
||||
last_validated_at=datetime.fromisoformat(data["last_validated_at"])
|
||||
if data.get("last_validated_at")
|
||||
else None,
|
||||
metadata=data.get("metadata"),
|
||||
)
|
||||
|
||||
def get_license(self) -> License | None:
|
||||
"""Get the cached license."""
|
||||
data = self._read("license")
|
||||
if data:
|
||||
return self._deserialize_license(data)
|
||||
return None
|
||||
|
||||
def set_license(self, license: License) -> None:
|
||||
"""Set the cached license."""
|
||||
self._write("license", self._serialize_license(license))
|
||||
|
||||
def get_validation_result(self) -> LicenseResult | None:
|
||||
"""Get the cached validation result."""
|
||||
data = self._read("validation")
|
||||
if data and data.get("license"):
|
||||
return LicenseResult(
|
||||
valid=data["valid"],
|
||||
license=self._deserialize_license(data["license"]),
|
||||
cached=True,
|
||||
)
|
||||
return None
|
||||
|
||||
def set_validation_result(self, result: LicenseResult, timestamp: datetime) -> None:
|
||||
"""Set the cached validation result."""
|
||||
data: dict[str, Any] = {"valid": result.valid}
|
||||
if result.license:
|
||||
data["license"] = self._serialize_license(result.license)
|
||||
self._write("validation", data)
|
||||
self._write("validation_time", {"timestamp": timestamp.isoformat()})
|
||||
|
||||
def get_validation_time(self) -> datetime | None:
|
||||
"""Get the validation timestamp."""
|
||||
data = self._read("validation_time")
|
||||
if data and data.get("timestamp"):
|
||||
return datetime.fromisoformat(data["timestamp"])
|
||||
return None
|
||||
|
||||
def get_license_key(self) -> str | None:
|
||||
"""Get the stored license key."""
|
||||
data = self._read("license_key")
|
||||
if data:
|
||||
return data.get("key")
|
||||
return None
|
||||
|
||||
def set_license_key(self, key: str) -> None:
|
||||
"""Set the license key."""
|
||||
self._write("license_key", {"key": key})
|
||||
|
||||
def clear(self) -> None:
|
||||
"""Clear all cached data."""
|
||||
self._delete("license")
|
||||
self._delete("validation")
|
||||
self._delete("validation_time")
|
||||
self._delete("license_key")
|
||||
|
|
@ -0,0 +1,288 @@
|
|||
"""Main client for IronLicensing SDK."""
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Callable
|
||||
|
||||
from .types import (
|
||||
LicenseOptions,
|
||||
License,
|
||||
LicenseResult,
|
||||
LicenseStatus,
|
||||
CheckoutResult,
|
||||
ProductTier,
|
||||
Feature,
|
||||
)
|
||||
from .transport import Transport
|
||||
from .cache import LicenseCache
|
||||
|
||||
|
||||
class LicenseRequiredError(Exception):
|
||||
"""Raised when a license or feature is required but not available."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class LicenseClient:
|
||||
"""IronLicensing client for license validation and activation."""
|
||||
|
||||
def __init__(self, options: LicenseOptions):
|
||||
self._options = options
|
||||
self._transport = Transport(
|
||||
options.api_base_url,
|
||||
options.public_key,
|
||||
options.product_slug,
|
||||
options.debug,
|
||||
)
|
||||
self._cache = LicenseCache(options.storage_key_prefix, options.debug)
|
||||
self._current_license: License | None = None
|
||||
self._on_license_change_callbacks: list[Callable[[License | None], None]] = []
|
||||
|
||||
# Load cached license
|
||||
self._current_license = self._cache.get_license()
|
||||
|
||||
if options.debug:
|
||||
print("[IronLicensing] Client initialized")
|
||||
|
||||
def validate(self, license_key: str | None = None) -> LicenseResult:
|
||||
"""Validate a license key (sync)."""
|
||||
key = license_key or self._cache.get_license_key()
|
||||
if not key:
|
||||
return LicenseResult(valid=False, error="No license key provided")
|
||||
|
||||
# Check cache first
|
||||
if self._options.enable_offline_cache:
|
||||
cached = self._get_cached_validation()
|
||||
if cached:
|
||||
return LicenseResult(
|
||||
valid=cached.valid,
|
||||
license=cached.license,
|
||||
cached=True,
|
||||
)
|
||||
|
||||
# Validate online
|
||||
result = self._transport.validate(key)
|
||||
|
||||
if result.valid and result.license:
|
||||
self._set_license(result.license, key)
|
||||
self._cache.set_validation_result(result, datetime.now())
|
||||
|
||||
return result
|
||||
|
||||
async def validate_async(self, license_key: str | None = None) -> LicenseResult:
|
||||
"""Validate a license key (async)."""
|
||||
key = license_key or self._cache.get_license_key()
|
||||
if not key:
|
||||
return LicenseResult(valid=False, error="No license key provided")
|
||||
|
||||
# Check cache first
|
||||
if self._options.enable_offline_cache:
|
||||
cached = self._get_cached_validation()
|
||||
if cached:
|
||||
return LicenseResult(
|
||||
valid=cached.valid,
|
||||
license=cached.license,
|
||||
cached=True,
|
||||
)
|
||||
|
||||
# Validate online
|
||||
result = await self._transport.validate_async(key)
|
||||
|
||||
if result.valid and result.license:
|
||||
self._set_license(result.license, key)
|
||||
self._cache.set_validation_result(result, datetime.now())
|
||||
|
||||
return result
|
||||
|
||||
def activate(self, license_key: str, machine_name: str | None = None) -> LicenseResult:
|
||||
"""Activate a license (sync)."""
|
||||
result = self._transport.activate(license_key, machine_name)
|
||||
|
||||
if result.valid and result.license:
|
||||
self._set_license(result.license, license_key)
|
||||
self._cache.set_validation_result(result, datetime.now())
|
||||
|
||||
return result
|
||||
|
||||
async def activate_async(self, license_key: str, machine_name: str | None = None) -> LicenseResult:
|
||||
"""Activate a license (async)."""
|
||||
result = await self._transport.activate_async(license_key, machine_name)
|
||||
|
||||
if result.valid and result.license:
|
||||
self._set_license(result.license, license_key)
|
||||
self._cache.set_validation_result(result, datetime.now())
|
||||
|
||||
return result
|
||||
|
||||
def deactivate(self) -> bool:
|
||||
"""Deactivate the current license (sync)."""
|
||||
key = self._cache.get_license_key()
|
||||
if not key:
|
||||
return False
|
||||
|
||||
success = self._transport.deactivate(key)
|
||||
if success:
|
||||
self._set_license(None, None)
|
||||
self._cache.clear()
|
||||
|
||||
return success
|
||||
|
||||
async def deactivate_async(self) -> bool:
|
||||
"""Deactivate the current license (async)."""
|
||||
key = self._cache.get_license_key()
|
||||
if not key:
|
||||
return False
|
||||
|
||||
success = await self._transport.deactivate_async(key)
|
||||
if success:
|
||||
self._set_license(None, None)
|
||||
self._cache.clear()
|
||||
|
||||
return success
|
||||
|
||||
def start_trial(self, email: str) -> LicenseResult:
|
||||
"""Start a trial (sync)."""
|
||||
result = self._transport.start_trial(email)
|
||||
|
||||
if result.valid and result.license:
|
||||
self._set_license(result.license, result.license.key)
|
||||
self._cache.set_validation_result(result, datetime.now())
|
||||
|
||||
return result
|
||||
|
||||
async def start_trial_async(self, email: str) -> LicenseResult:
|
||||
"""Start a trial (async)."""
|
||||
result = await self._transport.start_trial_async(email)
|
||||
|
||||
if result.valid and result.license:
|
||||
self._set_license(result.license, result.license.key)
|
||||
self._cache.set_validation_result(result, datetime.now())
|
||||
|
||||
return result
|
||||
|
||||
def has_feature(self, feature_key: str) -> bool:
|
||||
"""Check if a feature is enabled."""
|
||||
if not self._current_license:
|
||||
return False
|
||||
for feature in self._current_license.features:
|
||||
if feature.key == feature_key and feature.enabled:
|
||||
return True
|
||||
return False
|
||||
|
||||
def require_feature(self, feature_key: str) -> None:
|
||||
"""Require a feature, raises LicenseRequiredError if not available."""
|
||||
if not self.has_feature(feature_key):
|
||||
raise LicenseRequiredError(f"Feature '{feature_key}' requires a valid license")
|
||||
|
||||
def get_feature(self, feature_key: str) -> Feature | None:
|
||||
"""Get a feature by key."""
|
||||
if not self._current_license:
|
||||
return None
|
||||
for feature in self._current_license.features:
|
||||
if feature.key == feature_key:
|
||||
return feature
|
||||
return None
|
||||
|
||||
def get_features(self) -> list[Feature]:
|
||||
"""Get all features."""
|
||||
if not self._current_license:
|
||||
return []
|
||||
return self._current_license.features
|
||||
|
||||
@property
|
||||
def license(self) -> License | None:
|
||||
"""Get the current license."""
|
||||
return self._current_license
|
||||
|
||||
@property
|
||||
def status(self) -> LicenseStatus:
|
||||
"""Get the current license status."""
|
||||
if self._current_license:
|
||||
return self._current_license.status
|
||||
return "not_activated"
|
||||
|
||||
@property
|
||||
def is_licensed(self) -> bool:
|
||||
"""Check if the license is valid."""
|
||||
return self._current_license is not None and self._current_license.status in ("valid", "trial")
|
||||
|
||||
@property
|
||||
def is_trial(self) -> bool:
|
||||
"""Check if the license is a trial."""
|
||||
return (
|
||||
self._current_license is not None
|
||||
and (self._current_license.status == "trial" or self._current_license.type == "trial")
|
||||
)
|
||||
|
||||
@property
|
||||
def expires_at(self) -> datetime | None:
|
||||
"""Get the license expiration date."""
|
||||
if self._current_license:
|
||||
return self._current_license.expires_at
|
||||
return None
|
||||
|
||||
def get_tiers(self) -> list[ProductTier]:
|
||||
"""Get product tiers (sync)."""
|
||||
return self._transport.get_tiers()
|
||||
|
||||
async def get_tiers_async(self) -> list[ProductTier]:
|
||||
"""Get product tiers (async)."""
|
||||
return await self._transport.get_tiers_async()
|
||||
|
||||
def start_purchase(self, tier_id: str, email: str) -> CheckoutResult:
|
||||
"""Start a purchase checkout (sync)."""
|
||||
return self._transport.start_checkout(tier_id, email)
|
||||
|
||||
async def start_purchase_async(self, tier_id: str, email: str) -> CheckoutResult:
|
||||
"""Start a purchase checkout (async)."""
|
||||
return await self._transport.start_checkout_async(tier_id, email)
|
||||
|
||||
def on_license_change(self, callback: Callable[[License | None], None]) -> Callable[[], None]:
|
||||
"""Register a callback for license changes."""
|
||||
self._on_license_change_callbacks.append(callback)
|
||||
|
||||
def unsubscribe() -> None:
|
||||
if callback in self._on_license_change_callbacks:
|
||||
self._on_license_change_callbacks.remove(callback)
|
||||
|
||||
return unsubscribe
|
||||
|
||||
def _set_license(self, license: License | None, key: str | None) -> None:
|
||||
self._current_license = license
|
||||
|
||||
if license:
|
||||
self._cache.set_license(license)
|
||||
if key:
|
||||
self._cache.set_license_key(key)
|
||||
|
||||
for callback in self._on_license_change_callbacks:
|
||||
try:
|
||||
callback(license)
|
||||
except Exception as e:
|
||||
if self._options.debug:
|
||||
print(f"[IronLicensing] Error in license change callback: {e}")
|
||||
|
||||
def _get_cached_validation(self) -> LicenseResult | None:
|
||||
cached = self._cache.get_validation_result()
|
||||
time = self._cache.get_validation_time()
|
||||
|
||||
if not cached or not time:
|
||||
return None
|
||||
|
||||
cache_age = datetime.now() - time
|
||||
max_age = timedelta(minutes=self._options.cache_validation_minutes)
|
||||
|
||||
if cache_age <= max_age:
|
||||
return cached
|
||||
|
||||
# Check offline grace period
|
||||
grace_period = timedelta(days=self._options.offline_grace_days)
|
||||
if cache_age <= grace_period and cached.valid:
|
||||
if self._options.debug:
|
||||
print("[IronLicensing] Using offline grace period")
|
||||
return cached
|
||||
|
||||
return None
|
||||
|
||||
def close(self) -> None:
|
||||
"""Close the client."""
|
||||
self._transport.close()
|
||||
|
|
@ -0,0 +1,440 @@
|
|||
"""HTTP transport for IronLicensing SDK."""
|
||||
|
||||
import platform
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
import httpx
|
||||
|
||||
from .types import (
|
||||
License,
|
||||
LicenseResult,
|
||||
CheckoutResult,
|
||||
ProductTier,
|
||||
Activation,
|
||||
Feature,
|
||||
)
|
||||
|
||||
|
||||
class Transport:
|
||||
"""HTTP transport for communicating with the IronLicensing API."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
base_url: str,
|
||||
public_key: str,
|
||||
product_slug: str,
|
||||
debug: bool,
|
||||
):
|
||||
self._base_url = base_url
|
||||
self._public_key = public_key
|
||||
self._product_slug = product_slug
|
||||
self._debug = debug
|
||||
self._sync_client = httpx.Client(timeout=30.0)
|
||||
self._async_client: httpx.AsyncClient | None = None
|
||||
self._machine_id = self._get_machine_id()
|
||||
|
||||
def _log(self, message: str) -> None:
|
||||
if self._debug:
|
||||
print(f"[IronLicensing] {message}")
|
||||
|
||||
def _get_headers(self) -> dict[str, str]:
|
||||
return {
|
||||
"Content-Type": "application/json",
|
||||
"X-Public-Key": self._public_key,
|
||||
"X-Product-Slug": self._product_slug,
|
||||
}
|
||||
|
||||
def _get_machine_id(self) -> str:
|
||||
"""Get or generate a persistent machine ID."""
|
||||
storage_path = Path.home() / ".ironlicensing" / "machine_id"
|
||||
try:
|
||||
if storage_path.exists():
|
||||
return storage_path.read_text().strip()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
machine_id = str(uuid.uuid4())
|
||||
try:
|
||||
storage_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
storage_path.write_text(machine_id)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return machine_id
|
||||
|
||||
def _parse_license(self, data: dict) -> License:
|
||||
features = [
|
||||
Feature(
|
||||
key=f["key"],
|
||||
name=f["name"],
|
||||
enabled=f.get("enabled", True),
|
||||
description=f.get("description"),
|
||||
metadata=f.get("metadata"),
|
||||
)
|
||||
for f in data.get("features", [])
|
||||
]
|
||||
|
||||
return License(
|
||||
id=data["id"],
|
||||
key=data["key"],
|
||||
status=data["status"],
|
||||
type=data["type"],
|
||||
features=features,
|
||||
max_activations=data.get("maxActivations", 1),
|
||||
current_activations=data.get("currentActivations", 0),
|
||||
created_at=datetime.fromisoformat(data["createdAt"].replace("Z", "+00:00")),
|
||||
email=data.get("email"),
|
||||
name=data.get("name"),
|
||||
company=data.get("company"),
|
||||
expires_at=datetime.fromisoformat(data["expiresAt"].replace("Z", "+00:00"))
|
||||
if data.get("expiresAt")
|
||||
else None,
|
||||
last_validated_at=datetime.fromisoformat(data["lastValidatedAt"].replace("Z", "+00:00"))
|
||||
if data.get("lastValidatedAt")
|
||||
else None,
|
||||
metadata=data.get("metadata"),
|
||||
)
|
||||
|
||||
def _parse_activation(self, data: dict) -> Activation:
|
||||
return Activation(
|
||||
id=data["id"],
|
||||
machine_id=data["machineId"],
|
||||
activated_at=datetime.fromisoformat(data["activatedAt"].replace("Z", "+00:00")),
|
||||
last_seen_at=datetime.fromisoformat(data["lastSeenAt"].replace("Z", "+00:00")),
|
||||
machine_name=data.get("machineName"),
|
||||
platform=data.get("platform"),
|
||||
)
|
||||
|
||||
def validate(self, license_key: str) -> LicenseResult:
|
||||
"""Validate a license key (sync)."""
|
||||
self._log(f"Validating license: {license_key[:10]}...")
|
||||
|
||||
try:
|
||||
response = self._sync_client.post(
|
||||
f"{self._base_url}/api/v1/validate",
|
||||
headers=self._get_headers(),
|
||||
json={
|
||||
"licenseKey": license_key,
|
||||
"machineId": self._machine_id,
|
||||
},
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
return LicenseResult(
|
||||
valid=data.get("valid", False),
|
||||
license=self._parse_license(data["license"]) if data.get("license") else None,
|
||||
activations=[self._parse_activation(a) for a in data.get("activations", [])],
|
||||
)
|
||||
else:
|
||||
error_data = response.json()
|
||||
return LicenseResult(valid=False, error=error_data.get("error", f"HTTP {response.status_code}"))
|
||||
|
||||
except Exception as e:
|
||||
return LicenseResult(valid=False, error=str(e))
|
||||
|
||||
async def validate_async(self, license_key: str) -> LicenseResult:
|
||||
"""Validate a license key (async)."""
|
||||
self._log(f"Validating license: {license_key[:10]}...")
|
||||
|
||||
if self._async_client is None:
|
||||
self._async_client = httpx.AsyncClient(timeout=30.0)
|
||||
|
||||
try:
|
||||
response = await self._async_client.post(
|
||||
f"{self._base_url}/api/v1/validate",
|
||||
headers=self._get_headers(),
|
||||
json={
|
||||
"licenseKey": license_key,
|
||||
"machineId": self._machine_id,
|
||||
},
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
return LicenseResult(
|
||||
valid=data.get("valid", False),
|
||||
license=self._parse_license(data["license"]) if data.get("license") else None,
|
||||
activations=[self._parse_activation(a) for a in data.get("activations", [])],
|
||||
)
|
||||
else:
|
||||
error_data = response.json()
|
||||
return LicenseResult(valid=False, error=error_data.get("error", f"HTTP {response.status_code}"))
|
||||
|
||||
except Exception as e:
|
||||
return LicenseResult(valid=False, error=str(e))
|
||||
|
||||
def activate(self, license_key: str, machine_name: str | None = None) -> LicenseResult:
|
||||
"""Activate a license (sync)."""
|
||||
self._log(f"Activating license: {license_key[:10]}...")
|
||||
|
||||
try:
|
||||
response = self._sync_client.post(
|
||||
f"{self._base_url}/api/v1/activate",
|
||||
headers=self._get_headers(),
|
||||
json={
|
||||
"licenseKey": license_key,
|
||||
"machineId": self._machine_id,
|
||||
"machineName": machine_name or platform.node(),
|
||||
"platform": platform.system(),
|
||||
},
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
return LicenseResult(
|
||||
valid=data.get("valid", False),
|
||||
license=self._parse_license(data["license"]) if data.get("license") else None,
|
||||
activations=[self._parse_activation(a) for a in data.get("activations", [])],
|
||||
)
|
||||
else:
|
||||
error_data = response.json()
|
||||
return LicenseResult(valid=False, error=error_data.get("error", f"HTTP {response.status_code}"))
|
||||
|
||||
except Exception as e:
|
||||
return LicenseResult(valid=False, error=str(e))
|
||||
|
||||
async def activate_async(self, license_key: str, machine_name: str | None = None) -> LicenseResult:
|
||||
"""Activate a license (async)."""
|
||||
self._log(f"Activating license: {license_key[:10]}...")
|
||||
|
||||
if self._async_client is None:
|
||||
self._async_client = httpx.AsyncClient(timeout=30.0)
|
||||
|
||||
try:
|
||||
response = await self._async_client.post(
|
||||
f"{self._base_url}/api/v1/activate",
|
||||
headers=self._get_headers(),
|
||||
json={
|
||||
"licenseKey": license_key,
|
||||
"machineId": self._machine_id,
|
||||
"machineName": machine_name or platform.node(),
|
||||
"platform": platform.system(),
|
||||
},
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
return LicenseResult(
|
||||
valid=data.get("valid", False),
|
||||
license=self._parse_license(data["license"]) if data.get("license") else None,
|
||||
activations=[self._parse_activation(a) for a in data.get("activations", [])],
|
||||
)
|
||||
else:
|
||||
error_data = response.json()
|
||||
return LicenseResult(valid=False, error=error_data.get("error", f"HTTP {response.status_code}"))
|
||||
|
||||
except Exception as e:
|
||||
return LicenseResult(valid=False, error=str(e))
|
||||
|
||||
def deactivate(self, license_key: str) -> bool:
|
||||
"""Deactivate a license (sync)."""
|
||||
try:
|
||||
response = self._sync_client.post(
|
||||
f"{self._base_url}/api/v1/deactivate",
|
||||
headers=self._get_headers(),
|
||||
json={
|
||||
"licenseKey": license_key,
|
||||
"machineId": self._machine_id,
|
||||
},
|
||||
)
|
||||
return response.status_code == 200
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
async def deactivate_async(self, license_key: str) -> bool:
|
||||
"""Deactivate a license (async)."""
|
||||
if self._async_client is None:
|
||||
self._async_client = httpx.AsyncClient(timeout=30.0)
|
||||
|
||||
try:
|
||||
response = await self._async_client.post(
|
||||
f"{self._base_url}/api/v1/deactivate",
|
||||
headers=self._get_headers(),
|
||||
json={
|
||||
"licenseKey": license_key,
|
||||
"machineId": self._machine_id,
|
||||
},
|
||||
)
|
||||
return response.status_code == 200
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def start_trial(self, email: str) -> LicenseResult:
|
||||
"""Start a trial (sync)."""
|
||||
self._log(f"Starting trial for: {email}")
|
||||
|
||||
try:
|
||||
response = self._sync_client.post(
|
||||
f"{self._base_url}/api/v1/trial",
|
||||
headers=self._get_headers(),
|
||||
json={
|
||||
"email": email,
|
||||
"machineId": self._machine_id,
|
||||
},
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
return LicenseResult(
|
||||
valid=data.get("valid", False),
|
||||
license=self._parse_license(data["license"]) if data.get("license") else None,
|
||||
)
|
||||
else:
|
||||
error_data = response.json()
|
||||
return LicenseResult(valid=False, error=error_data.get("error", f"HTTP {response.status_code}"))
|
||||
|
||||
except Exception as e:
|
||||
return LicenseResult(valid=False, error=str(e))
|
||||
|
||||
async def start_trial_async(self, email: str) -> LicenseResult:
|
||||
"""Start a trial (async)."""
|
||||
self._log(f"Starting trial for: {email}")
|
||||
|
||||
if self._async_client is None:
|
||||
self._async_client = httpx.AsyncClient(timeout=30.0)
|
||||
|
||||
try:
|
||||
response = await self._async_client.post(
|
||||
f"{self._base_url}/api/v1/trial",
|
||||
headers=self._get_headers(),
|
||||
json={
|
||||
"email": email,
|
||||
"machineId": self._machine_id,
|
||||
},
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
return LicenseResult(
|
||||
valid=data.get("valid", False),
|
||||
license=self._parse_license(data["license"]) if data.get("license") else None,
|
||||
)
|
||||
else:
|
||||
error_data = response.json()
|
||||
return LicenseResult(valid=False, error=error_data.get("error", f"HTTP {response.status_code}"))
|
||||
|
||||
except Exception as e:
|
||||
return LicenseResult(valid=False, error=str(e))
|
||||
|
||||
def _parse_tier(self, data: dict) -> ProductTier:
|
||||
features = [
|
||||
Feature(
|
||||
key=f["key"],
|
||||
name=f["name"],
|
||||
enabled=f.get("enabled", True),
|
||||
description=f.get("description"),
|
||||
metadata=f.get("metadata"),
|
||||
)
|
||||
for f in data.get("features", [])
|
||||
]
|
||||
|
||||
return ProductTier(
|
||||
id=data["id"],
|
||||
slug=data["slug"],
|
||||
name=data["name"],
|
||||
price=data["price"],
|
||||
currency=data["currency"],
|
||||
features=features,
|
||||
description=data.get("description"),
|
||||
billing_period=data.get("billingPeriod"),
|
||||
)
|
||||
|
||||
def get_tiers(self) -> list[ProductTier]:
|
||||
"""Get product tiers (sync)."""
|
||||
try:
|
||||
response = self._sync_client.get(
|
||||
f"{self._base_url}/api/v1/tiers",
|
||||
headers=self._get_headers(),
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
return [self._parse_tier(t) for t in data.get("tiers", [])]
|
||||
return []
|
||||
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
async def get_tiers_async(self) -> list[ProductTier]:
|
||||
"""Get product tiers (async)."""
|
||||
if self._async_client is None:
|
||||
self._async_client = httpx.AsyncClient(timeout=30.0)
|
||||
|
||||
try:
|
||||
response = await self._async_client.get(
|
||||
f"{self._base_url}/api/v1/tiers",
|
||||
headers=self._get_headers(),
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
return [self._parse_tier(t) for t in data.get("tiers", [])]
|
||||
return []
|
||||
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
def start_checkout(self, tier_id: str, email: str) -> CheckoutResult:
|
||||
"""Start a checkout session (sync)."""
|
||||
try:
|
||||
response = self._sync_client.post(
|
||||
f"{self._base_url}/api/v1/checkout",
|
||||
headers=self._get_headers(),
|
||||
json={
|
||||
"tierId": tier_id,
|
||||
"email": email,
|
||||
},
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
return CheckoutResult(
|
||||
success=True,
|
||||
checkout_url=data.get("checkoutUrl"),
|
||||
session_id=data.get("sessionId"),
|
||||
)
|
||||
else:
|
||||
error_data = response.json()
|
||||
return CheckoutResult(success=False, error=error_data.get("error", f"HTTP {response.status_code}"))
|
||||
|
||||
except Exception as e:
|
||||
return CheckoutResult(success=False, error=str(e))
|
||||
|
||||
async def start_checkout_async(self, tier_id: str, email: str) -> CheckoutResult:
|
||||
"""Start a checkout session (async)."""
|
||||
if self._async_client is None:
|
||||
self._async_client = httpx.AsyncClient(timeout=30.0)
|
||||
|
||||
try:
|
||||
response = await self._async_client.post(
|
||||
f"{self._base_url}/api/v1/checkout",
|
||||
headers=self._get_headers(),
|
||||
json={
|
||||
"tierId": tier_id,
|
||||
"email": email,
|
||||
},
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
return CheckoutResult(
|
||||
success=True,
|
||||
checkout_url=data.get("checkoutUrl"),
|
||||
session_id=data.get("sessionId"),
|
||||
)
|
||||
else:
|
||||
error_data = response.json()
|
||||
return CheckoutResult(success=False, error=error_data.get("error", f"HTTP {response.status_code}"))
|
||||
|
||||
except Exception as e:
|
||||
return CheckoutResult(success=False, error=str(e))
|
||||
|
||||
def close(self) -> None:
|
||||
"""Close the transport."""
|
||||
self._sync_client.close()
|
||||
if self._async_client:
|
||||
# Note: async client should be closed in an async context
|
||||
pass
|
||||
|
|
@ -0,0 +1,113 @@
|
|||
"""Type definitions for IronLicensing SDK."""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
from typing import Any, Literal
|
||||
|
||||
LicenseStatus = Literal[
|
||||
"valid",
|
||||
"expired",
|
||||
"suspended",
|
||||
"revoked",
|
||||
"invalid",
|
||||
"trial",
|
||||
"trial_expired",
|
||||
"not_activated",
|
||||
"unknown",
|
||||
]
|
||||
|
||||
LicenseType = Literal["perpetual", "subscription", "trial"]
|
||||
|
||||
BillingPeriod = Literal["monthly", "yearly", "lifetime"]
|
||||
|
||||
|
||||
@dataclass
|
||||
class LicenseOptions:
|
||||
"""Configuration options for the LicenseClient."""
|
||||
|
||||
public_key: str
|
||||
product_slug: str
|
||||
api_base_url: str = "https://api.ironlicensing.com"
|
||||
debug: bool = False
|
||||
enable_offline_cache: bool = True
|
||||
cache_validation_minutes: int = 60
|
||||
offline_grace_days: int = 7
|
||||
storage_key_prefix: str = "ironlicensing"
|
||||
|
||||
|
||||
@dataclass
|
||||
class Feature:
|
||||
"""A feature in a license."""
|
||||
|
||||
key: str
|
||||
name: str
|
||||
enabled: bool
|
||||
description: str | None = None
|
||||
metadata: dict[str, Any] | None = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class License:
|
||||
"""License information."""
|
||||
|
||||
id: str
|
||||
key: str
|
||||
status: LicenseStatus
|
||||
type: LicenseType
|
||||
features: list[Feature]
|
||||
max_activations: int
|
||||
current_activations: int
|
||||
created_at: datetime
|
||||
email: str | None = None
|
||||
name: str | None = None
|
||||
company: str | None = None
|
||||
expires_at: datetime | None = None
|
||||
last_validated_at: datetime | None = None
|
||||
metadata: dict[str, Any] | None = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class Activation:
|
||||
"""Activation information."""
|
||||
|
||||
id: str
|
||||
machine_id: str
|
||||
activated_at: datetime
|
||||
last_seen_at: datetime
|
||||
machine_name: str | None = None
|
||||
platform: str | None = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class LicenseResult:
|
||||
"""Result of license validation."""
|
||||
|
||||
valid: bool
|
||||
license: License | None = None
|
||||
activations: list[Activation] | None = None
|
||||
error: str | None = None
|
||||
cached: bool = False
|
||||
|
||||
|
||||
@dataclass
|
||||
class CheckoutResult:
|
||||
"""Result of checkout."""
|
||||
|
||||
success: bool
|
||||
checkout_url: str | None = None
|
||||
session_id: str | None = None
|
||||
error: str | None = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class ProductTier:
|
||||
"""Product tier for purchase."""
|
||||
|
||||
id: str
|
||||
slug: str
|
||||
name: str
|
||||
price: float
|
||||
currency: str
|
||||
features: list[Feature] = field(default_factory=list)
|
||||
description: str | None = None
|
||||
billing_period: BillingPeriod | None = None
|
||||
Loading…
Reference in New Issue