← Files Life Sciences DatabasesARCHIVED FILE
skills/clinicaltrials-skill/scripts/clinicaltrials_client.py
11.4 KB · Sep 30, 2026 · 23:00 UTC
#!/usr/bin/env python3
"""Compact ClinicalTrials.gov v2 helper for ChatGPT-imported skills."""
from __future__ import annotations
import json
import sys
from pathlib import Path
from typing import Any
sys.path.insert(0, str(Path(__file__).resolve().parents[3] / "scripts"))
from database_source_contract import apply_source_contract # noqa: E402
try:
import requests
except ImportError as exc: # pragma: no cover
requests = None
REQUESTS_IMPORT_ERROR = exc
else:
REQUESTS_IMPORT_ERROR = None
BASE_URL = "https://clinicaltrials.gov/api/v2"
PATHS = {
"studies": "/studies",
"metadata": "/studies/metadata",
"search_areas": "/studies/search-areas",
"enums": "/studies/enums",
"stats_size": "/stats/size",
"field_values": "/stats/field/values",
"field_sizes": "/stats/field/sizes",
}
def error(code: str, message: str, warnings: list[str] | None = None) -> dict[str, Any]:
return {
"ok": False,
"error": {"code": code, "message": message},
"warnings": warnings or [],
}
def _compact(value: Any, max_items: int, max_depth: int) -> Any:
if isinstance(value, str):
return value if len(value) <= 240 else value[:240] + "..."
if max_depth <= 0:
if isinstance(value, (dict, list)):
return "..."
return value
if isinstance(value, list):
out = [_compact(item, max_items, max_depth - 1) for item in value[:max_items]]
if len(value) > max_items:
out.append(f"... (+{len(value) - max_items} more)")
return out
if isinstance(value, dict):
out: dict[str, Any] = {}
items = list(value.items())
for key, item in items[:max_items]:
out[str(key)] = _compact(item, max_items, max_depth - 1)
if len(items) > max_items:
out["_truncated_keys"] = len(items) - max_items
return out
return value
def _require_int(name: str, value: Any, default: int) -> int:
if value is None:
return default
if not isinstance(value, int) or value <= 0:
raise ValueError(f"`{name}` must be a positive integer.")
return value
def _require_bool(name: str, value: Any, default: bool) -> bool:
if value is None:
return default
if not isinstance(value, bool):
raise ValueError(f"`{name}` must be a boolean.")
return value
def parse_input(payload: Any) -> dict[str, Any]:
if not isinstance(payload, dict):
raise ValueError("Input must be one JSON object.")
action = payload.get("action", "studies")
if action not in set(PATHS) | {"request"}:
raise ValueError(
"`action` must be one of: studies, metadata, search_areas, enums, stats_size, field_values, field_sizes, request."
)
params = payload.get("params") or {}
if not isinstance(params, dict):
raise ValueError("`params` must be an object.")
path = payload.get("path")
if action == "request":
if not isinstance(path, str) or not path.strip():
raise ValueError("`path` is required when `action=request`.")
return {
"action": action,
"path": path.strip() if isinstance(path, str) else PATHS.get(action),
"params": params,
"max_items": _require_int("max_items", payload.get("max_items"), 10),
"max_depth": _require_int("max_depth", payload.get("max_depth"), 3),
"max_pages": _require_int("max_pages", payload.get("max_pages"), 1),
"timeout_sec": _require_int("timeout_sec", payload.get("timeout_sec"), 30),
"save_raw": _require_bool("save_raw", payload.get("save_raw"), False),
"raw_output_path": payload.get("raw_output_path"),
}
def _save_raw(data: Any, raw_output_path: str | None) -> str:
path = Path(raw_output_path or "/tmp/clinicaltrials-response.json")
path.parent.mkdir(parents=True, exist_ok=True)
if isinstance(data, bytes):
raw_bytes = data
elif isinstance(data, str):
raw_bytes = data.encode("utf-8")
else:
raw_bytes = json.dumps(data, indent=2).encode("utf-8")
path.write_bytes(raw_bytes)
return str(path)
def execute(payload: Any) -> dict[str, Any]:
if requests is None:
return error("missing_dependency", f"`requests` is required: {REQUESTS_IMPORT_ERROR}")
config = parse_input(payload)
session = requests.Session()
session.headers.update({"Accept": "application/json"})
try:
if config["action"] == "studies":
next_page_token = None
if isinstance(config["params"].get("pageToken"), str):
next_page_token = config["params"]["pageToken"]
studies: list[Any] = []
pages: list[dict[str, Any]] = []
raw_pages: list[bytes] = []
total_count: int | None = None
pages_fetched = 0
for _ in range(config["max_pages"]):
params = dict(config["params"])
if next_page_token:
params["pageToken"] = next_page_token
response = session.get(
BASE_URL + config["path"],
params=params,
timeout=config["timeout_sec"],
)
response.raise_for_status()
if config["save_raw"]:
raw_content = getattr(response, "content", None)
raw_pages.append(
bytes(raw_content)
if isinstance(raw_content, (bytes, bytearray, memoryview))
else response.text.encode("utf-8")
)
page = response.json()
pages.append(page)
pages_fetched += 1
page_studies = page.get("studies") if isinstance(page.get("studies"), list) else []
studies.extend(page_studies)
if isinstance(page.get("totalCount"), int):
total_count = page["totalCount"]
next_page_token = (
page.get("nextPageToken")
if isinstance(page.get("nextPageToken"), str)
else None
)
if not next_page_token:
break
raw_output_path = None
raw_page_paths: list[str] = []
if config["save_raw"] and raw_pages:
raw_output_path = _save_raw(raw_pages[0], config["raw_output_path"])
raw_page_paths.append(raw_output_path)
primary_path = Path(raw_output_path)
for index, raw_page in enumerate(raw_pages[1:], start=2):
page_path = primary_path.with_name(
f"{primary_path.stem}.page-{index}{primary_path.suffix}"
)
raw_page_paths.append(_save_raw(raw_page, str(page_path)))
available = total_count if isinstance(total_count, int) else len(studies)
records = _compact(
studies[: config["max_items"]],
config["max_items"],
config["max_depth"],
)
if isinstance(records, list):
for record, study in zip(records, studies, strict=False):
if not isinstance(record, dict) or not isinstance(study, dict):
continue
protocol = study.get("protocolSection")
identification = (
protocol.get("identificationModule") if isinstance(protocol, dict) else None
)
nct_id = (
identification.get("nctId")
if isinstance(identification, dict)
else study.get("nctId")
)
if isinstance(nct_id, str):
record.setdefault("nctId", nct_id)
output = {
"ok": True,
"source": "clinicaltrials-v2",
"action": "studies",
"pages_fetched": pages_fetched,
"next_page_token": next_page_token,
"record_count_returned": min(len(studies), config["max_items"]),
"record_count_available": available,
"truncated": len(studies) > config["max_items"] or next_page_token is not None,
"records": records,
"raw_output_path": raw_output_path,
"warnings": [],
}
if len(raw_page_paths) > 1:
output["raw_page_paths"] = raw_page_paths
return apply_source_contract(
output,
"clinicaltrials-skill",
BASE_URL + config["path"],
)
response = session.get(
BASE_URL + config["path"],
params=config["params"],
timeout=config["timeout_sec"],
)
response.raise_for_status()
data = response.json()
raw_content = getattr(response, "content", None)
raw_response = (
bytes(raw_content)
if isinstance(raw_content, (bytes, bytearray, memoryview))
else response.text.encode("utf-8")
)
raw_output_path = (
_save_raw(raw_response, config["raw_output_path"]) if config["save_raw"] else None
)
target = data
top_keys = list(data)[: config["max_items"]] if isinstance(data, dict) else None
if isinstance(data, dict):
for key in ("studies", "fields", "areas", "enums", "values", "sizes"):
value = data.get(key)
if isinstance(value, list):
target = value
break
output = {
"ok": True,
"source": "clinicaltrials-v2",
"action": config["action"],
"raw_output_path": raw_output_path,
"warnings": [],
}
if isinstance(target, list):
output.update(
{
"record_count_returned": min(len(target), config["max_items"]),
"record_count_available": len(target),
"truncated": len(target) > config["max_items"],
"records": _compact(
target[: config["max_items"]],
config["max_items"],
config["max_depth"],
),
}
)
else:
output["summary"] = _compact(target, config["max_items"], config["max_depth"])
output["top_keys"] = top_keys
return apply_source_contract(
output,
"clinicaltrials-skill",
BASE_URL + config["path"],
mode="metadata" if config["action"] != "request" else None,
)
except ValueError as exc:
return error("invalid_response", str(exc))
except requests.RequestException as exc:
return error(
"network_error",
f"ClinicalTrials.gov request failed ({type(exc).__name__}).",
)
finally:
session.close()
def main() -> int:
try:
payload = json.load(sys.stdin)
except Exception as exc: # noqa: BLE001
sys.stdout.write(json.dumps(error("invalid_json", f"Could not parse JSON input: {exc}")))
return 2
try:
output = execute(payload)
except ValueError as exc:
output = error("invalid_input", str(exc))
code = 2
else:
code = 0 if output.get("ok") else 1
sys.stdout.write(json.dumps(output))
return code
if __name__ == "__main__":
raise SystemExit(main())
SHA-256: 124b40281b96040790b2cc04adbc48223227303679b24e6834719c1e7c685230