feat: add jiangchang platform kit skeleton
Add a shared SDK scaffold for entitlement checks and a reusable Gitea release workflow template to standardize skill packaging and publishing across projects.
This commit is contained in:
9
sdk/jiangchang_skill_core/__init__.py
Normal file
9
sdk/jiangchang_skill_core/__init__.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from .client import EntitlementClient
|
||||
from .guard import enforce_entitlement
|
||||
from .models import EntitlementResult
|
||||
|
||||
__all__ = [
|
||||
"EntitlementClient",
|
||||
"EntitlementResult",
|
||||
"enforce_entitlement",
|
||||
]
|
||||
63
sdk/jiangchang_skill_core/client.py
Normal file
63
sdk/jiangchang_skill_core/client.py
Normal file
@@ -0,0 +1,63 @@
|
||||
import os
|
||||
from typing import Any
|
||||
|
||||
import requests
|
||||
|
||||
from .errors import EntitlementServiceError
|
||||
from .models import EntitlementResult
|
||||
|
||||
|
||||
class EntitlementClient:
|
||||
def __init__(
|
||||
self,
|
||||
base_url: str | None = None,
|
||||
api_key: str | None = None,
|
||||
timeout_seconds: int | None = None,
|
||||
) -> None:
|
||||
self.base_url = (base_url or os.getenv("JIANGCHANG_AUTH_BASE_URL", "")).rstrip("/")
|
||||
self.api_key = api_key or os.getenv("JIANGCHANG_AUTH_API_KEY", "")
|
||||
self.timeout_seconds = timeout_seconds or int(
|
||||
os.getenv("JIANGCHANG_AUTH_TIMEOUT_SECONDS", "5")
|
||||
)
|
||||
if not self.base_url:
|
||||
raise EntitlementServiceError("missing JIANGCHANG_AUTH_BASE_URL")
|
||||
|
||||
def check_entitlement(
|
||||
self,
|
||||
user_id: str,
|
||||
skill_slug: str,
|
||||
trace_id: str = "",
|
||||
context: dict[str, Any] | None = None,
|
||||
) -> EntitlementResult:
|
||||
url = f"{self.base_url}/api/entitlements/check"
|
||||
payload = {
|
||||
"user_id": user_id,
|
||||
"skill_slug": skill_slug,
|
||||
"trace_id": trace_id,
|
||||
"context": context or {},
|
||||
}
|
||||
headers = {"Content-Type": "application/json"}
|
||||
if self.api_key:
|
||||
headers["Authorization"] = f"Bearer {self.api_key}"
|
||||
try:
|
||||
res = requests.post(
|
||||
url,
|
||||
json=payload,
|
||||
headers=headers,
|
||||
timeout=self.timeout_seconds,
|
||||
)
|
||||
except requests.RequestException as exc:
|
||||
raise EntitlementServiceError(f"entitlement request failed: {exc}") from exc
|
||||
|
||||
if res.status_code != 200:
|
||||
raise EntitlementServiceError(f"entitlement http status {res.status_code}")
|
||||
try:
|
||||
body = res.json()
|
||||
except ValueError as exc:
|
||||
raise EntitlementServiceError("entitlement response is not json") from exc
|
||||
|
||||
data = body.get("data") or {}
|
||||
allow = bool(data.get("allow", False))
|
||||
reason = str(data.get("reason") or body.get("msg") or "")
|
||||
expire_at = str(data.get("expire_at") or "")
|
||||
return EntitlementResult(allow=allow, reason=reason, expire_at=expire_at, raw=body)
|
||||
10
sdk/jiangchang_skill_core/errors.py
Normal file
10
sdk/jiangchang_skill_core/errors.py
Normal file
@@ -0,0 +1,10 @@
|
||||
class EntitlementError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class EntitlementDeniedError(EntitlementError):
|
||||
pass
|
||||
|
||||
|
||||
class EntitlementServiceError(EntitlementError):
|
||||
pass
|
||||
22
sdk/jiangchang_skill_core/guard.py
Normal file
22
sdk/jiangchang_skill_core/guard.py
Normal file
@@ -0,0 +1,22 @@
|
||||
from .client import EntitlementClient
|
||||
from .errors import EntitlementDeniedError
|
||||
from .models import EntitlementResult
|
||||
|
||||
|
||||
def enforce_entitlement(
|
||||
user_id: str,
|
||||
skill_slug: str,
|
||||
trace_id: str = "",
|
||||
context: dict | None = None,
|
||||
client: EntitlementClient | None = None,
|
||||
) -> EntitlementResult:
|
||||
c = client or EntitlementClient()
|
||||
result = c.check_entitlement(
|
||||
user_id=user_id,
|
||||
skill_slug=skill_slug,
|
||||
trace_id=trace_id,
|
||||
context=context or {},
|
||||
)
|
||||
if not result.allow:
|
||||
raise EntitlementDeniedError(result.reason or "skill not purchased or expired")
|
||||
return result
|
||||
10
sdk/jiangchang_skill_core/models.py
Normal file
10
sdk/jiangchang_skill_core/models.py
Normal file
@@ -0,0 +1,10 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
|
||||
@dataclass
|
||||
class EntitlementResult:
|
||||
allow: bool
|
||||
reason: str = ""
|
||||
expire_at: str = ""
|
||||
raw: dict[str, Any] | None = None
|
||||
19
sdk/pyproject.toml
Normal file
19
sdk/pyproject.toml
Normal file
@@ -0,0 +1,19 @@
|
||||
[build-system]
|
||||
requires = ["setuptools>=68", "wheel"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "jiangchang-skill-core"
|
||||
version = "0.1.0"
|
||||
description = "Common entitlement SDK for Jiangchang skills"
|
||||
requires-python = ">=3.10"
|
||||
dependencies = [
|
||||
"requests>=2.31.0",
|
||||
]
|
||||
|
||||
[tool.setuptools]
|
||||
package-dir = {"" = "."}
|
||||
|
||||
[tool.setuptools.packages.find]
|
||||
where = ["."]
|
||||
include = ["jiangchang_skill_core*"]
|
||||
Reference in New Issue
Block a user