Skip to content

Integración del SDK

Integración del SDK de Python

Añade licenciamiento de Keyright a una aplicación Python: instala el paquete keyright, verifica licencias sin conexión, activa en línea, restringe características según los derechos, y gestiona pruebas y máquinas aisladas (air-gapped) — fallando cerrado por diseño.

Este es el recorrido del cliente de Python — desde pip install hasta una aplicación lista para distribuir que desbloquea características de pago contra una clave de licencia real. El paquete keyright verifica los mismos exactos formatos de licencia y lease que el SDK de .NET, de modo que una línea de productos multilenguaje puede compartir un tenant de Keyright y una clave pública. Esta página dedica su profundidad al código de cliente de Python; los bloques de código usan la superficie real del SDK, así que cópialos tal cual.

Antes de empezar: la configuración del proveedor

Los pasos del proveedor de una sola vez son independientes del lenguaje y se cubren por completo en otro sitio — hazlos una vez, luego vuelve aquí para el cliente:

  • Crea tu producto y anota su slug (el identificador que tu aplicación envía como product).
  • Copia tu clave pública de firma — una cadena SubjectPublicKeyInfo en base64. No es un secreto; se distribuye dentro de tu aplicación y solo puede verificar firmas, nunca acuñarlas. Toda la seguridad proviene de que la clave privada permanezca cifrada en el servidor.
  • Define niveles y derechos — cada nivel lleva un número de puestos y una plantilla de banderas nombradas y límites numéricos que se incorporan a cada licencia.

Paso a paso, con las pantallas del panel y las llamadas de API equivalentes, está en Primeros pasos y el recorrido de .NET. Todo lo de abajo asume que tienes tu slug de producto y tu clave pública en base64 a mano.

Cómo funciona

Emites claves de licencia del lado del servidor en Keyright; cada una se firma con la clave privada RSA de tu tenant, que nunca sale del servidor. Tu aplicación Python embebe solo la clave pública correspondiente y verifica licencias sin conexión contra ella, de modo que una comprobación de licencia rutinaria no necesita red. Para la aplicación de puestos y la revocación también activas en línea: la aplicación intercambia una clave de licencia por un lease firmado de vida corta vinculado a la máquina, y luego sigue funcionando sin conexión hasta que ese lease caduca. Cualquier cosa que no cuadre — firma errónea, producto equivocado, caducidad, revocación, un reloj retrasado — se resuelve a la edición Free en lugar de lanzar una excepción. Falla cerrado.

Instala el SDK

El paquete está en PyPI. Necesita Python 3.8+ e incorpora cryptography para la verificación RSA:

pip install "keyright>=1.1.4"

Todo lo que necesitas se exporta desde el paquete de nivel superior:

from keyright import (
    KeyrightClient,
    KeyrightOptions,
    LicenseInfo,
    LicenseStatus,
    Edition,
    MachineFingerprint,
)

Inicializa el cliente

Construye un KeyrightClient en el arranque con la fábrica estática initialize. Valida las opciones y carga la clave pública por adelantado (lanzando ValueError si la clave falta), así que constrúyelo una vez y reutilízalo:

from keyright import KeyrightClient, KeyrightOptions

license = KeyrightClient.initialize(KeyrightOptions(
    product="acme-app",                              # must match the product slug you issue keys for
    public_key_base64="MIIBIjANBgkq...",             # the base64 public key from your dashboard
    service_url="https://keyright.delta1labs.com",   # omit if you ship offline license files only
))

Solo product y public_key_base64 son significativos para configurar; service_url solo se requiere para la activación en línea. KeyrightOptions acepta varios argumentos de palabra clave opcionales que controlan de dónde se lee una licencia y cómo se valida:

KeyrightOptions(
    product="acme-app",
    public_key_base64="MIIBIjANBgkq...",
    service_url="https://keyright.delta1labs.com",

    # Explicit license sources (highest precedence first)
    license_string=None,                 # a license JSON string passed directly
    license_file_path=None,              # a path to a license file
    env_var_name="KEYRIGHT_LICENSE",     # env var naming a license file path (this is the default)

    # Keep an old key valid during a rotation window
    additional_public_keys_base64=["<previous public key>"],

    # Offline revocation — ship a signed revocation list with your build
    revocation_list_json=None,
    revocation_list_path=None,

    # Node-lock tolerance: how many soft-component changes to allow (default 1)
    node_lock_tolerance=1,
))

El SDK resuelve una licencia a partir de varias fuentes, en orden de precedencia (de mayor a menor): un license_string explícito, luego un license_file_path explícito, luego el archivo nombrado por la variable de entorno KEYRIGHT_LICENSE, luego config_license_path, luego la ruta de datos de aplicación del SO ({LocalAppData}/Keyright/{product}/license.json), y finalmente el lease de activación en caché. Normalmente no defines ninguna de estas — la activación en línea escribe la caché del lease por ti.

Restringe características sin conexión

Llama a validate(). Resuelve la mejor licencia de las fuentes de arriba, verifica la firma RSA, la coincidencia de producto, el bloqueo por equipo, la caducidad, cualquier lista de revocación distribuida y el estado de prueba/manipulación-de-reloj — todo sin conexión — y nunca lanza una excepción. Ante cualquier fallo se resuelve a un LicenseInfo en la edición Free que lleva el motivo.

validate() devuelve una tupla (LicenseInfo, source) — siempre desempaquétala. source es una cadena de diagnóstico corta ("lease", "environment", "default", "none", …) que te dice qué fuente ganó:

from keyright import Edition

info, source = license.validate()   # -> (LicenseInfo, str) — unpack the tuple

if info.is_paid:                     # True for any edition above Free
    enable_paid_features()

if info.edition == Edition.ENTERPRISE:
    enable_enterprise_features()

Si solo quieres el LicenseInfo y no te importa de qué fuente vino, validate_info() devuelve solo el primer elemento:

info = license.validate_info()       # equivalent to license.validate()[0]

Restringe según los derechos

Prefiere restringir características individuales según los derechos en lugar de la edición, para que cambiar la plantilla de un nivel no implique distribuir código nuevo. LicenseInfo.entitlements es un EntitlementSet con dos accesores tipados que fallan cerrado:

info = license.validate_info()

# Boolean flag — a missing or non-truthy flag reads as False
if info.entitlements.is_enabled("export"):
    show_export_command()

# Numeric limit — pass the fail-closed fallback yourself; a missing or
# unparseable value returns your fallback, and "unlimited" returns a very large int
max_projects = info.entitlements.get_limit("max-projects", fallback=1)
if current_project_count >= max_projects:
    prompt_to_upgrade()

Para una única comprobación rápida el cliente también expone atajos de conveniencia que validan en el acto — license.is_enabled("export"), license.get_limit("max-projects", 1) y license.edition(). Cada uno vuelve a ejecutar validate() internamente, así que cuando compruebes varios derechos a la vez, valida una vez y reutiliza el EntitlementSet:

info = license.validate_info()
ents = info.entitlements
can_export = ents.is_enabled("export")
max_seats = ents.get_limit("max-seats", 1)

Tanto is_enabled como get_limit fallan cerrado: una bandera ausente está deshabilitada, un límite ausente o no analizable devuelve tu fallback. Combina una comprobación de edición con una comprobación de derecho cuando una característica pertenece a un nivel y a una bandera de plantilla, y ambas deben superarse.

Lee el estado para diagnóstico

Para averiguar por qué se resolvió de la forma en que lo hizo una comprobación — para un diálogo de “Register” o logging — lee los campos en snake_case de LicenseInfo:

info, source = license.validate()

print(info.status)          # LicenseStatus enum: VALID, NO_LICENSE, SIGNATURE_INVALID,
                            #   EXPIRED, MACHINE_MISMATCH, REVOKED, CLOCK_TAMPERED, MALFORMED
print(info.message)         # human-facing explanation string
print(info.licensee)        # who the license was issued to
print(info.edition)         # Edition.FREE | LICENSED | ENTERPRISE
print(info.is_valid)        # True only when status == LicenseStatus.VALID
print(info.is_paid)         # True for any edition above Free
print(info.status_badge)    # e.g. "Enterprise", "Licensed Trial", "Free"
print(info.kind)            # LicenseKind.PERPETUAL | SUBSCRIPTION | TRIAL
print(info.is_trial)        # True for a trial license/lease
print(info.expiry_utc)      # datetime | None (None = perpetual)
print(info.days_remaining)  # int | None — e.g. 27 for a "27 days left" badge
print(info.trial_days)      # int | None — the trial's configured length
print(source)               # which source resolved it: "lease", "environment", ...

is_valid es la comprobación estricta (el estado es VALID); is_paid es la comprobación de “desbloquear características de pago” (edición por encima de Free). Una prueba válida es ambas.

Activa en línea con una clave de licencia

Cuando un cliente introduce una clave, llama a activate(key). Es síncrona. Hace POST de la clave más un id de máquina estable a tu servicio, que consume un puesto y devuelve un lease firmado de vida corta vinculado a esa máquina. El SDK verifica el lease contra tu clave pública embebida y lo almacena en caché localmente, de modo que cada validate() posterior tiene éxito sin red hasta que transcurre la ventana de gracia del lease.

activate no lanza excepción para las rutas de fallo ordinarias (clave errónea, límite de puestos, sin conexión, revocada) — devuelve un LicenseInfo que falla cerrado, exactamente como validate(). Devuelve el LicenseInfo directamente (no una tupla). Inspecciona el resultado:

info = license.activate(customer_entered_key)

if info.is_valid and info.is_paid:
    # Activated. The lease is cached; the app now works offline until it expires.
    show_licensed_ui(info.status_badge)     # e.g. "Enterprise" or "Enterprise Trial"
else:
    # Surface info.message; the app stays in Free mode.
    show_activation_error(info.message)      # "All seats for this license are in use.", etc.
  • Los puestos se aplican del lado del servidor. Activar más máquinas de las que la licencia permite devuelve un resultado de límite de puestos (info.status == LicenseStatus.NO_LICENSE, mensaje “All seats for this license are in use.”) y ningún lease. Reactivar una máquina que ya está vinculada es idempotente — no se consume un puesto extra.
  • Gracia sin conexión. Si el servicio es inalcanzable, activate recurre a cualquier lease en caché que siga siendo válido, de modo que una breve caída no deja fuera al usuario. Cuando el lease se acerca a su caducidad, la aplicación debe volver a alcanzar el servidor para refrescarlo.
  • La revocación surte efecto en el siguiente refresco — ver abajo.

activate lanza ValueError solo por errores de programación: un service_url ausente o una clave vacía.

Desactiva para liberar un puesto

Para liberar el puesto de esta máquina de modo que pueda reactivarse en otro lugar, llama a deactivate(key). Vincula el mismo id de máquina que usa activate y hace POST al servicio. A diferencia de activate, no tiene fallback sin conexión — si el servicio no puede alcanzarse el error se propaga, así que envuélvelo:

try:
    status = license.deactivate(license_key)   # -> "ok" | "not_found"
    if status == "ok":
        # The seat is freed and the locally cached lease has been deleted.
        show_free_ui()
except (RuntimeError, OSError) as ex:
    show_error(f"Could not reach the licensing service: {ex}")

Ante "ok" el SDK borra el lease en caché local para este producto, de modo que un validate() posterior vuelve a Free.

Máquinas aisladas (air-gapped): importa un lease sin conexión

Para una máquina que nunca puede alcanzar el servicio, un operador firma un lease sin conexión para su id de máquina (panel Licenses → offline lease, o POST /admin/licenses/{id}/offline-lease) y entrega el JSON por archivo. Para llevar el id de esa máquina al operador, léelo localmente:

from keyright import MachineFingerprint

machine_id = MachineFingerprint.current().to_bound_string()   # "KRM1:..." — hand this to the operator
print(machine_id)

Luego importa el lease entregado. import_offline_lease verifica firma, producto, máquina y no-caducado por la misma ruta que usa un lease en caché, lo almacena en caché localmente si tiene éxito, y lanza ValueError si el lease es inválido — así que manéjalo:

try:
    with open("acme.lease.json", encoding="utf-8") as f:
        info = license.import_offline_lease(f.read())
    # Cached. Subsequent validate() calls now succeed offline.
    show_licensed_ui(info.status_badge)
except ValueError as ex:
    # wrong signature / product / machine, or expired
    show_error(str(ex))

Bloqueo por equipo

El SDK deriva una huella digital de máquina estable a partir de un componente ancla (el GUID de máquina) más señales blandas (nombre de host, SO). node_lock_tolerance (por defecto 1) deja pasar un pequeño cambio de hardware o SO sin dejar fuera al usuario, mientras que el ancla siempre debe coincidir. La petición de activación envía este id de máquina para que los puestos se cuenten por dispositivo. MachineFingerprint.current().to_bound_string() da la cadena exacta a la que el servidor vincula un lease; MachineFingerprint.current().display_id da una forma corta y amigable para personas KR-XXXX-XXXX para conversaciones de soporte.

Pruebas

No hay un método de prueba en el SDK — una clave de prueba se obtiene fuera de banda llamando al endpoint público POST /v1/trial (que envía por email la clave al cliente), luego se pasa a activate() exactamente como una clave de pago. Una clave de prueba se activa por la misma ruta activate exacta que una de pago. Muestra el estado directamente desde LicenseInfo:

info = license.validate_info()
if info.is_trial:
    badge = info.status_badge          # e.g. "Enterprise Trial"
    left = info.days_remaining         # e.g. 27  -> "27 days left"
    show_trial_banner(badge, left)

La duración de una prueba se cuenta desde la primera activación en esta máquina (el SDK lo registra localmente). Cuando vence, validate() devuelve Free con status == LicenseStatus.EXPIRED y un mensaje que nombra la duración de la prueba. Una prueba se reemplaza sin fisuras cuando el cliente activa luego una clave de pago. Flujo completo: Pruebas gratuitas de autoservicio.

Solución de problemas

  • La activación dice que la clave no se reconoció. La causa más común es una discrepancia de producto: la clave se emitió para un producto (o tenant) distinto del slug product con el que tu cliente está configurado. El servidor acota la activación por producto, así que una clave válida para acme-app no activará un cliente inicializado con product="other-app". Confirma que el slug coincide con el producto bajo el que emitiste la clave.
  • Todo se lee como Free. Es el diseño — el SDK falla cerrado ante cualquier problema de verificación en lugar de lanzar una excepción. Lee info.status (NO_LICENSE, SIGNATURE_INVALID, EXPIRED, MACHINE_MISMATCH, REVOKED, CLOCK_TAMPERED, MALFORMED) e info.message para ver cuál es. Un SIGNATURE_INVALID casi siempre significa que el public_key_base64 embebido no coincide con el tenant que firmó la clave.
  • validate() “devuelve dos cosas”. Devuelve una tupla (LicenseInfo, source) — desempaquétala (info, source = license.validate()), o llama a validate_info() para solo el LicenseInfo.
  • Una licencia por tiempo limitado de repente no valida. Si el reloj del sistema se mueve hacia atrás más allá de la tolerancia (por defecto 24h) en una prueba o suscripción, el SDK lo trata como manipulación del reloj y devuelve el estado CLOCK_TAMPERED. No se queda pegado — ajusta la hora correcta y la validación se recupera en la siguiente llamada. Las licencias perpetuas nunca están sujetas a esta comprobación.
  • ValueError de activate o deactivate. Estas lanzan excepción solo por errores de programación — un service_url ausente o una clave vacía. Los fallos ordinarios de licenciamiento vuelven como un LicenseInfo, no como una excepción.

Véase también