Skip to content

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.

Copy the folder directly into your tool:

your_tool/
├── your_tool.py
└── vendor/
    └── cg_license/
        ├── __init__.py
        ├── license.py
        ├── storage.py
        ├── exceptions.py
        ├── api.py
        └── fingerprint.py

Then add to sys.path before importing:

import sys, os
vendor_path = os.path.join(os.path.dirname(__file__), "vendor")
sys.path.insert(0, vendor_path)
from cg_license import LicenseClient

Required dependencies

PyJWT >= 2.8.0
cryptography >= 40.0.0

Install alongside the SDK:

pip install "PyJWT>=2.8.0" "cryptography>=40.0.0"

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:

pip install "PyJWT>=2.8.0"

ModuleNotFoundError: No module named 'cryptography'

cryptography is missing:

pip install "cryptography>=40.0.0"

NetworkError on first activation

  • Check server_url has no trailing slash.
  • Verify the server is reachable: curl https://your-domain.com/health
  • Corporate proxies: set HTTPS_PROXY environment 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_salt is stored securely (not hardcoded in public repo, use build-time injection or obfuscation)
  • [ ] check_license() is called at startup, not lazily
  • [ ] MachineLimitError shows a helpful message with upgrade link
  • [ ] LicenseExpiredError shows 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