v0.3.0 — entitlements catalog in PublicPoliciesResponse

Mirrors keysat 0014 + TS/Rust SDK 0.3.0. PublicPoliciesProduct
gains entitlements_catalog: list[EntitlementDef] with slug + name +
description. SDK consumers' in-app tier pickers can render display
names + tooltip descriptions instead of raw slugs. Empty list on
legacy products without a catalog. No breaking change.
This commit is contained in:
Keysat
2026-05-10 07:58:59 -05:00
parent 94654f6526
commit f2ea74d7e3
3 changed files with 28 additions and 2 deletions
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "keysat-licensing-client"
version = "0.2.0"
version = "0.3.0"
description = "Python client for Keysat — a self-hosted Bitcoin-paid software licensing server that runs on Start9. Verifies signed license keys offline and wraps the HTTP API for purchase, redemption, and revocation checks."
readme = "README.md"
requires-python = ">=3.10"
+3 -1
View File
@@ -67,6 +67,7 @@ try:
ValidateResponse,
ValidateOptions,
StartPurchaseOptions,
EntitlementDef,
PublicPolicy,
PublicPoliciesProduct,
PublicPoliciesResponse,
@@ -81,6 +82,7 @@ try:
"ValidateResponse",
"ValidateOptions",
"StartPurchaseOptions",
"EntitlementDef",
"PublicPolicy",
"PublicPoliciesProduct",
"PublicPoliciesResponse",
@@ -93,4 +95,4 @@ except ImportError:
# httpx not installed — that's fine, online client is optional.
pass
__version__ = "0.2.0"
__version__ = "0.3.0"
+24
View File
@@ -89,12 +89,27 @@ class PublicPolicy:
trial_days: int
@dataclass
class EntitlementDef:
"""One entry in a product's entitlements catalog (Keysat
migration 0014). Operator declares the closed list once per
product; policies pick from this list. Use ``name`` as the
human-readable label when rendering an in-app tier picker
(e.g. "AI summaries" instead of the raw ``ai_summaries`` slug).
"""
slug: str
name: str
description: str
@dataclass
class PublicPoliciesProduct:
slug: str
name: str
description: str
base_price_sats: int
entitlements_catalog: list[EntitlementDef]
@dataclass
@@ -277,12 +292,21 @@ class Client:
raw = self._get(f"/v1/products/{product_slug}/policies")
product = raw.get("product", {}) or {}
policies_raw = raw.get("policies") or []
catalog_raw = product.get("entitlements_catalog") or []
return PublicPoliciesResponse(
product=PublicPoliciesProduct(
slug=product.get("slug", ""),
name=product.get("name", ""),
description=product.get("description", "") or "",
base_price_sats=int(product.get("base_price_sats", 0)),
entitlements_catalog=[
EntitlementDef(
slug=c.get("slug", ""),
name=c.get("name", "") or c.get("slug", ""),
description=c.get("description", "") or "",
)
for c in catalog_raw
],
),
policies=[
PublicPolicy(