Python SDK¶
The CG Lounge License Server Python SDK handles license validation, activation, caching, and offline grace periods with a clean API surface.
Installation¶
Download the latest SDK from the home page and unzip into your project's vendor/ folder.
Required dependencies¶
Install alongside the SDK:
Basic Setup¶
from cg_license import LicenseClient
client = LicenseClient(
server_url="https://your-domain.com",
product_id="prod_abc123",
product_salt="your-product-salt",
)
That is the entire setup. No API keys in the client, no environment variables required.
Complete Integration Example¶
import sys
from cg_license import LicenseClient
from cg_license.exceptions import (
LicenseNotFoundError,
LicenseRevokedError,
LicenseExpiredError,
MachineLimitError,
NetworkError,
InvalidTokenError,
)
client = LicenseClient(
server_url="https://your-domain.com",
product_id="prod_abc123",
product_salt="your-product-salt",
)
def startup_license_check():
if client.check_license():
run_tool()
return
error = client.get_error()
if error is None:
run_tool()
return
if error.code == "degraded":
print(f"WARNING: {error.message}")
run_tool() # Still works in degraded mode
elif error.code == "not_activated":
license_key = prompt_user_for_key()
activate_license(license_key)
elif error.code == "revoked":
print("ERROR: Your license has been revoked. Contact support.")
sys.exit(1)
elif error.code == "expired":
print("ERROR: License has expired.")
sys.exit(1)
elif error.code == "machine_limit":
print("ERROR: Machine limit reached. Deactivate another machine or upgrade.")
sys.exit(1)
elif error.code == "network":
print(f"ERROR: Cannot reach license server and no cached token found. {error.message}")
sys.exit(1)
def activate_license(key: str):
try:
info = client.activate(key)
print(f"Activated. Expires: {info.get('expiresAt') or 'never'}")
print(f"Machines: {info.get('machinesUsed')}/{info.get('maxMachines')}")
except MachineLimitError as e:
print(f"Cannot activate: {e.message}")
except LicenseNotFoundError:
print("Invalid license key.")
def prompt_user_for_key() -> str:
return input("Enter your license key: ").strip()
if __name__ == "__main__":
startup_license_check()
API Reference¶
LicenseClient¶
LicenseClient(
server_url: str,
product_id: str,
product_salt: str,
storage_dir: str | None = None, # override default storage location
public_key: str | None = None, # RSA public key for token verification
)
Methods¶
| Method | Description | Returns |
|---|---|---|
check_license() |
Validate cached token or re-validate online. Primary entry point. | bool |
get_error() |
Get the last error from check_license(). |
LicenseError \| None |
get_status() |
Get current license status string (active, degraded, suspended, etc). | str \| None |
get_message() |
Get any message from the server (e.g., degraded warning). | str \| None |
get_license_info() |
Get current license metadata dict. | dict \| None |
activate(key) |
Activate a license key on this machine. Writes token to storage. | dict |
deactivate() |
Deactivate this machine and delete cached token. | bool |
send_heartbeat() |
Send heartbeat for floating license session. | bool |
check_license() return value¶
Returns True if the tool is licensed and can run (including degraded state). Returns False if not licensed. Call get_error() after a False return to inspect the reason.
activate() return value¶
{
"machinesUsed": int,
"maxMachines": int,
"expiresAt": str | None,
"status": str,
"variant": str | None,
"message": str | None,
}
get_license_info() return value¶
When called after check_license() or activate():
{
"license_key": str | None,
"product_id": str,
"status": str,
"variant": str | None,
"message": str | None,
"machines_used": int | None,
"max_machines": int | None,
"expires_at": str | None,
"license_expires_at": str | None,
"days_remaining": int | None,
}
Error Classes¶
from cg_license.exceptions import (
LicenseError, # base class: .message (str), .code (str)
LicenseNotFoundError, # code: "not_found"
LicenseRevokedError, # code: "revoked"
LicenseExpiredError, # code: "expired"
LicenseSuspendedError, # code: "suspended"
LicenseDegradedError, # code: "degraded"
MachineLimitError, # code: "machine_limit"
NetworkError, # code: "network"
InvalidTokenError, # code: "invalid_token"
NotActivatedError, # code: "not_activated"
)
All exceptions inherit from LicenseError and expose:
| Attribute | Type | Description |
|---|---|---|
.message |
str |
Human-readable error message |
.code |
str |
Machine-readable error code |
| Error | When raised |
|---|---|
LicenseNotFoundError |
Key does not exist on server |
LicenseRevokedError |
Creator revoked the license |
LicenseExpiredError |
License past expiry |
LicenseSuspendedError |
License is suspended |
LicenseDegradedError |
License in degraded state (tool still runs) |
MachineLimitError |
Too many machines activated |
NetworkError |
No network, timeout, or server error |
InvalidTokenError |
JWT signature invalid or corrupt |
NotActivatedError |
No license activated on this machine |
Offline Behavior¶
The SDK issues a JWT token on first successful validation. This token is cached locally and used for offline validation.
First use (online):
check_license() -> server validates -> JWT issued -> cached to disk
Subsequent uses (offline):
check_license() -> load cached JWT -> verify signature locally -> valid
After token expiry (offline):
Grace period starts (7 days)
After grace period ends:
check_license() returns False, get_error().code == "invalid_token"
Handling degraded state (e.g., server-side warning):
if client.check_license():
error = client.get_error()
if error and error.code == "degraded":
show_warning(error.message)
run_tool()
else:
error = client.get_error()
# handle error.code
Token Cache Locations¶
| Platform | Path |
|---|---|
| Windows | %APPDATA%\CGLicense\<product_id>\license.dat |
| macOS | ~/Library/Application Support/CGLicense/<product_id>/license.dat |
| Linux | ~/.config/CGLicense/<product_id>/license.dat |
Override with storage_dir:
client = LicenseClient(
server_url="...",
product_id="...",
product_salt="...",
storage_dir="/custom/path/to/storage",
)
Troubleshooting¶
ModuleNotFoundError: No module named 'cg_license'¶
The SDK is not on sys.path. If embedded, check that the vendor path is inserted before the import:
import sys, os
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "vendor"))
from cg_license import LicenseClient
Verify the vendor path is correct and points to the folder containing cg_license/.
ModuleNotFoundError: No module named 'jwt'¶
PyJWT is missing:
ModuleNotFoundError: No module named 'cryptography'¶
cryptography is missing:
NetworkError on first activation¶
- Check
server_urlhas no trailing slash. - Verify the server is reachable:
curl https://your-domain.com/health - Corporate proxies: set
HTTPS_PROXYenvironment variable.
InvalidTokenError on cached token¶
The cached token is corrupt or was written by a different product salt. Delete the cache file and re-activate:
| Platform | File to delete |
|---|---|
| Windows | %APPDATA%\CGLicense\<product_id>\license.dat |
| macOS | ~/Library/Application Support/CGLicense/<product_id>/license.dat |
| Linux | ~/.config/CGLicense/<product_id>/license.dat |
Production Checklist¶
- [ ]
product_saltis stored securely (not hardcoded in public repo, use build-time injection or obfuscation) - [ ]
check_license()is called at startup, not lazily - [ ]
MachineLimitErrorshows a helpful message with upgrade link - [ ]
LicenseExpiredErrorshows renewal link - [ ] Grace period behavior is documented for users
- [ ] Deactivation is accessible from your tool's UI (Help menu, etc.)
- [ ] Tested offline by disabling network and running for 30+ simulated days