ptm-client
Lightweight Python client for the Prompt Test Manager (PTM) API. Single runtime dependency: requests.
Install
pip install ptm-client
Quick start
from ptm_client import PTMClient
client = PTMClient(base_url="https://ptm.example.com", token="your-api-token")
# List prompts, filtered by tag
prompts = client.list_prompts(tag="my_team")
# Fetch a prompt and its test cases
detail = client.get_prompt("my_team.summarizer")
tests = client.get_prompt_tests("my_team.summarizer")
# Run an eval against the library prompt
run = client.run_eval(
prompt_ids=["my_team.summarizer"],
provider_ids=["openai_gpt55"],
)
# Or run a one-off manual eval
run = client.run_manual_eval(
{
"prompt_text": "Summarize: {{text}}",
"tests": [{"description": "smoke", "vars": {"text": "Hello, world."}}],
"provider_profiles": ["openai_gpt55"],
}
)
# Block until complete, then fetch a report
result = client.wait_for_run(run["run_key"], timeout=120)
html = client.run_report(run["run_key"])
json_report = client.run_report(run["run_key"], format="json")
Constructor
PTMClient(base_url, token, timeout=30, *, verify_ssl=None)
token is a PTM personal access token or service account token. timeout is the HTTP request timeout in seconds.
verify_ssl controls TLS certificate verification. When None (the default), the value is read from the PTM_SSL_VERIFY environment variable, which defaults to true. Set PTM_SSL_VERIFY=false (or pass verify_ssl=False) to allow self-signed or otherwise invalid certificates - intended for local dev against a homelab PTM instance, never for production. The matching urllib3.InsecureRequestWarning is silenced when verification is disabled.
Public methods
Prompts
list_prompts(tag=None, team=None, service=None, source=None, search=None, group=None)get_prompt(prompt_id)get_prompt_tests(prompt_id)list_prompt_versions(prompt_id)(v0.3.0)get_prompt_version(prompt_id, version_number)(v0.3.0)
Providers
list_providers()
Evaluations
run_eval(prompt_ids, provider_ids, **kwargs)run_manual_eval(payload)run_prompt_eval(prompt_id, provider_ids, *, inject_vars=None, extra_tests=None, visibility_scope="org_visible", label=None)
Runs
list_runs(limit=50, terminal_only=False, mine_only=False)(v0.3.0)get_run(run_key)wait_for_run(run_key, timeout=300, poll_interval=5)wait_for_security_scan(scan_id, *, timeout=300, poll_interval=3)run_report(run_key, format="html")-html/json/markdown/csv
Optimization (v0.3.0)
submit_optimization(prompt_id, provider_profiles=None, judge_profile=None, max_cycles=10, target_score=90.0, min_improvement=2.0, max_cost_usd=20.0, comparison_strategy=None, visibility_scope=None, *, stability_samples=None, validation_samples=None, flakiness_threshold=None, min_consistent_improvement=None, variance_aware_mutator=None, variance_signal=None, enforce_target_score=None, chained_baseline_mode=None)(variance-aware fields added in v0.6.0;chained_baseline_modein v0.13.0; omit any to inherit server admin defaults)optimize_prompt(...)- deprecated alias forsubmit_optimization; emitsDeprecationWarning; removed in v1.0.0get_optimization_status(prompt_id)get_optimization_history(prompt_id)get_optimization_detail(optimization_id)(v0.3.0)cancel_optimization(optimization_id)wait_for_optimization(prompt_id, *, timeout=600, poll_interval=10)
Skills (v0.10.0)
list_skills(namespace=None, search=None, tags=None, include_deprecated=False, page=1, page_size=50, held=None, source=None, kind=None)-held=Truelists only skills on a security hold (withdisabled_reason,disabled_at,disabled_by);source(a skill source slug) andkind(owned/linked) (v0.24.0) narrow to linked skillsget_skill(skill_id_or_qname)get_skill_by_name(qualified_name)get_skills_by_prompt(prompt_id)publish_skill(prompt_id, payload, *, skill_name=None)update_skill(skill_id, payload)delete_skill(skill_id)install_skill_for_claude_code(skill_id, *, install_name=None)- writes bundle to~/.claude/skills/{slug}/invoke_skill(skill_id, inputs, *, provider_profile=None, amend=False)(v0.10.0;amendadded v0.14.0; DEPRECATED in v0.15.0 - emitsDeprecationWarningand routes toinvoke_skill_eval_ptm)invoke_skill_by_name(qualified_name, inputs, *, provider_profile=None, amend=False)(v0.14.0; in v0.15.0 routes toinvoke_skill_eval_ptm)invoke_prompt(prompt_id, inputs, *, provider_profile=None, amend=False)(v0.10.0;amendadded v0.14.0; DEPRECATED in v0.15.0 - routes toinvoke_prompt_eval_ptm)
Skill sources (v0.24.0, server 2.7.0)
Third-party Agent Skills repositories and Claude plugin marketplaces an admin or IT admin approved. Their skills are linked skills, named <source>:<name>, read-only, and installed into a folder named after their install_name (the upstream SKILL.md name), so an install is a spec-conformant Agent Skills folder for Claude Code, Codex or Hermes.
list_skill_sources(trust_state=None, kind=None)-{"items": [...]}; policy and sync health only formanage_skill_sourcesholders.request_skill_source(url, *, reason, provider=None, audience="org", audience_group_id=None)- request an https repository (provideris required for a self-hosted host); nothing syncs until it is approved.list_skill_source_entries(source_id, status=None, page=1, page_size=50)- every skill folder found upstream with its status (quarantined,available,blocked,unsupported, ...) and reason.- With skill sources disabled on the deployment these raise
PTMNotFoundError.
Security scanning (v0.22.0)
run_security_scan(target_type, target_id, *, deep=False, version_number=None, judge_profile=None, wait=False, timeout=300)-target_typeis"skill"(UUID) or"prompt"(prompt_id). Every scan is queued on the worker and returns thependingrow;wait=Trueblocks for the final row (completed, orstatus: "error", never a fabricated safe verdict) and raisesPTMTimeoutErroraftertimeoutseconds.deep=Trueruns an LLM-assisted scan.judge_profile(deep only) picks a judge-capable provider profile instead of the org default; the CLI flag is--judge. Each finding carriesevidence,explanationandremediationwhen the scanner provides them.get_security_scan(scan_id)(v0.23.0) - one scan row by id, for polling a queued scan; 404 when the caller cannot see its target.list_security_scans(target_type, target_id, *, limit=10, offset=0, version_number=None)- history, newest first;limitis 1-100.- The skill
target_idis the UUID; the CLI also accepts a qualified name or bare public slug and looks it up first. With scanning disabled on the deployment both methods raisePTMNotFoundError("Security scanning is not enabled"). Findings are attacker-authored text: display them, never execute them. - A skill on security hold raises
PTMLockedError(HTTP 423) from install, download, contents and invoke, and from the backing prompt'sget_prompt_contents,get_prompt_versionand invoke methods for callers who cannot edit the prompt and lackmanage_skill_catalog.exc.reasonis the manager's full recorded reason (the message only embeds a truncated preview of the body). - A block-mode security gate raises
PTMValidationErrorwhosesecurity_blockholds the structured detail (verdict,risk_score,findings_total,findings); publishing the prompt of a held skill raisesPTMConflictErrorwhosesecurity_holdholds the detail. Both messages name the security cause instead of the generic shape / retry hint.
Inline-vs-eval execution surfaces (v0.15.0)
PTM 1.16.x exposes three execution modes per kind (skill + prompt). Use the helper that matches the user's intent.
invoke_skill_eval_ptm(skill_id, inputs, provider_profile, *, judge_profile=None, timeout_seconds=60, amend=False)- blocking server-side eval; returns scored output and retains aEval: <qualified_name>row in run history.invoke_prompt_eval_ptm(prompt_id, inputs, provider_profile, ...)- prompt twin.invoke_skill_with_bundle(skill_id, inputs, provider_profile, ...)- returns the skill bundle for local execution AND enqueues a parallel server-side scored eval. Does not block on the eval. Response carries the bundle plusrun_key.invoke_skill_with_bundle_by_name(qualified_name, ...)- resolves the qualified name and forwards.invoke_prompt_with_bundle(prompt_id, inputs, provider_profile, ...)- prompt twin.record_skill_client_inline_run(skill_id, *, provider_label, duration_ms, input_keys, success)- 204 telemetry for client-side inline executions. All four kwargs are REQUIRED server-side; passing any one omitted raises HTTP 422. The endpoint sees only the names of input keys, never their values.record_prompt_client_inline_run(prompt_id, *, provider_label, duration_ms, input_keys, success)- prompt twin.
Contents + usage metrics (v0.14.0)
get_skill_contents(skill_id_or_qname)- fetch bundle in memory as{prompt_md, manifest, files}; no disk write, no LLM callget_prompt_contents(prompt_id, *, version=None)- fetch prompt contract as{prompt_text, version, tests, deepeval_metrics, tool_definitions}get_skill_usage_metrics(skill_id)-{lifetime, last_30d, last_7d, last_request_at}aggregated from fetch-event audit rows; returns null shape when org settinginline_usage_metrics_enabled=falseget_prompt_usage_metrics(prompt_id)- same shape for prompt fetch events
Groups (v0.13.0)
list_groups()create_prompt(payload)
Test-case shapes
PTM evaluates with three optional scoring layers. Use any combination.
Promptfoo assertions (deterministic)
Go in the assert array inside each test case:
{
"description": "mention the topic with enough length",
"vars": {"transcript": "..."},
"assert": [
{"type": "icontains", "value": "API migration"},
{"type": "javascript", "value": "output.length >= 100"},
],
}
DeepEval metrics (semantic, judge-LLM)
Go in additional_metrics at the payload root:
{
"additional_metrics": [
{"name": "relevance", "criteria": "Output addresses the input topic.", "threshold": 0.7},
],
"judge_profile": "openai_gpt56_terra",
}
KPI configs (custom weighted expressions)
Go in additional_kpis at the payload root:
{
"additional_kpis": [
{
"name": "cost_ok",
"description": "Under $0.05",
"expression": "1 if cost < 0.05 else 0",
"weight": 1.0,
},
],
}
Inline examples
run_manual_eval - full control
run = client.run_manual_eval(
{
"label": "my_custom_eval",
"prompt_text": "Summarize: {{text}}",
"tests": [{"description": "short text", "vars": {"text": "The quick brown fox."}}],
"provider_profiles": ["openai_gpt55"],
"cost_threshold": 1.0,
"latency_threshold_ms": 30000,
}
)
run_prompt_eval - fetch from PTM + inject live data
run = client.run_prompt_eval(
prompt_id="my_team.summarizer",
provider_ids=["openai_gpt55"],
inject_vars={"transcript": real_transcript, "meeting_title": "Weekly 1:1"},
)
result = client.wait_for_run(run["run_key"], timeout=120)
Error handling
from ptm_client import PTMClient, PTMError, PTMTimeoutError
try:
result = client.wait_for_run(run_key, timeout=60)
except PTMTimeoutError:
print("Run did not complete in time")
except PTMError as e:
print(f"PTM API error ({e.status_code}): {e}")
PTMError wraps all HTTP errors, ConnectionError, and requests.Timeout. Check e.status_code (0 for connection/timeout failures). Typed subclasses: PTMPermissionError (403), PTMNotFoundError (404), PTMConflictError (409; .security_hold on a held-skill republish), PTMValidationError (422; .security_block on a block-mode security gate), PTMLockedError (423, security hold; .reason).
CLI
ptm (or python -m ptm_client) reads PTM_API_BASE_URL and PTM_API_TOKEN from the environment.
| Command | What it does |
|---|---|
ptm install <skill> [--target-dir DIR] [--target-agent claude|codex|hermes] [--version N] [--project-dir DIR] |
Install a skill bundle by UUID or qualified name (a linked skill as <source>:<name>, into a folder named after its install_name). A security hold prints the reason and exits 1. |
ptm invoke <ref> --provider PROFILE [--type prompt|skill] [--input KEY=VALUE ...] [--timeout S] |
Run a prompt or skill through the eval pipeline. |
ptm security-scan <skill|prompt> <id> [--deep] [--version N] [--judge PROFILE] [--wait] [--timeout S] |
Queue a scan and print the row; --wait prints the final row. Writes a scan row and an audit event; --deep spends judge tokens. |
ptm security-scans <skill|prompt> <id> [--limit N] [--version N] |
Print scan history, newest first (read only). |
ptm sources list [--trust-state S] [--kind git_repo|plugin_marketplace] |
Print the skill sources you can see as JSON (read only). |
ptm sources request <url> --reason TEXT [--provider github|gitea|gitlab] |
Request a skill source and print it with its preview (records an audit event; an admin or IT admin approves). |
For both scan commands a skill <id> may be a UUID, a qualified name or a bare public slug. An invalid combination (--judge without --deep, --version on a skill) prints Invalid arguments: ... and exits 2 without sending the scan request.
Compatibility
- Python 3.12+
- PTM backend >= 1.9.0 required.
get_skill_contents,get_prompt_contents, and usage-metrics methods require PTM >= 1.16.0. Skill invoke methods require PTM >= 1.13.1. Older backends work for all other methods.
Metadata
Release files for ptm-client 0.24.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| ptm_client-0.24.0.tar.gz | 73.2 kB | Without provenance | Details for ptm_client-0.24.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| ptm_client-0.24.0-py3-none-any.whl | Python 3 | none | any | Details for ptm_client-0.24.0-py3-none-any.whl |
Total release size: 117.0 kB
Release files / ptm_client-0.24.0.tar.gz
| Download URL | ptm_client-0.24.0.tar.gz |
|---|---|
| Size | 73.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
50dc3a0fe58206202430e085ab610c21d3989eb96f92cec41e5ec62c03535bb5
|
|
BLAKE2b-256 checksum How to use checksums |
ced0b3e56da8ac317744791e5a5d7bd5cb258d67e043a29527ca4f891ffa9375
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.13.16
|
Release files / ptm_client-0.24.0-py3-none-any.whl
| Download URL | ptm_client-0.24.0-py3-none-any.whl |
|---|---|
| Size | 43.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
b0095b443fdc57a5ce92ea4469fe5e537c032949fdf3689fc3bee5fa3800bba8
|
|
BLAKE2b-256 checksum How to use checksums |
1c7102bcc01f50e4f44fbe3ac556955247514b61ef28a66547933be22e248b25
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.13.16
|
Security for ptm-client 0.24.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.