← Files JinkōARCHIVED FILE
skills/jinko-protocol/scripts/create_protocol_design.py
6.47 KB · Oct 2, 2026 · 00:29 UTC
#!/usr/bin/env python3
"""Create a Jinkō protocol design from arm overrides.
Dry-run by default. Pass --apply to create the ProtocolDesign project item.
"""
from __future__ import annotations
import argparse
import json
import sys
from pathlib import Path
from typing import Any
try:
from dotenv import load_dotenv
except ImportError: # pragma: no cover - depends on local environment
load_dotenv = None
DEFAULT_ARM_NAMES = ["iv_low_dose", "po_mid_dose", "iv_high_dose"]
def load_env() -> None:
if load_dotenv is not None:
load_dotenv()
def load_sdk():
try:
from jinko import JinkoClient
from jinko.exceptions import JinkoError
except ImportError:
print(
"Cannot import jinko. Install the SDK: pip install jinko-sdk",
file=sys.stderr,
)
return None
return JinkoClient, JinkoError
def split_csv(value: str) -> list[str]:
return [item.strip() for item in value.split(",") if item.strip()]
def build_arms(
*,
dose_key: str,
route_key: str,
doses: list[str],
routes: list[str],
arm_names: list[str],
control_arm: str,
) -> list[dict[str, Any]]:
if not (len(doses) == len(routes) == len(arm_names)):
raise ValueError("doses, routes, and arm names must have the same length")
if control_arm not in arm_names:
raise ValueError("control arm must be one of the arm names")
arms = []
for name, dose, route in zip(arm_names, doses, routes):
arms.append({
"armControl": None if name == control_arm else control_arm,
"armIsActive": True,
"armName": name,
"armOverrides": [
{"key": dose_key, "formula": dose},
{"key": route_key, "formula": route},
],
"armWeight": 1,
})
return arms
def override_keys(arms: list[dict[str, Any]]) -> set[str]:
return {
override["key"]
for arm in arms
for override in arm.get("armOverrides", [])
if isinstance(override, dict) and "key" in override
}
def validate_model_override_keys(model: Any, keys: set[str]) -> None:
component_ids = {component.id for component in model.components.list()}
missing = sorted(keys.difference(component_ids))
if missing:
raise ValueError(
"Override keys are not present as model components: " + ", ".join(missing)
)
def print_arm_summary(arms: list[dict[str, Any]]) -> None:
for arm in arms:
overrides = ", ".join(
f"{override['key']}={override['formula']}"
for override in arm.get("armOverrides", [])
)
print(
f"- {arm['armName']} control={arm.get('armControl')} overrides=[{overrides}]"
)
def resolve_folder(client: Any, folder_ref: str | None, *, create: bool) -> Any | None:
if folder_ref is None:
return None
folder = client.get_folder(folder_ref)
if folder is not None:
return folder
folder = client.get_folder_by_name(folder_ref, exact_match_only=True)
if folder is not None:
return folder
if not create:
raise ValueError(
f"Folder {folder_ref!r} was not found. Pass --create-folder to create it."
)
return client.create_folder(folder_ref)
def main() -> int:
parser = argparse.ArgumentParser(description="Create a Jinkō protocol design.")
parser.add_argument(
"--model-sid", help="Optional model SID to link and validate override keys."
)
parser.add_argument("--name", default="sdk-three-arm-protocol")
parser.add_argument(
"--description", default="Three-arm protocol design created with the SDK."
)
parser.add_argument("--dose-key", default="Dose")
parser.add_argument("--route-key", default="route")
parser.add_argument("--doses", default="1.0,2.0,3.0")
parser.add_argument("--routes", default="iv,po,iv")
parser.add_argument("--arm-names", default=",".join(DEFAULT_ARM_NAMES))
parser.add_argument("--control-arm", default="iv_low_dose")
parser.add_argument(
"--folder",
help="Existing folder id or exact folder name for the new protocol design.",
)
parser.add_argument(
"--create-folder",
action="store_true",
help="Create --folder when missing. Treats --folder as a folder name.",
)
parser.add_argument(
"--apply", action="store_true", help="Actually create the protocol design."
)
args = parser.parse_args()
if args.create_folder and not args.folder:
print("--create-folder requires --folder", file=sys.stderr)
return 1
try:
arms = build_arms(
dose_key=args.dose_key,
route_key=args.route_key,
doses=split_csv(args.doses),
routes=split_csv(args.routes),
arm_names=split_csv(args.arm_names),
control_arm=args.control_arm,
)
except ValueError as exc:
print(str(exc), file=sys.stderr)
return 1
print(f"Protocol arms: {len(arms)}")
print_arm_summary(arms)
if args.model_sid:
print(f"Model link: {args.model_sid}")
else:
print("Model link: none")
if not args.apply:
print(f"Would create protocol design named {args.name!r}.")
if args.folder:
action = "Would create or reuse" if args.create_folder else "Would reuse"
print(f"{action} folder: {args.folder}")
print("Run again with --apply to create the protocol design.")
return 0
load_env()
sdk = load_sdk()
if sdk is None:
return 1
JinkoClient, JinkoError = sdk
try:
client = JinkoClient()
folder = resolve_folder(client, args.folder, create=args.create_folder)
model = client.get_model(args.model_sid) if args.model_sid else None
if model is not None:
validate_model_override_keys(model, override_keys(arms))
protocol = client.create_protocol_design(
arms,
model=model,
folder=folder,
name=args.name,
description=args.description,
)
print(f"Created protocol design {protocol.sid}")
if folder is not None:
print(f"Folder: {folder.path}")
if getattr(protocol, "url", None):
print(protocol.url)
return 0
except (ValueError, JinkoError) as exc:
print(f"Protocol design creation failed: {exc}", file=sys.stderr)
return 2
if __name__ == "__main__":
raise SystemExit(main())
SHA-256: ba98cb0acbd91520414ea03561aa1a9af6956444ebc0a5a4b6badd0436cfb4bb